BESSER Agentic Framework (BAF) Generator¶
The BAF generator produces a BAF Agent based on a given B-UML agent model.
Let’s generate the agent for Greetings Agent defined in Agent model. You should create a BAFGenerator object, provide the agent model, and use the generate method as follows:
from besser.generators.agents.baf_generator import BAFGenerator
generator: BAFGenerator = BAFGenerator(model=agent)
generator.generate()
Optional constructor parameters:
output_dir: Output directory (default:output/in the current directory).config_path: Path to a JSON configuration file for the agent. It is read withjson.load, so a YAML file will fail to parse.config: Configuration dictionary (alternative toconfig_path).config_yaml: Raw YAML text to write out as the agent’sconfig.yamlinstead of the template-rendered default. A sqlitedb.sqlentry that names its file underdatabaseis rewritten tofile, the key BAF reads.openai_api_key: OpenAI API key for LLM-powered agent features.generation_mode: See Generation Modes below.a2a_descriptor: Optional resolved A2A topology and governance, normally supplied by the Docker Compose generator. It extends the same agent’s authored states, actions, transitions, and personalization. Descriptors with no outbound peers and no A2A server use the ordinary render.bind_host: Interface the websocket and Streamlit servers listen on in the defaultconfig.yaml(default"localhost", so a standalone agent is not reachable from the network). The Docker Compose generator passes"0.0.0.0"because a server bound to localhost inside a container cannot be reached through the published ports. A suppliedconfig_yamlis written as-is.test_mode: WhenTrue, the agent is generated to be driven headlessly in an isolated test environment such as the Agent Simulator:every WebSocket/Streamlit platform is created with
use_ui=False(the built-in Streamlit chat UI is not launched); withouttest_modeit is created withuse_ui=True. The only exception isconfig={"agentPlatform": "websocket"}, which always usesuse_ui=False;each workspace declared in the agent model is pre-created inside the output folder. Its
pathis made relative (backslashes become/, drive letters and leading slashes are dropped, e.g./tmp/databecomestmp/data); a blank path falls back to the workspace name. A path with..segments, or with no relative part at all (e.g./), raisesValueErrorbecause it would point outside the output folder.
The generated files land in the <<current_directory>>/output folder:
<AgentName>.py: the agent script, named after the agent model — notagent.py.config.yaml: the agent’s configuration file.readme.txt: how to run the generated agent.tools.py: the agent’s tool function definitions — only when the model declares tools.skills/: one Markdown file per skill — only when the model declares skills.personalized_agent_model.py/personalized_agent_model.json: the personalized agent model, written alongside the others only when a personalization config is supplied and the mode is notCODE_ONLY.
Check out the BAF documentation for more details on how to use the generated agent: BESSER Agentic Framework Documentation.
A2A and authored behavior¶
A2A generation requires exactly one authored initial state. Human-facing agents
keep their configured platform; headless workers expose only the A2A server,
and hybrid agents expose both. Outbound tagged or legacy boundary states run
their authored body before contacting peers. Repeated edges to the same peer
at different states remain distinct, including governed merge stages.
Peer replies feed applicable authored receive transitions and are available as
session.get('a2a_result'). Ordinary tagged sends carry the flow and sender
service as well as the message.
Computed primitive-action results are captured independently of send_reply
and store_in_session. The last result-producing action in a body supplies
its local A2A result, even when it is hidden from the human chat. Each invocation
has a fresh capture, so previous session results are not reused accidentally.
Results are logged at INFO level. An empty computed A2A result raises an error;
a body without a computed result retains authored-reply or legacy input behavior.
Inbound requests use an isolated BAF session. Resolved flow and from
bindings select the authored destination and honor applicable inbound transition
conditions. Automatic and conditional transitions continue until the graph
waits for another event, with a limit of 100 transitions per request. Ambiguous
bindings require a flow or from value. RPC GUI actions are returned as
action notices; human sessions render the authored GUI on their platform.
An RPC session lasts one request, so interactive GUI conversations use the human
platform. Governance leaf requests execute one selected local body and return
its result without following later automatic transitions or calling peers.
For topology-only models without usable state bindings, the generated runtime extends an existing state with the legacy swarm behavior. It never replaces the authored graph with a separate greetings/work/idle graph.
Governed merges¶
A merging AgenticGateway that carries a Governance DSL policy is run by the
agent of the lane that owns it. For a voting policy (VotingPolicy,
MajorityPolicy, AbsoluteMajorityPolicy) the merge collects one candidate
output from each producer (the agents on the branches flowing into the gateway),
then one ballot per policy participant, and counts them in the container with
the baked governance_engine.tally:
Weights –
VotingPolicyweighs each ballot by the participant’sconfidence; the majority policies count one vote per ballot.Share – the leading candidate’s support divided by the weight cast (
VotingPolicy), by every ballot including abstentions (AbsoluteMajorityPolicy), or by the ballots that voted (MajorityPolicy).Decision – a candidate is selected only if it is the unique leader and its share meets the policy
ratio(default0.5): strictly above it forMajorityPolicyandAbsoluteMajorityPolicy(“more than half” at the default), at or above it forVotingPolicy. A 1-1 tie is therefore never a majority.No decision – on a tie or a share below the ratio no candidate is picked: the reply lists every candidate output for a human to decide, and the audit footer states why (
no decision: tie between C1, C2orbest share ... below ratio).
The selected candidate is returned verbatim with an audit footer (policy, ratio, winner, scores, abstentions). Participants without a running service count as abstentions. Policies with a human participant pause the merge and wait for the human’s ballot before tallying.
Generation Modes¶
The BAF generator supports three generation modes via the generation_mode parameter:
from besser.generators.agents.baf_generator import BAFGenerator, GenerationMode
# Default: full pipeline (personalization + templated code)
generator = BAFGenerator(model=agent, generation_mode=GenerationMode.FULL)
# Skip personalization, render templates immediately
generator = BAFGenerator(model=agent, generation_mode=GenerationMode.CODE_ONLY)
# Run personalization JSON/model export only (no code templates)
generator = BAFGenerator(model=agent, generation_mode=GenerationMode.PERSONALIZED_ONLY)
FULL (default): Runs personalization (if configured) followed by templated code generation.
CODE_ONLY: Skips personalization helpers and renders templates immediately. Use this when you do not need personalization assets.
PERSONALIZED_ONLY: Runs only the personalization JSON/model export. Use this to produce personalization artifacts without generating the agent code.
Personalization¶
The BAF generator can adapt the generated agent to an end-user’s profile —
language, style, readability, modality, platform, LLM, and more. The
personalization flow is opt-in: with no config passed the generator behaves
identically to the classic pipeline.
See Agent Personalization for the structured configuration schema, the
two recommendation backends (rule-based and LLM-based), the variant mechanisms
(languages, variations, configuration variants, personalization mapping), and
the OPENAI_API_KEY lookup order.
RAG Support¶
If the agent model includes RAG elements (see Agent model),
the generator produces the vector store setup (Chroma), text splitter configuration,
and session.run_rag() calls. A data folder is created for each RAG element
where you should place your PDF documents before running the agent. The folder
name is the lower-cased RAG element name (e.g. Knowledge_Base becomes
knowledge_base/).
Reasoning States and Multi-LLM¶
The generator emits the multi-LLM and reasoning constructs described in Agent model:
Every LLM registered via
agent.new_llm()is generated as anagent.new_llm(...)call, andagent.set_default_llm(...)is emitted when the chosen default differs from the first-registered (auto) default.ReasoningStatestates are generated through thenew_reasoning_statefactory, carrying theirllmreference,max_steps,enable_task_planning,stream_steps,system_promptandfallback_message.Agent-level tools, skills and workspaces are emitted as
agent.new_tool(),agent.new_skill()andagent.new_workspace()calls.
Consumers (LLMReply, DBReply, RAG, reasoning states) reference their
LLM by llm_name; when omitted, the agent’s default LLM is used.
Session Variables and Reply Options¶
Several actions can read from and write to the user session:
Session-variable interpolation. When the matching flag is
True, every{key}placeholder in the text is replaced at runtime withsession.get('key')(an empty string when unset);{user_message}is replaced with the last user message. The flags areuse_session_varsonAgentReply,WebSocketReplyMarkdown,WebSocketReplyHTMLandWebSocketReplySpeech;system_prompt_use_session_varsonLLMReplyandLLMChatReply;prompt_use_session_varsonRAGReply;system_message_prefix_use_session_varsonWebCrawlLLMReply; andcustom_input_prompt_use_session_varsfor custom input prompts. Without the flag, the text is emitted as-is, braces included.Custom input prompt.
LLMReply,RAGReplyandDBReply(LLM query mode) use the last user message as input by default. Withinput_prompt_mode='custom'they usecustom_input_promptinstead.store_in_session.
LLMReply,LLMChatReply,RAGReply,DBReplyandWebCrawlLLMReplystore their result withsession.set(<key>, result)whenstore_in_sessionnames a key, so later actions and states can use it (for example through{key}interpolation).send_reply. When
False, the result is computed (and stored ifstore_in_sessionis set) but not sent to the user. Defaults toTrue.
from besser.BUML.metamodel.state_machine.agent import AgentReply, LLMReply
from besser.BUML.metamodel.state_machine.state_machine import Body
summarize_state = agent.new_state("summarize")
summarize_state.set_body(Body("summarize_body", actions=[
# Silently summarize the question and keep the result in the session
LLMReply(prompt="Summarize the question in one sentence.",
input_prompt_mode="custom",
custom_input_prompt="Question about {topic}: {user_message}",
custom_input_prompt_use_session_vars=True,
store_in_session="summary", send_reply=False),
# ...then reuse it in a templated reply
AgentReply("You asked: {summary}", use_session_vars=True),
]))
GUI Generation¶
A GUIReplyAction sends a GUI as a chat message. The GUI is a
GUIModel registered on the agent with
agent.add_gui_model(gui_id, gui_model) (stored in agent.gui_models);
the action references it through its gui_id (see
Agent model).
For each distinct gui_id referenced by a GUIReplyAction, the generator
writes a module guis/<gui_id as identifier>.py in the output folder. The
module holds the BUML code of the GUI model (gui_model) and wraps it in a
BAF AgentGUI object named gui. The agent imports it and the state body
calls platform.reply_gui(session, <module>). Generation raises
ValueError when a GUIReplyAction references a gui_id that is not
registered in agent.gui_models, or when two ids map to the same module name
(for example order-form and order form).
Transitions can react to the GUI: state.when_form_submitted(form_id=...)
fires when the user submits the form with that id (any form when form_id is
omitted), and state.when_event(GUIEvent(message_id=...)) fires on events
sent by that GUI.
from besser.BUML.metamodel.gui import GUIModel, Module, Screen, Text
from besser.BUML.metamodel.state_machine.agent import (
Agent, AgentReply, GUIReplyAction, WebSocketPlatform
)
from besser.BUML.metamodel.state_machine.state_machine import Body
from besser.generators.agents.baf_generator import BAFGenerator
agent = Agent("order_agent")
agent.platforms.append(WebSocketPlatform())
# The GUI shown in the chat, registered under the id the GUIReplyAction references
title = Text(name="title", content="Place your order")
screen = Screen(name="order_screen", description="", view_elements={title}, is_main_page=True)
order_gui = GUIModel(name="order_gui", package="", versionCode="1", versionName="1.0",
description="", modules={Module(name="order_module", screens={screen})})
agent.add_gui_model("order_form", order_gui)
ask = agent.new_state("ask", initial=True)
thanks = agent.new_state("thanks")
ask.set_body(Body("ask_body", actions=[GUIReplyAction(gui_id="order_form", is_form=True)]))
thanks.set_body(Body("thanks_body", actions=[AgentReply("Thanks, your order is in!")]))
ask.when_form_submitted(form_id="order_form").go_to(thanks)
generator: BAFGenerator = BAFGenerator(model=agent)
generator.generate()
# output/
# order_agent.py <- imports guis.order_form, calls platform.reply_gui(session, order_form)
# guis/
# __init__.py
# order_form.py <- gui_model (BUML code) + gui = AgentGUI(...)
Missing BAF Features¶
Currently, some features available in BAF are stil missing in the B-UML agent model and the BAF Generator. Most notably:
Platform Configuration
Entities
Processors