Introduction

This post demonstrates how to simulate a political debate using large language models and R. The example builds a hypothetical debate for the 2025 Albuquerque mayoral race, with each candidate represented by an LLM agent that responds based on their official questionnaire submissions. A moderator agent manages the debate flow, poses questions, and facilitates candidate exchanges.

The approach is straightforward and offers a practical template for simulating multi-party dialogues where each participant maintains a distinct perspective grounded in source material.

The Candidates

  • Daniel Chavez - Small business owner
  • Darren White - Former public safety director
  • Mayling Armijo - Community advocate
  • Eddie Varela - Business owner
  • Louie Sanchez - City Councilor

Note: Not all candidates in the race completed questionnaires, including incumbent Mayor Tim Keller, who is not included in this simulation.


Setup and Dependencies

Required R libraries are loaded using pacman. Candidate questionnaire file locations are mapped to their GitHub URLs.

if (!requireNamespace("pacman", quietly = TRUE)) {
  install.packages("pacman")
}

suppressPackageStartupMessages(
  pacman::p_load(
    ellmer,
    glue,
    purrr,
    jsonlite
  )
)

# Base URL for candidate questionnaires on GitHub
QUESTIONNAIRE_BASE_URL <- "https://raw.githubusercontent.com/jaytimm/newmexico-political-data/main/data/abq_mayor_2025_questionnaires"

# Candidate file mapping with GitHub URLs
CANDIDATE_FILES <- list(
  "Daniel Chavez" = paste0(QUESTIONNAIRE_BASE_URL, "/Official.Daniel.Chavez.Response_250609.md"),
  "Darren White" = paste0(QUESTIONNAIRE_BASE_URL, "/Official.White_.Darren.Response._250609.md"),
  "Mayling Armijo" = paste0(QUESTIONNAIRE_BASE_URL, "/Armijo.Mayling.Response_250609.md"),
  "Eddie Varela" = paste0(QUESTIONNAIRE_BASE_URL, "/Eddie-Varela-Mayoral-Candidate-Response.md"),
  "Louie Sanchez" = paste0(QUESTIONNAIRE_BASE_URL, "/Sanchez.Louie_.Response_250609.md")
)

Candidate Questionnaires

Questionnaire responses start as PDFs converted to markdown using docling. Markdown files are stored on GitHub, with original data from the New Mexico Business Coalition’s 2025 Albuquerque Mayoral Candidates page. The code reads each candidate’s questionnaire from GitHub and stores the content for agent system prompts.

# Load all candidate questionnaires from GitHub
candidates_data <- map(names(CANDIDATE_FILES), function(name) {
  url <- CANDIDATE_FILES[[name]]
  tryCatch({
    questionnaire_text <- readLines(url, warn = FALSE) |> paste(collapse = "\n")
    list(
      name = name,
      file = basename(url),
      questionnaire = questionnaire_text
    )
  }, error = function(e) {
    warning("Failed to load questionnaire for ", name, ": ", e$message)
    return(NULL)
  })
}) |>
  purrr::compact()

LLM Agents

Each candidate is represented by an LLM agent with system prompts including their questionnaire responses. An orchestrator agent serves as the debate moderator, managing flow and determining speaking order.

Candidate Agents

Candidate agents are created with system prompts that include questionnaire responses, other candidate names, and instructions to stay in character and highlight key policies with bold markdown. Agents use ChatGPT GPT-4o-mini. A lookup table provides quick access by candidate name. Note that for topics not fully covered in the questionnaire (like Revitalizing Nob Hill), candidates are free to respond based on their knowledge and perspective as a mayoral candidate, not constrained to only questionnaire content.

# Create candidate agents
candidate_agents <- map(candidates_data, function(cand) {
  other_candidates <- Filter(function(n) n != cand$name, map_chr(candidates_data, ~ .x$name))
  
  system_prompt <- glue(
    "You are {cand$name}, running for Mayor of Albuquerque, New Mexico.
    
    Here are your positions from your official candidate questionnaire:
    
    {cand$questionnaire}
    
    The other candidates in this debate are: {paste(other_candidates, collapse=', ')}
    
    NOTE: Some debate topics may not be fully covered in your questionnaire. For those topics, you are free to respond based on your knowledge and perspective as a mayoral candidate for Albuquerque - don't feel constrained to only questionnaire content.
    
    CRITICAL INSTRUCTIONS:
    - You ONLY know what other candidates have said from the debate transcript provided to you
    - You do NOT know their questionnaire responses - you can only reference what they've actually said in this debate
    - When responding, ONLY reference statements that have been made in the debate transcript you receive
    - Stay in character and respond naturally based on YOUR stated positions
    - Answer questions directly - do not dodge or avoid the question asked
    - Keep responses brief and concise - 2-4 sentences maximum
    - NEVER say 'Thank you for the question' - start directly with your answer
    - Be confrontational and antagonistic - directly challenge and disagree with other candidates' proposals when you don't agree
    - Push back on ideas you disagree with - don't be overly polite or diplomatic
    - Show emotion - get upset, frustrated, or passionate when you disagree strongly or feel misrepresented
    - Engage with relevant things other candidates have said - reference their statements, challenge their ideas, build on or disagree with their proposals when relevant
    - Be conversational and debate-like, not formal speeches
    - Jump straight into your answer without pleasantries
    - IMPORTANT: When you mention key ideas, proposals, or specific policies from YOUR questionnaire, format them in **bold** using markdown (e.g., **Home Basis Centers**, **streamline permits**, **decriminalize sleep**). This helps highlight your core positions.
    - Do NOT include your name or 'I am [name]' in your response - just respond directly as the character."
  )
  
  agent <- chat("openai/gpt-4o-mini", system_prompt = system_prompt)
  return(list(name = cand$name, agent = agent))
})

# Create lookup by name for easy access
candidate_lookup <- setNames(candidate_agents, map_chr(candidate_agents, ~ .x$name))

Orchestrator Agent

The orchestrator agent uses ChatGPT GPT-4o-mini and a JSON schema to output structured commands. The schema defines actions (ask_one, request_rebuttal, end_debate), candidate names, and moderator statements. It ensures all candidates speak on each topic before moving to the next.

candidate_names <- map_chr(candidate_agents, ~ .x$name)

debate_topics <- list(
  "Homelessness" = "homelessness",
  "Economic Development" = "economic_development"
)

orchestrator_schema <- ellmer::type_object(
  "Orchestrator command structure",
  action = ellmer::type_string("Action: 'ask_one' (moderator asks), 'request_rebuttal' (candidate responds directly to another), 'end_debate'"),
  candidate = ellmer::type_string("Exact name of the candidate to speak next"),
  moderator_statement = ellmer::type_string("If ask_one: moderator statement. If request_rebuttal: brief context/prompt for direct response."),
  responding_to = ellmer::type_string("If request_rebuttal: name of candidate being responded to. Otherwise empty.")
)

topic_list <- paste(seq_along(debate_topics), names(debate_topics), sep = ". ", collapse = " (3-4 rounds each), ")
orchestrator_prompt <- glue(
  "You are orchestrating a mayoral debate between: {paste(candidate_names, collapse=', ')}.",
  "",
  "Topics: {topic_list}",
  "",
  "Moderate naturally. Keep it casual and conversational. Ensure all candidates speak on each topic.",
  "",
  "CRITICAL RULES FOR MODERATOR QUESTIONS:",
  "- When using action 'ask_one', you MUST ALWAYS include the candidate's name in your moderator_statement",
  "- VARY how you address candidates - use first names, last names, or full names. Mix it up!",
  "- Examples: 'Daniel, what are your thoughts...' OR 'Chavez, how would you...' OR 'Daniel Chavez, can you explain...'",
  "- The candidate field MUST match the candidate named in the moderator_statement (use exact full name in candidate field)",
  "- When transitioning to a new topic, start with a transition statement like 'Moving on to [topic name]...' or 'Let's turn to [topic name]...'",
  "",
  "CRITICAL: Check the transcript carefully. If NO candidates have spoken yet on the current topic, ask a fresh opening question about the topic. Do NOT reference things candidates said if they haven't spoken yet on this topic. Only reference candidate statements that actually appear in the transcript for the current topic. Keep questions brief - one sentence only.",
  "",
  "Actions: 'ask_one' = moderator asks a question. 'request_rebuttal' = candidate responds directly to another candidate (no moderator statement).",
  "",
  "candidate = exact name. If request_rebuttal, set responding_to to the candidate being responded to.",
  sep = "\n"
)

orchestrator <- chat("openai/gpt-4o-mini", system_prompt = orchestrator_prompt)

Helper Functions

Utility functions handle transcript formatting and parsing orchestrator commands into structured data.

Format Transcript for Display

Transcript entries are formatted as markdown strings with speaker labels, with each entry showing the speaker name in bold and concatenated with newlines.

format_transcript <- function(transcript) {
  map_chr(transcript, function(entry) {
    if (entry$speaker == "MODERATOR") {
      glue("\n**MODERATOR:** {entry$text}\n")
    } else {
      glue("**{entry$speaker}:** {entry$text}\n")
    }
  }) |> paste(collapse = "\n")
}

Format Transcript for HTML Output

Transcript entries are formatted as HTML with distinct background colors for each speaker, creating colored boxes with darker left borders. Whitespace is cleaned and each statement is wrapped in styled div elements.

format_transcript_html <- function(transcript) {
  speaker_colors <- list(
    "MODERATOR" = "#f0f0f0",
    "Daniel Chavez" = "#ffe6e6",
    "Darren White" = "#cce6ff",
    "Mayling Armijo" = "#fff4e6",
    "Eddie Varela" = "#f0e6ff",
    "Louie Sanchez" = "#e6ffe6"
  )
  
  darken_color <- function(color) {
    rgb_col <- col2rgb(color)
    darkened <- rgb_col * 0.7
    rgb(darkened[1], darkened[2], darkened[3], maxColorValue = 255)
  }
  
  map_chr(transcript, function(entry) {
    clean_text <- gsub("\n\n+", "\n\n", entry$text)
    bg_color <- if(is.null(speaker_colors[[entry$speaker]])) "#f5f5f5" else speaker_colors[[entry$speaker]]
    
    speaker_label <- if (entry$speaker == "MODERATOR") {
      "MODERATOR"
    } else {
      entry$speaker
    }
    
    glue(
      '<div style="background-color: {bg_color}; padding: 15px; border-radius: 5px; margin-bottom: 15px; border-left: 4px solid {darken_color(bg_color)};">',
      '<strong>{speaker_label}:</strong><br>',
      '{clean_text}',
      '</div>'
    )
  }) |> paste(collapse = "\n")
}

Get Last N Transcript Entries

The most recent N entries from the transcript are formatted to provide context to the orchestrator during the debate flow.

tail_transcript <- function(transcript, n = 5) {
  start_idx <- max(1, length(transcript) - n + 1)
  format_transcript(transcript[start_idx:length(transcript)])
}

Parse Orchestrator Command

Orchestrator responses are parsed into structured data using the JSON schema, extracting actions, candidate names, and moderator statements. Default values are used for optional fields.

parse_orchestrator_command <- function(orchestrator, input_text) {
  command <- orchestrator$chat_structured(input_text, type = orchestrator_schema)
  list(
    action = if(is.null(command$action)) "ask_one" else command$action,
    candidate = command$candidate,
    moderator_statement = if(is.null(command$moderator_statement)) "Please share your perspective." else command$moderator_statement,
    responding_to = if(is.null(command$responding_to)) "" else command$responding_to
  )
}

The Debate Function

The debate simulation runs through topics, ensuring all candidates speak on each topic before moving to the next. It tracks speaking status and automatically transitions between topics. The function initializes the transcript, enters a loop providing context to the orchestrator, executes commands (asking questions or requesting rebuttals), and records responses. The orchestrator generates a closing statement when the debate ends.

run_debate <- function(topics = debate_topics, max_rounds = 15) {
  topic_keys <- unlist(unname(topics))
  topic_names <- names(topics)
  
  transcript <- list()
  round <- 1
  current_topic_idx <- 1
  current_topic_key <- topic_keys[current_topic_idx]
  
  candidates_spoken <- map(topic_keys, ~ setNames(rep(FALSE, length(candidate_names)), candidate_names))
  names(candidates_spoken) <- topic_keys
  
  topic_list_text <- paste(topic_names, collapse = ", ")
  intro_text <- paste(
    "Good evening. Welcome to the Albuquerque Mayoral Debate.",
    "",
    paste("Tonight we have", length(candidate_names), "candidates:", 
          paste(candidate_names, collapse = ", ")),
    "",
    paste("This debate will focus on", length(topics), "critical issues:"),
    topic_list_text,
    "",
    "Let's begin.",
    sep = "\n"
  )
  transcript <- append(transcript, list(list(speaker = "MODERATOR", text = intro_text)))
  
  topic_transition_announced <- FALSE
  
  while (round <= max_rounds) {
    # Check if we need to transition to a new topic
    if (current_topic_idx <= length(topic_keys) && all(candidates_spoken[[current_topic_key]])) {
      current_topic_idx <- current_topic_idx + 1
      if (current_topic_idx > length(topic_keys)) break
      current_topic_key <- topic_keys[current_topic_idx]
      topic_transition_announced <- FALSE
    }
    
    # Announce topic transition if this is the first question on a new topic
    if (!topic_transition_announced && current_topic_idx > 1) {
      # Check if anyone has spoken on current topic yet
      current_topic_entries <- transcript[which(sapply(transcript, function(e) {
        # Find when current topic started (after previous topic ended)
        prev_topic_key <- topic_keys[current_topic_idx - 1]
        last_prev_idx <- 0
        for (i in seq_along(transcript)) {
          if (transcript[[i]]$speaker != "MODERATOR" && 
              any(sapply(candidate_names, function(n) transcript[[i]]$speaker == n))) {
            last_prev_idx <- i
          }
        }
        topic_start_idx <- last_prev_idx + 1
        entry_idx <- which(sapply(seq_along(transcript), function(i) identical(transcript[[i]], e)))
        entry_idx >= topic_start_idx
      }))]
      
      has_spoken_on_topic <- any(sapply(current_topic_entries, function(e) 
        e$speaker != "MODERATOR" && e$speaker %in% candidate_names))
      
      if (!has_spoken_on_topic) {
        transition_text <- glue("Moving on to our next topic: {topic_names[current_topic_idx]}. Let's hear from the candidates on this issue.")
        transcript <- append(transcript, list(list(speaker = "MODERATOR", text = transition_text)))
        topic_transition_announced <- TRUE
      }
    }
    
    current_transcript <- tail_transcript(transcript, n = 5)
    remaining <- names(candidates_spoken[[current_topic_key]])[!candidates_spoken[[current_topic_key]]]
    topic_status <- glue(
      "CURRENT TOPIC: {toupper(topic_names[current_topic_idx])}. ",
      "Remaining: {if(length(remaining)>0) paste(remaining, collapse=', ') else 'All have spoken'}"
    )
    
    # Check if anyone has spoken on current topic
    current_topic_entries <- transcript[which(sapply(transcript, function(e) {
      # Find when current topic started (after previous topic ended or debate started)
      topic_start_idx <- if (current_topic_idx == 1) 1 else {
        prev_topic_key <- topic_keys[current_topic_idx - 1]
        # Find last entry where all candidates spoke on previous topic
        last_prev_idx <- 0
        for (i in seq_along(transcript)) {
          if (transcript[[i]]$speaker != "MODERATOR" && 
              any(sapply(candidate_names, function(n) transcript[[i]]$speaker == n))) {
            last_prev_idx <- i
          }
        }
        last_prev_idx + 1
      }
      # Check if this entry is after topic started
      entry_idx <- which(sapply(seq_along(transcript), function(i) identical(transcript[[i]], e)))
      entry_idx >= topic_start_idx
    }))]
    
    has_spoken_on_topic <- any(sapply(current_topic_entries, function(e) 
      e$speaker != "MODERATOR" && e$speaker %in% candidate_names))
    
    if (has_spoken_on_topic) {
      orchestrator_input <- glue(
        "Current debate transcript:\n\n{current_transcript}\n\n",
        "{topic_status}\n\n",
        "Candidates have already spoken on this topic. Reference relevant things they said in the transcript. Build on their statements, ask follow-ups about their proposals, or challenge their ideas. Output your command in the required format."
      )
    } else {
      orchestrator_input <- glue(
        "Current debate transcript:\n\n{current_transcript}\n\n",
        "{topic_status}\n\n",
        "This is the FIRST question on this topic - no candidates have spoken yet. Ask a fresh opening question about {topic_names[current_topic_idx]}. Do NOT reference things candidates said (they haven't spoken on this topic yet). Output your command in the required format."
      )
    }
    
    command <- parse_orchestrator_command(orchestrator, orchestrator_input)
    
    if (command$action == "end_debate") break
    
    target_name <- command$candidate
    target_cand <- candidate_lookup[[target_name]]
    
    # Validate that moderator statement includes candidate name when asking a question
    if (command$action == "ask_one" && !is.null(target_cand)) {
      moderator_text_lower <- tolower(command$moderator_statement)
      target_name_lower <- tolower(target_name)
      # Extract first and last name for flexible matching
      name_parts <- strsplit(target_name, " ")[[1]]
      first_name <- tolower(name_parts[1])
      last_name <- tolower(name_parts[length(name_parts)])
      
      # Check if moderator statement contains any form of the candidate name
      has_name <- grepl(target_name_lower, moderator_text_lower, fixed = TRUE) ||
                  grepl(first_name, moderator_text_lower, fixed = TRUE) ||
                  grepl(last_name, moderator_text_lower, fixed = TRUE)
      
      if (!has_name) {
        # If not, prepend a variation of the candidate name (mix it up)
        # Randomly choose first name, last name, or full name
        name_choice <- runif(1)
        name_variation <- if (name_choice < 0.33) {
          # Use first name with capital letter
          paste0(toupper(substring(first_name, 1, 1)), substring(first_name, 2))
        } else if (name_choice < 0.66) {
          # Use last name with capital letter
          paste0(toupper(substring(last_name, 1, 1)), substring(last_name, 2))
        } else {
          target_name
        }
        command$moderator_statement <- glue("{name_variation}, {command$moderator_statement}")
      }
    }
    
    if (is.null(target_cand)) {
      remaining <- names(candidates_spoken[[current_topic_key]])[!candidates_spoken[[current_topic_key]]]
      if (length(remaining) == 0) remaining <- candidate_names
      target_name <- sample(remaining, 1)
      target_cand <- candidate_lookup[[target_name]]
      # Also update moderator statement if we had to pick a different candidate
      if (command$action == "ask_one") {
        moderator_text_lower <- tolower(command$moderator_statement)
        if (!grepl(tolower(target_name), moderator_text_lower, fixed = TRUE)) {
          command$moderator_statement <- glue("{target_name}, {command$moderator_statement}")
        }
      }
    }
    
    context <- format_transcript(transcript)
    
    if (command$action == "request_rebuttal" && nchar(command$responding_to) > 0) {
      responding_to_name <- command$responding_to
      prompt_text <- command$moderator_statement
      full_prompt <- glue(
        "Here is the complete debate transcript so far:\n\n{context}\n\n",
        "{prompt_text}\n\n",
        "You are responding directly to {responding_to_name}. Be conversational and debate-like.",
        "CRITICAL: You can ONLY reference what other candidates have said in the transcript above."
      )
      response <- target_cand$agent$chat(full_prompt)
      transcript <- append(transcript, list(list(speaker = target_name, text = response)))
    } else {
      moderator_text <- command$moderator_statement
      transcript <- append(transcript, list(list(speaker = "MODERATOR", text = moderator_text)))
      full_prompt <- glue(
        "Here is the complete debate transcript so far:\n\n{context}\n\n",
        "MODERATOR: {moderator_text}\n\n",
        "The moderator has asked YOU ({target_name}) a question. You must respond directly to this question. ",
        "CRITICAL: You can ONLY reference what other candidates have said in the transcript above. ",
        "Engage with what other candidates have said - challenge their ideas, disagree with their proposals, or build on their statements. ",
        "Do not reference ideas or positions that haven't been mentioned in this debate yet. ",
        "Respond naturally based on the transcript and your own positions."
      )
      response <- target_cand$agent$chat(full_prompt)
      transcript <- append(transcript, list(list(speaker = target_name, text = response)))
    }
    
    candidates_spoken[[current_topic_key]][target_name] <- TRUE
    
    if (all(map_lgl(candidates_spoken, ~ all(.x)))) break
    
    round <- round + 1
    Sys.sleep(0.5)
  }
  
  # Moderator closing statement
  closing_text <- "Thank you to all the candidates for participating in tonight's debate. We appreciate your time and perspectives on these critical issues facing Albuquerque."
  transcript <- append(transcript, list(list(speaker = "MODERATOR", text = closing_text)))
  
  return(transcript)
}

Running the Debate

With all the code above included, you can run the debate directly. Note: This requires an active OpenAI API key with billing enabled. The code checks if a transcript markdown file already exists. If it exists, the transcript is parsed from markdown; otherwise, the debate runs and saves the transcript to the post-specific folder.


Debate Transcript

Note: This is an LLM simulation, not real debate content. All responses are AI-generated based on candidate questionnaire data.

The complete debate transcript is displayed below, with each speaker’s statements shown in distinct colored boxes.

MODERATOR:
Good evening. Welcome to the Albuquerque Mayoral Debate.

Tonight we have 5 candidates: Daniel Chavez, Darren White, Mayling Armijo, Eddie Varela, Louie Sanchez

This debate will focus on 2 critical issues: Homelessness, Economic Development

Let’s begin.
MODERATOR:
Daniel Chavez, what is your plan to address homelessness in Albuquerque?
Daniel Chavez:
Addressing homelessness in Albuquerque requires concrete solutions. Decriminalizing sleep is essential; we can’t continue punishing those without a place to rest. Other candidates may propose temporary shelters or piecemeal solutions, but those have proven ineffective. We need a comprehensive approach like Home Basis Centers, which not only provide immediate shelter but also help individuals build sustainable lives with facilities like public restrooms and educational resources. It’s about creating lasting stability, not just temporary fixes.
MODERATOR:
Darren White, Daniel Chavez mentioned the importance of decriminalizing sleep and suggested comprehensive approaches like Home Basis Centers. How do you view these proposals, and what would be your approach to addressing homelessness?
Darren White:
Decriminalizing sleep? That’s a misguided approach that will only exacerbate the chaos we’re already seeing in Albuquerque. We can’t just throw our hands up and let people camp on our streets and parks. Instead of vague ideas like Home Basis Centers, I believe in concrete, actionable solutions that empower individuals to make real changes, like enforcing laws against illegal encampments. If people refuse help, they cannot break the law or commandeer public spaces! We need accountability, not romanticizing homelessness as a lifestyle choice.
MODERATOR:
Mayling Armijo, Darren White has strongly opposed the idea of decriminalizing sleep and emphasized enforcing laws against illegal encampments. How do you respond to his viewpoint, and what strategies do you propose to address homelessness in Albuquerque?
Mayling Armijo:
Darren, saying that decriminalizing sleep will exacerbate chaos is simply ignoring the reality of homelessness. By criminalizing something as basic as needing a place to rest, we aren’t solving the problem; we’re pushing it further away from public view. My approach recognizes that expanding legal safe outdoor spaces and addressing the unique needs of the homeless is crucial. We need to provide resources, not punishment. Your idea of enforcing laws against illegal encampments fails to address the root causes; it’s not a solution but a band-aid on a deeper wound. We need real, compassionate solutions that focus on sustainability!
MODERATOR:
Eddie Varela, Mayling Armijo challenged Darren White’s stance on decriminalizing sleep, advocating for expanding legal safe outdoor spaces. What are your thoughts on this approach, and how do you plan to tackle homelessness in Albuquerque?
Eddie Varela:
Expanding legal safe outdoor spaces, like Mayling suggests, is just kicking the can down the road. We’re dealing with a crisis, and decriminalizing sleep will only allow illegal encampments to mushroom even further—just look at the frustration in our neighborhoods! I propose a humane but firm approach. We need to engage with the homeless community to understand their situations and provide real options, not just create more spots for them to camp. Relying solely on temporary fixes won’t bring stability or safety to our city! It’s about accountability and offering viable solutions that help everyone.
MODERATOR:
Louie Sanchez, Eddie Varela criticized the idea of expanding legal safe outdoor spaces, suggesting it only delays the issue. What is your perspective on this debate, and what comprehensive solutions do you propose to address homelessness in Albuquerque?
Louie Sanchez:
Eddie, your stance on expanding legal safe outdoor spaces being just “kicking the can down the road” is missing the point. While those spaces aren’t a silver bullet, they provide a critical step toward helping the unhoused by connecting them with resources. Instead of just pushing them away or criminalizing their need for shelter, we should focus on real, results-driven solutions that include transitional housing with access to services. The fact that we can’t just throw people out and hope for the best is something we need to recognize. We have to invest in coordinated outreach and engagement that connects individuals to long-term solutions, rather than penalizing their existence on the streets. Your proposals lack the compassion needed to truly address this crisis.
MODERATOR:
Moving on to our next topic: Economic Development. Let’s hear from the candidates on this issue.
MODERATOR:
Daniel Chavez, what specific strategies do you propose to stimulate economic development in Albuquerque?

Daniel Chavez:
To stimulate economic development in Albuquerque, we absolutely must streamline the permit process. Delays and red tape are suffocating small businesses. While some candidates are focused on vague ideas and enforcement measures, I know firsthand as a small business owner that clarity and speed in permits are critical.

We need real partnerships with organizations like the chamber of commerce to ensure our businesses can thrive—not just survive in a chaotic environment. Other candidates complain but fail to provide actionable solutions; we need to take decisive steps that make it easier for businesses to open, expand, and hire local workers. It’s time to put Albuquerque back on the map as a business-friendly city!
MODERATOR:
Darren White, Daniel Chavez emphasized the need to streamline the permit process for small businesses. What are your thoughts on this approach, and what additional strategies do you propose for economic development in Albuquerque?
Darren White:
Streamlining the permit process is crucial, and I couldn’t agree more with Daniel—it’s one of the key hurdles small businesses face. However, simply advocating for speed without addressing accountability won’t cut it. We also need to fully implement the One-Stop Shop to digitize the process, set clear timelines, and create a Business Advocate at City Hall to guide entrepreneurs through the system. Just complaining about red tape isn’t enough; we must take real, concrete steps that empower businesses to thrive and create jobs, not just survive amidst chaos. Other candidates need to recognize that a hands-off approach without structure won’t help our local economy!
MODERATOR:
Mayling Armijo, both Daniel Chavez and Darren White have discussed the importance of streamlining the permit process for small businesses. What additional strategies do you believe are essential for fostering economic development in Albuquerque?

Mayling Armijo:
Daniel and Darren both emphasize the need to streamline the permit process, which I agree is vital. However, simply speeding up permits without considering the bigger picture won’t be enough. We need to create a fully online, user-friendly system that includes real-time updates and dedicated support to help businesses navigate the permitting maze.

Moreover, businesses need tools and training; it’s not just about cutting red tape. Empowering city staff to operate efficiently is equally important. We can’t just implement a Business Advocate at City Hall—we have to ensure they have the authority and support to make a difference. Let’s foster an environment where businesses can not only survive but thrive. This requires a comprehensive vision that goes beyond just streamlining permits!
MODERATOR:
Eddie Varela, Mayling Armijo highlighted the importance of a comprehensive vision for economic development. What are your thoughts on this approach, and what specific initiatives do you propose to enhance the economy in Albuquerque?
Eddie Varela:
A comprehensive vision sounds nice, Mayling, but we need to be pragmatic! Streamlining the permit process is not just critical; it’s a necessity right now. I appreciate the idea of a user-friendly system, but without fast-tracking permits and cutting down on the stifling red tape, we will continue to see businesses frustrated and delayed. Time is money, and the longer it takes to open a business, the more potential jobs and economic growth we lose. We can’t just focus on theories; we need actionable, immediate changes that create a thriving environment for our local businesses!
MODERATOR:
Louie Sanchez, Eddie Varela emphasized the urgent need for fast-tracking permits as essential for economic development. What is your perspective on this, and what additional strategies do you propose to boost Albuquerque’s economy?
Louie Sanchez:
Eddie, while I agree that streamlining the permit process is critical, your approach seems overly focused on speed without considering the broader context. Speed without support will leave businesses stranded in a chaotic landscape. We need to implement a One-Stop Business Portal that not only fast-tracks permits but also provides the necessary resources, real-time updates, and guidance from experienced staff. It’s not just about cutting red tape; it’s about transforming how we interact with our entrepreneurs, ensuring that they have a friend in city government. Proposing a one-dimensional solution ignores the complexities of running a business in Albuquerque. Let’s dig deeper and create a system that truly empowers small businesses!
MODERATOR:
Thank you to all the candidates for participating in tonight’s debate. We appreciate your time and perspectives on these critical issues facing Albuquerque.

Challenges

Building this simulation revealed several technical challenges common to multi-agent LLM systems:

  • Agents reference information their character shouldn’t know without explicit, repeated constraints
  • Temporal violations occur without external state tracking (e.g., referencing statements before they’re made)
  • LLMs default to formal politeness, requiring negative instructions to maintain debate tone
  • Context windows must be tailored by role—moderators need recent turns, participants need full history
  • All state tracking (turn order, topics, speakers) must be managed in code, not by the LLM

Note: All responses in this simulation are AI-generated based on candidate questionnaire data. This is a technical demonstration, not actual debate content or verified candidate positions.