Skip to content

Commit cb20d4d

Browse files
BinglanLiclaude
andcommitted
feat: add AgentSpec for per-agent identity and system prompt parameterization
Implements Feature 2 of the Multi-Agent Future Roadmap (all 3 phases). - New `BaseAgent/agent_spec.py`: `AgentSpec` dataclass with `name`, `role`, `system_prompt_override`, `tool_names`, `skill_names`, `llm`, `source`, and `temperature` fields. - `BaseAgent/__init__.py`: exports `AgentSpec`. - `prompts.py`: replaces hardcoded biomedical role with `{role_description}` slot; adds `_DEFAULT_ROLE_DESCRIPTION` constant for the fallback value. - `base_agent.py`: `BaseAgent.__init__` accepts `spec: AgentSpec | None`; `configure()` applies `spec.tool_names` / `spec.skill_names` filters; `_generate_system_prompt()` injects `spec.role` or returns `spec.system_prompt_override` directly. Explicit kwargs always beat spec. - `tests/test_agent_spec.py`: 22 unit tests (no API key required). - `examples/08_agent_identity.py`: demonstrates role injection, tool subset, model override, prompt override, and backwards compatibility. `spec=None` is the default; all existing behaviour is unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4edc5d3 commit cb20d4d

7 files changed

Lines changed: 473 additions & 11 deletions

File tree

BaseAgent/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from .base_agent import BaseAgent
66
from .events import AgentEvent, EventType
77
from .resources import Skill
8+
from .agent_spec import AgentSpec
89

910
__version__ = "0.1.0"
1011
__author__ = "BaseAgent Contributors"
11-
__all__ = ["BaseAgent", "AgentEvent", "EventType", "Skill"]
12+
__all__ = ["BaseAgent", "AgentEvent", "EventType", "Skill", "AgentSpec"]

BaseAgent/agent_spec.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""
2+
AgentSpec: Identity and persona configuration for a BaseAgent instance.
3+
4+
Used by the multi-agent system to give each agent a distinct name, role,
5+
tool subset, skill subset, and optional model override.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from dataclasses import dataclass, field
11+
from typing import TYPE_CHECKING
12+
13+
if TYPE_CHECKING:
14+
from BaseAgent.llm import SourceType
15+
16+
17+
@dataclass
18+
class AgentSpec:
19+
"""Identity and configuration for a single agent.
20+
21+
When passed to ``BaseAgent(spec=...)``, the spec:
22+
23+
* Sets ``agent.name`` (used for attribution in multi-agent logs).
24+
* Injects ``role`` into the system prompt header.
25+
* Optionally restricts which tools and skills are visible to this agent.
26+
* Optionally overrides the LLM model, provider, and temperature.
27+
28+
All fields except ``name`` and ``role`` are optional. When a field is
29+
``None`` the corresponding ``BaseAgentConfig`` default is used unchanged,
30+
so omitting a field never changes existing behaviour.
31+
32+
Args:
33+
name: Unique agent identifier, e.g. ``"ontology_analyst"``.
34+
role: One-line description injected into the system prompt, e.g.
35+
``"a biomedical ontology analyst that …"``.
36+
system_prompt_override: If provided, replaces the entire generated
37+
system prompt. ``name`` and ``role`` are still stored but
38+
``role`` is not injected.
39+
tool_names: If provided, only the named tools are enabled for this
40+
agent. ``None`` means all loaded tools are enabled.
41+
skill_names: If provided, only the named skills are enabled.
42+
Skills with ``trigger="manual"`` are never removed regardless of
43+
this list. ``None`` means all loaded skills are enabled.
44+
llm: Model name override, e.g. ``"claude-sonnet-4-20250514"``.
45+
source: Provider override, e.g. ``"Anthropic"``.
46+
temperature: Sampling temperature override.
47+
"""
48+
49+
name: str
50+
role: str
51+
system_prompt_override: str | None = None
52+
tool_names: list[str] | None = None
53+
skill_names: list[str] | None = None
54+
llm: str | None = None
55+
source: "SourceType | None" = None
56+
temperature: float | None = None

BaseAgent/base_agent.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333
_CUSTOM_SOFTWARE_SECTION,
3434
_SKILLS_SECTION,
3535
_SKILL_ENTRY_TEMPLATE,
36+
_DEFAULT_ROLE_DESCRIPTION,
3637
)
3738
from BaseAgent.resources import Skill
39+
from BaseAgent.agent_spec import AgentSpec
3840
from BaseAgent.config import default_config
3941
from BaseAgent.resource_manager import ResourceManager
4042
from BaseAgent.retriever import ToolRetriever
@@ -61,6 +63,7 @@ def __init__(
6163
checkpoint_db_path: str | None = None,
6264
require_approval: str | None = None,
6365
skills_directory: str | None = None,
66+
spec: AgentSpec | None = None,
6467
):
6568
"""
6669
Args:
@@ -77,10 +80,27 @@ def __init__(
7780
"always" — interrupt before every code block;
7881
"dangerous_only" — interrupt only for bash/R code.
7982
skills_directory: Path to a directory of SKILL.md files to load on startup.
83+
spec: Optional agent identity and persona. When provided, ``spec.name``,
84+
``spec.role``, ``spec.tool_names``, ``spec.skill_names``,
85+
``spec.llm``, ``spec.source``, and ``spec.temperature`` override
86+
the corresponding defaults. Explicit keyword arguments take
87+
priority over spec fields.
8088
"""
89+
self.spec = spec
90+
91+
# Agent name: from spec, or default
92+
self.name: str = spec.name if spec is not None else "agent"
93+
94+
# Resolve settings: explicit kwargs > spec > default_config
8195
self.path = path if path is not None else default_config.path
82-
self.llm_model_name = llm if llm is not None else default_config.llm
83-
self.source = source if source is not None else default_config.source
96+
self.llm_model_name = (
97+
llm if llm is not None
98+
else (spec.llm if spec is not None and spec.llm is not None else default_config.llm)
99+
)
100+
self.source = (
101+
source if source is not None
102+
else (spec.source if spec is not None and spec.source is not None else default_config.source)
103+
)
84104
self.timeout_seconds = timeout_seconds if timeout_seconds is not None else default_config.timeout_seconds
85105
self.base_url = base_url if base_url is not None else default_config.base_url
86106
self.api_key = api_key if api_key is not None else default_config.api_key
@@ -96,6 +116,13 @@ def __init__(
96116
#TODO: add self-critic mode
97117
self.self_critic = False
98118

119+
# Temperature: explicit kwarg not exposed yet, so spec takes precedence over default
120+
_temperature = (
121+
spec.temperature
122+
if spec is not None and spec.temperature is not None
123+
else default_config.temperature
124+
)
125+
99126
# Initialize the LLM agent
100127
self.source, self.llm = get_llm(
101128
self.llm_model_name,
@@ -104,6 +131,7 @@ def __init__(
104131
base_url=self.base_url,
105132
api_key=self.api_key,
106133
config=default_config,
134+
temperature=_temperature,
107135
)
108136

109137
# Initialize the resource manager (replaces ToolRegistry)
@@ -694,9 +722,20 @@ def _generate_system_prompt(
694722
When tool retriever is not used, all resources have selected=True by default.
695723
When tool retriever is used, only retrieved resources are marked selected=True.
696724
"""
725+
# If the spec provides a full override, return it directly
726+
if self.spec is not None and self.spec.system_prompt_override is not None:
727+
return self.spec.system_prompt_override
728+
697729
# Base prompt
698730
prompt_modifier = get_base_prompt_template(self_critic=self_critic)
699-
prompt_format_dict = {}
731+
732+
# Role description: from spec, or fall back to the default
733+
role_description = (
734+
self.spec.role
735+
if self.spec is not None
736+
else _DEFAULT_ROLE_DESCRIPTION
737+
)
738+
prompt_format_dict = {"role_description": role_description}
700739

701740
# Add custom resources section from resource manager
702741
custom_tools = self.resource_manager.collection.custom_tools
@@ -825,6 +864,13 @@ def configure(self, self_critic=False, test_time_scale_round=0):
825864
if self.skills_directory:
826865
self.load_skills(self.skills_directory)
827866

867+
# Apply AgentSpec resource filters (must happen after resources are loaded)
868+
if self.spec is not None:
869+
if self.spec.tool_names is not None:
870+
self.resource_manager.select_tools_by_names(self.spec.tool_names)
871+
if self.spec.skill_names is not None:
872+
self.resource_manager.select_skills_by_names(self.spec.skill_names)
873+
828874
# Generate the system prompt (will be built automatically from ResourceManager)
829875
self.system_prompt = self._generate_system_prompt(
830876
self_critic=self_critic,

BaseAgent/prompts.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44

55
from textwrap import dedent
66

7+
_DEFAULT_ROLE_DESCRIPTION = (
8+
"a helpful biomedical assistant assigned with the task of problem-solving"
9+
)
10+
711
_SYSTEM_PROMPT_HEADER = \
812
"""
9-
You are a helpful biomedical assistant assigned with the task of problem-solving.
13+
You are {role_description}.
1014
To achieve this, you will be using an interactive coding environment equipped with a variety of tool functions, data, and softwares to assist you throughout the process.
1115
1216
Given a task, make a plan first. The plan should be a numbered list of steps that you will take to solve the task. Be specific and detailed.

0 commit comments

Comments
 (0)