Agent Config
Forward typed configuration from your UI into the agent's reasoning loop.
"""Agno agent backing the Agent Config Object demo.Reads three forwarded properties — ``tone``, ``expertise``,``responseLength`` — that the CopilotKit provider forwards on every run,and composes the system prompt dynamically per turn.Agno does not have a LangGraph-style ``configurable`` channel; instead thecustom AGUI handler in ``agent_server.py`` (mounted at``/agent-config/agui``) reads ``RunAgentInput.forwarded_props``, builds afresh system prompt, and constructs a per-request Agno ``Agent`` with thatprompt before invoking it. The factory in this module produces thoseper-request agents."""from typing import Literalfrom agno.agent.agent import Agentfrom agno.models.openai import OpenAIChatfrom dotenv import load_dotenvload_dotenv()Tone = Literal["professional", "casual", "enthusiastic"]Expertise = Literal["beginner", "intermediate", "expert"]ResponseLength = Literal["concise", "detailed"]DEFAULT_TONE: Tone = "professional"DEFAULT_EXPERTISE: Expertise = "intermediate"DEFAULT_RESPONSE_LENGTH: ResponseLength = "concise"VALID_TONES: set[str] = {"professional", "casual", "enthusiastic"}VALID_EXPERTISE: set[str] = {"beginner", "intermediate", "expert"}VALID_RESPONSE_LENGTHS: set[str] = {"concise", "detailed"}def read_properties(forwarded_props: dict | None) -> dict[str, str]: """Read the forwarded ``properties`` dict with defensive defaults. The CopilotKit provider forwards its ``properties`` prop as top-level keys on ``forwarded_props`` (see the runtime's run handler). This function never raises — every unrecognized value falls back to the matching ``DEFAULT_*`` constant. """ props = forwarded_props or {} tone = props.get("tone", DEFAULT_TONE) expertise = props.get("expertise", DEFAULT_EXPERTISE) response_length = props.get("responseLength", DEFAULT_RESPONSE_LENGTH) if tone not in VALID_TONES: tone = DEFAULT_TONE if expertise not in VALID_EXPERTISE: expertise = DEFAULT_EXPERTISE if response_length not in VALID_RESPONSE_LENGTHS: response_length = DEFAULT_RESPONSE_LENGTH return { "tone": tone, "expertise": expertise, "response_length": response_length, }def build_system_prompt(tone: str, expertise: str, response_length: str) -> str: """Compose a system prompt from the three axes.""" 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."), } 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]}" )def build_agent(forwarded_props: dict | None) -> Agent: """Build a per-request Agno agent whose system prompt reflects the forwarded provider properties. Constructed fresh on each run so the system prompt is current; the Agno session DB still tracks history via ``session_id`` (the AGUI handler passes ``thread_id`` through). """ props = read_properties(forwarded_props) system_prompt = build_system_prompt( props["tone"], props["expertise"], props["response_length"] ) return Agent( model=OpenAIChat(id="gpt-4o-mini", temperature=0.4, timeout=120), tools=[], description=system_prompt, )# A neutral default so AgentOS' agent-registry init doesn't fail before the# first run materialises a per-request agent.agent = build_agent(None)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({...}):
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:
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.
