CopilotKit

Agent Config

Forward typed configuration from your UI into the agent's reasoning loop.


"""LlamaIndex agent backing the Agent Config Object demo.Mirrors `langgraph-python/src/agents/agent_config_agent.py`. The LangGraphoriginal reads three forwarded properties — `tone`, `expertise`,`responseLength` — from the run's `RunnableConfig.configurable.properties`and composes the system prompt dynamically per turn.`get_ag_ui_workflow_router` does not expose the same `RunnableConfig` hooksurface, so the LlamaIndex port applies the default profile at startup andexposes the same three-axis prompt composition for parity. The frontendprovider wiring (`<CopilotKitProvider properties={{ tone, ... }}>`) stilldemonstrates the client-side API — the forwarded props are visible in therun payload even if the current router does not yet recompose the promptper turn. Extending the router to read forwarded props is tracked as aTODO in the package-level PARITY_NOTES."""from __future__ import annotationsimport osfrom llama_index.llms.openai import OpenAIfrom llama_index.protocols.ag_ui.router import get_ag_ui_workflow_routerDEFAULT_TONE = "professional"DEFAULT_EXPERTISE = "intermediate"DEFAULT_RESPONSE_LENGTH = "concise"TONE_RULES = {    "professional": "Use neutral, precise language. No emoji. Short sentences.",    "casual": "Use friendly, conversational language. Contractions OK. Light humor welcome.",    "enthusiastic": "Use upbeat, energetic language. Exclamation points OK. Emoji OK.",}EXPERTISE_RULES = {    "beginner": "Assume no prior knowledge. Define jargon. Use analogies.",    "intermediate": "Assume common terms are understood; explain specialized terms.",    "expert": "Assume technical fluency. Use precise terminology. Skip basics.",}LENGTH_RULES = {    "concise": "Respond in 1-3 sentences.",    "detailed": "Respond in multiple paragraphs with examples where relevant.",}def build_system_prompt(tone: str, expertise: str, response_length: str) -> str:    return (        "You are a helpful assistant.\n\n"        f"Tone: {TONE_RULES[tone]}\n"        f"Expertise level: {EXPERTISE_RULES[expertise]}\n"        f"Response length: {LENGTH_RULES[response_length]}"    )DEFAULT_SYSTEM_PROMPT = build_system_prompt(    DEFAULT_TONE, DEFAULT_EXPERTISE, DEFAULT_RESPONSE_LENGTH)_openai_kwargs = {}if os.environ.get("OPENAI_BASE_URL"):    _openai_kwargs["api_base"] = os.environ["OPENAI_BASE_URL"]agent_config_router = get_ag_ui_workflow_router(    llm=OpenAI(model="gpt-4o-mini", temperature=0.4, **_openai_kwargs),    frontend_tools=[],    backend_tools=[],    system_prompt=DEFAULT_SYSTEM_PROMPT,    initial_state={},)

You have a working agent and want the user to be able to tune how it behaves: tone, expertise level, response length, language, persona. By the end of this guide, your UI will own a typed config object that the agent reads on every run and rebuilds its system prompt from.

When to use this#

Reach for agent config whenever the agent's behaviour depends on user-controllable settings that don't fit naturally as chat input:

  • Tone, voice, persona: "playful", "formal", "casual"
  • Expertise level: "beginner", "intermediate", "expert"
  • Response shape: short / medium / long, structured / prose, language
  • Domain switches: which knowledge base to consult, which tool subset to enable

If the values are a channel the user occasionally tunes (a settings panel, a toolbar of selects), agent config is the right shape. If the values are content the agent should write back to (notes, a document, a plan), use Shared State instead.

How agent config flows from the UI into the agent's reasoning loop depends on your runtime architecture. Agents living behind a runtime read it from agent state on every run, while in-process agents receive the same object as forwarded properties on the provider — same UX, slightly different wiring on each side.

How it works#

Agent config is a typed object the frontend owns and keeps in sync with the agent. There are two pieces: the UI side, which owns the React state and pushes every change into agent state, and the backend node, which reads those fields out of state and turns them into a system prompt.

The UI side stays simple. Hold the typed config in React state, then mirror every change into the agent through agent.setState({...}):

frontend/src/app/page.tsx — UI owns the typed config
function ConfigStateSync({ config }: { config: AgentConfig }) {
  const { agent } = useAgent({ agentId: "agent-config" });
  useEffect(() => {
    agent.setState({ ...config });
  }, [agent, config]);
  return null;
}

The backend half is also a single node. Read the config out of state at the top of every run and use it to build the system prompt for that turn:

backend/agent.py — agent reads config and rebuilds the system prompt
async def my_agent_node(state: AgentState, config: RunnableConfig):
    cfg = state.get("config", {})
    tone = cfg.get("tone", "casual")
    expertise = cfg.get("expertise", "intermediate")
    response_length = cfg.get("response_length", "medium")
    system_prompt = build_system_prompt(tone, expertise, response_length)
    # ...

The agent reads the latest typed config at the start of every turn, rebuilds the system prompt, runs the turn. This is the same shape as the shared-state write-side pattern; agent config is just a specific use of that pattern with a UI-owned typed object on top.