commonadk 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- commonadk/__init__.py +46 -0
- commonadk/adapters/__init__.py +65 -0
- commonadk/adapters/autogen_adapter.py +398 -0
- commonadk/adapters/base.py +75 -0
- commonadk/adapters/claude_agent.py +277 -0
- commonadk/adapters/crewai_adapter.py +284 -0
- commonadk/adapters/google_adk.py +150 -0
- commonadk/adapters/langgraph_adapter.py +426 -0
- commonadk/adapters/openai_agents.py +141 -0
- commonadk/cli.py +525 -0
- commonadk/loader.py +214 -0
- commonadk/mermaid.py +69 -0
- commonadk/mixed.py +557 -0
- commonadk/models.py +277 -0
- commonadk/validation.py +207 -0
- commonadk-0.0.1.dist-info/METADATA +363 -0
- commonadk-0.0.1.dist-info/RECORD +20 -0
- commonadk-0.0.1.dist-info/WHEEL +4 -0
- commonadk-0.0.1.dist-info/entry_points.txt +2 -0
- commonadk-0.0.1.dist-info/licenses/LICENSE +201 -0
commonadk/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""commonadk -- define an agent system once, run it on any agent SDK.
|
|
2
|
+
|
|
3
|
+
See plan.md at the repo root for the full hypothesis and architecture. This
|
|
4
|
+
package (M1) is the framework-neutral core: models, loader, validation, and
|
|
5
|
+
the mermaid renderer. It has no dependency on any agent SDK.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .adapters import BaseAdapter, get_adapter
|
|
9
|
+
from .loader import load
|
|
10
|
+
from .mermaid import render_mermaid, write_interaction_layer
|
|
11
|
+
from .mixed import MixedSystem, RuntimeUnit, build_mixed
|
|
12
|
+
from .models import (
|
|
13
|
+
AgentConfig,
|
|
14
|
+
AgentSpec,
|
|
15
|
+
EnvRequirement,
|
|
16
|
+
InteractionEdge,
|
|
17
|
+
InteractionGraph,
|
|
18
|
+
Project,
|
|
19
|
+
ProjectConfig,
|
|
20
|
+
Requires,
|
|
21
|
+
ToolParameter,
|
|
22
|
+
ToolSpec,
|
|
23
|
+
)
|
|
24
|
+
from .validation import ValidationError
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BaseAdapter",
|
|
28
|
+
"get_adapter",
|
|
29
|
+
"load",
|
|
30
|
+
"render_mermaid",
|
|
31
|
+
"write_interaction_layer",
|
|
32
|
+
"MixedSystem",
|
|
33
|
+
"RuntimeUnit",
|
|
34
|
+
"build_mixed",
|
|
35
|
+
"AgentConfig",
|
|
36
|
+
"AgentSpec",
|
|
37
|
+
"EnvRequirement",
|
|
38
|
+
"InteractionEdge",
|
|
39
|
+
"InteractionGraph",
|
|
40
|
+
"Project",
|
|
41
|
+
"ProjectConfig",
|
|
42
|
+
"Requires",
|
|
43
|
+
"ToolParameter",
|
|
44
|
+
"ToolSpec",
|
|
45
|
+
"ValidationError",
|
|
46
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Adapter registry -- turns a framework-neutral `Project` into a live SDK agent.
|
|
2
|
+
|
|
3
|
+
Each target's actual SDK is imported lazily, only when that target is
|
|
4
|
+
requested via `get_adapter`. Importing `commonadk` (or calling
|
|
5
|
+
`commonadk.load()`) must keep working with no agent SDK installed at all --
|
|
6
|
+
this module and `base.py` have zero SDK imports at module scope to guarantee
|
|
7
|
+
that.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from importlib import import_module
|
|
13
|
+
|
|
14
|
+
from .base import BaseAdapter
|
|
15
|
+
|
|
16
|
+
# target -> (module to import, class name to instantiate, pip extra to suggest)
|
|
17
|
+
_REGISTRY: dict[str, tuple[str, str, str]] = {
|
|
18
|
+
"google-adk": ("commonadk.adapters.google_adk", "GoogleADKAdapter", "google"),
|
|
19
|
+
"openai": ("commonadk.adapters.openai_agents", "OpenAIAgentsAdapter", "openai"),
|
|
20
|
+
"claude": ("commonadk.adapters.claude_agent", "ClaudeAgentSDKAdapter", "claude"),
|
|
21
|
+
"crewai": ("commonadk.adapters.crewai_adapter", "CrewAIAdapter", "crewai"),
|
|
22
|
+
"autogen": ("commonadk.adapters.autogen_adapter", "AutoGenAdapter", "autogen"),
|
|
23
|
+
"langgraph": ("commonadk.adapters.langgraph_adapter", "LangGraphAdapter", "langgraph"),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def known_targets() -> list[str]:
|
|
28
|
+
"""Sorted list of registered target names -- no SDK import required.
|
|
29
|
+
|
|
30
|
+
Used by `validation.py`'s `runtime:` check to validate a name against
|
|
31
|
+
the registry without importing any adapter module (and therefore
|
|
32
|
+
without importing any agent SDK) -- see mixed-target-design.md, "What
|
|
33
|
+
`runtime:` means now".
|
|
34
|
+
"""
|
|
35
|
+
return sorted(_REGISTRY)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_adapter(target: str) -> BaseAdapter:
|
|
39
|
+
"""Look up and instantiate the adapter for `target`.
|
|
40
|
+
|
|
41
|
+
Raises `ValueError` naming the known targets if `target` is unrecognized,
|
|
42
|
+
or `ImportError` with a `pip install "commonadk[<extra>]"` hint if the
|
|
43
|
+
target is recognized but its underlying SDK is not installed.
|
|
44
|
+
"""
|
|
45
|
+
if target not in _REGISTRY:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Unknown build target {target!r}. Known targets: "
|
|
48
|
+
f"{sorted(_REGISTRY)}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
module_path, class_name, extra = _REGISTRY[target]
|
|
52
|
+
try:
|
|
53
|
+
module = import_module(module_path)
|
|
54
|
+
except ImportError as e:
|
|
55
|
+
raise ImportError(
|
|
56
|
+
f"target {target!r} requires its SDK to be installed. "
|
|
57
|
+
f'Install it with: pip install "commonadk[{extra}]" '
|
|
58
|
+
f"(underlying import error: {e})"
|
|
59
|
+
) from e
|
|
60
|
+
|
|
61
|
+
adapter_cls = getattr(module, class_name)
|
|
62
|
+
return adapter_cls()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
__all__ = ["BaseAdapter", "get_adapter", "known_targets"]
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""AutoGen adapter: `AgentSpec` -> live `autogen_agentchat` objects.
|
|
2
|
+
|
|
3
|
+
This targets Microsoft's current AutoGen stack (`autogen-agentchat` /
|
|
4
|
+
`autogen-core` / `autogen-ext`, 0.4+), NOT the community `ag2` fork -- the
|
|
5
|
+
two forked from the same original project and now have unrelated APIs.
|
|
6
|
+
Verified against the installed packages: autogen-agentchat 0.7.5,
|
|
7
|
+
autogen-core 0.7.5, autogen-ext 0.7.5 (`autogen_agentchat.agents`,
|
|
8
|
+
`autogen_agentchat.teams`, `autogen_agentchat.base`, and
|
|
9
|
+
`autogen_ext.models.{openai,anthropic}`, introspected via `inspect.signature`
|
|
10
|
+
/ `inspect.getsource` and exercised directly during M7 -- not taken from
|
|
11
|
+
memory of the API, which is unreliable here given the fork).
|
|
12
|
+
|
|
13
|
+
WHAT `build()` RETURNS -- read this first: like the Google ADK and OpenAI
|
|
14
|
+
Agents adapters, this SDK has real persistent agent objects
|
|
15
|
+
(`autogen_agentchat.agents.AssistantAgent`), each wired with a
|
|
16
|
+
`model_client`, its own `tools.py` functions, and a `handoffs` list of
|
|
17
|
+
target agent *names* (see "Edge mapping" below). But an `AssistantAgent`
|
|
18
|
+
with handoffs configured needs a *team* to actually route those handoffs at
|
|
19
|
+
run time -- AutoGen's own mechanism for this is `autogen_agentchat.teams.
|
|
20
|
+
Swarm`, "a group chat team that selects the next speaker based on handoff
|
|
21
|
+
message[s]" (its own docstring). Investigated, not assumed, whether a lone
|
|
22
|
+
`AssistantAgent` with handoffs is runnable on its own: it is NOT -- handoffs
|
|
23
|
+
only take effect inside a `Swarm` (or another team); a bare `AssistantAgent.
|
|
24
|
+
run()` just answers once and never consults `.handoffs` at all.
|
|
25
|
+
|
|
26
|
+
So this adapter builds every reachable agent once (see "Edge mapping"), then
|
|
27
|
+
picks the return shape based on whether the build root actually has
|
|
28
|
+
somewhere to hand off to:
|
|
29
|
+
|
|
30
|
+
- The build root has NO outgoing edges (a leaf, e.g. `writer` in the shipped
|
|
31
|
+
example): there is nothing to route to, so this adapter returns the bare
|
|
32
|
+
`AssistantAgent` -- the simplest, most directly runnable object for that
|
|
33
|
+
case. Usage: `result = await agent.run(task="...")`.
|
|
34
|
+
- The build root HAS at least one outgoing edge: this adapter returns a
|
|
35
|
+
ready-to-run `autogen_agentchat.teams.Swarm` whose `participants` are
|
|
36
|
+
every reachable agent, build root first (`BaseAdapter._reachable_agents`
|
|
37
|
+
already returns `agent_name` at index 0, which is also exactly the
|
|
38
|
+
property `Swarm` requires: verified via `Swarm.__init__` -- the first
|
|
39
|
+
participant becomes the initial speaker). Usage:
|
|
40
|
+
`result = await team.run(task="...")`.
|
|
41
|
+
|
|
42
|
+
Usage (mirroring cli.py's `_run_autogen`):
|
|
43
|
+
|
|
44
|
+
import asyncio
|
|
45
|
+
from commonadk import load
|
|
46
|
+
|
|
47
|
+
project = load("common/")
|
|
48
|
+
built = project.build("coordinator", target="autogen") # a Swarm here
|
|
49
|
+
|
|
50
|
+
async def main():
|
|
51
|
+
result = await built.run(task="Research EV adoption")
|
|
52
|
+
print(result.messages[-1].content)
|
|
53
|
+
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
|
|
56
|
+
`max_turns` on the returned `Swarm` -- a deliberate, documented default, not
|
|
57
|
+
an SDK requirement: investigated directly against `Swarm`'s own docs and
|
|
58
|
+
`BaseGroupChat.__init__` -- with neither a `termination_condition` nor a
|
|
59
|
+
`max_turns` set, a group chat "will run indefinitely": if the current
|
|
60
|
+
speaker doesn't send a handoff message, `SwarmGroupChatManager` just lets
|
|
61
|
+
the same speaker go again, and there is no other built-in stop condition
|
|
62
|
+
(unlike a bare `AssistantAgent.run()`, which always returns after exactly
|
|
63
|
+
one turn). Since `commonadk run` needs a single execution that reliably
|
|
64
|
+
terminates (matching every other adapter's `_run_*` in cli.py), and
|
|
65
|
+
`max_turns`/`termination_condition` are `Swarm` CONSTRUCTOR-only fields with
|
|
66
|
+
no equivalent per-call override on `run`/`run_stream` (verified via
|
|
67
|
+
`inspect.signature`), this adapter sets `max_turns=len(reachable)` on every
|
|
68
|
+
`Swarm` it returns: exactly enough speaker-turns for one full pass down a
|
|
69
|
+
linear delegate/handoff chain (root speaks and hands off, ..., the final
|
|
70
|
+
agent speaks its answer and the budget is exhausted right after). This is a
|
|
71
|
+
heuristic, not a guarantee, for branchier graphs (multi-parent, cycles) --
|
|
72
|
+
documented here as a known v1 limitation, not silently assumed correct. A
|
|
73
|
+
caller who wants a different turn budget or an interactive, longer-running
|
|
74
|
+
conversation should not rely on the `Swarm` this adapter returns for that;
|
|
75
|
+
building one directly from `AssistantAgent`s (this adapter's own approach,
|
|
76
|
+
above) with an explicit `termination_condition` is the escape hatch.
|
|
77
|
+
|
|
78
|
+
Edge mapping (v1 intersection decision, plan.md "Edge semantics v1", same
|
|
79
|
+
call as openai_agents.py): both `delegate` and `handoff` edges map to
|
|
80
|
+
AutoGen's one handoff mechanism -- `AssistantAgent(handoffs=[...])`. commonadk
|
|
81
|
+
does not yet distinguish them for this target either.
|
|
82
|
+
|
|
83
|
+
KEY PROPERTY, investigated not assumed -- handoff targets are plain NAME
|
|
84
|
+
STRINGS, not object references: `AssistantAgent.__init__` accepts
|
|
85
|
+
`handoffs: List[HandoffBase | str] | None`, and a bare `str` is wrapped as
|
|
86
|
+
`HandoffBase(target=that_string)` (verified via `inspect.getsource`) --
|
|
87
|
+
`Swarm` resolves those names against its own `participants` list by name at
|
|
88
|
+
run time, there is no parent-tracking or "already referenced" guard
|
|
89
|
+
anywhere in construction. This makes multi-parent graphs and cycles even
|
|
90
|
+
more trivially fine here than in openai_agents.py (which at least memoizes
|
|
91
|
+
live object references): this adapter builds one `AssistantAgent` per
|
|
92
|
+
logical agent name (memoized in a `dict[str, AssistantAgent]`, matching
|
|
93
|
+
`_reachable_agents`'s own dedup) and each agent's `handoffs` list is just
|
|
94
|
+
`[edge.to for edge in ... if edge.from_ == name]` -- plain strings, so a name
|
|
95
|
+
reachable by two paths or a path that cycles back to the build root needs no
|
|
96
|
+
special handling at all: it is simply the same dict entry, and a cycle back
|
|
97
|
+
to `agent_name` is just another string in some other agent's `handoffs`
|
|
98
|
+
list, not a construction hazard (`agent_name` itself is never excluded from
|
|
99
|
+
`memo`, unlike the Claude/CrewAI adapters' flat registries, since here
|
|
100
|
+
"being referenced by name" carries no risk of infinite recursion or
|
|
101
|
+
double-registration).
|
|
102
|
+
|
|
103
|
+
Tool wiring: `AssistantAgent(tools=[...])` accepts PLAIN CALLABLES directly
|
|
104
|
+
(verified via `inspect.getsource` of `AssistantAgent.__init__`) -- it wraps
|
|
105
|
+
each with `autogen_core.tools.FunctionTool(tool, description=tool.__doc__)`
|
|
106
|
+
itself, introspecting the function's signature and docstring exactly like
|
|
107
|
+
every `tools.py` function already provides (enforced upstream by
|
|
108
|
+
validation.py). So this adapter passes `[t.func for t in spec.tools]`
|
|
109
|
+
straight through with no wrapping of its own -- simpler than every other
|
|
110
|
+
adapter in this codebase.
|
|
111
|
+
|
|
112
|
+
Model routing -- investigated against the installed `autogen_ext.models`
|
|
113
|
+
package tree (`anthropic`, `azure`, `ollama`, `openai`, ... submodules;
|
|
114
|
+
`importlib.metadata.metadata("autogen-ext").get_all("Requires-Dist")` for
|
|
115
|
+
the full extras list). Three providers get a real, verified path; anything
|
|
116
|
+
else is a clear unsupported-provider error:
|
|
117
|
+
|
|
118
|
+
- `openai/<model>` -> `autogen_ext.models.openai.OpenAIChatCompletionClient
|
|
119
|
+
(model=<bare id>)` -- the native OpenAI client, per plan.md's explicit
|
|
120
|
+
instruction for this provider.
|
|
121
|
+
- `anthropic/<model>` -> `autogen_ext.models.anthropic.
|
|
122
|
+
AnthropicChatCompletionClient(model=<bare id>, model_info=...)` -- a real,
|
|
123
|
+
separately-shipped native client module (needs the `anthropic` package,
|
|
124
|
+
already a transitive dependency of this project's `claude`/`crewai`
|
|
125
|
+
extras and pinned directly in this adapter's own `autogen` extra).
|
|
126
|
+
CRITICAL LANDMINE, verified not assumed: this client's bundled model-name
|
|
127
|
+
table (`autogen_ext.models.anthropic._model_info._MODEL_INFO`) only knows
|
|
128
|
+
a handful of hardcoded, DATED model ids (e.g. `claude-opus-4-20250514`)
|
|
129
|
+
and falls back to fuzzy prefix-matching for anything else -- and that
|
|
130
|
+
fallback is buggy for exactly the kind of aliased model id this project's
|
|
131
|
+
own examples use: `"claude-sonnet-5".startswith("claude-2.0".split("-2")
|
|
132
|
+
[0])` == `"claude-sonnet-5".startswith("claude")` == True, so an unrelated
|
|
133
|
+
legacy entry (`claude-2.0`, `function_calling: False`) silently wins the
|
|
134
|
+
match -- verified directly: constructing the client on `"claude-sonnet-5"`
|
|
135
|
+
with no explicit `model_info` returns `function_calling: False`, and then
|
|
136
|
+
`AssistantAgent.__init__` raises "The model does not support function
|
|
137
|
+
calling" as soon as this adapter passes any tools/handoffs. This adapter
|
|
138
|
+
works around it by ALWAYS passing an explicit `model_info` for this
|
|
139
|
+
provider (`_ANTHROPIC_MODEL_INFO` below, `function_calling: True`),
|
|
140
|
+
bypassing the stale table entirely rather than trusting it for a model id
|
|
141
|
+
it clearly does not know about.
|
|
142
|
+
- `gemini/<model>` -> also `OpenAIChatCompletionClient(model=<bare id>,
|
|
143
|
+
model_info=...)`. There is no separate native Gemini client class shipped
|
|
144
|
+
anywhere in `autogen_ext.models` (the `autogen-ext[gemini]` extra only
|
|
145
|
+
pulls in `google-genai`, used by the unrelated `semantic-kernel` optional
|
|
146
|
+
integration, not by any model-client class) -- but `OpenAIChatCompletionClient.
|
|
147
|
+
__init__` itself special-cases Gemini: verified via `inspect.getsource`,
|
|
148
|
+
when the model name starts with `"gemini-"` it automatically points
|
|
149
|
+
`base_url` at Gemini's OpenAI-compatible endpoint
|
|
150
|
+
(`GEMINI_OPENAI_BASE_URL`) and reads `GEMINI_API_KEY` from the
|
|
151
|
+
environment if no `api_key` is given -- this genuinely IS "the shipped
|
|
152
|
+
Gemini path", it just lives inside the OpenAI client rather than a
|
|
153
|
+
dedicated module. Its bundled model-info table is INCOMPLETE, not buggy
|
|
154
|
+
(verified: `gemini-2.5-flash` is listed, but `gemini-2.5-pro` -- used
|
|
155
|
+
directly by the shipped example's `researcher` agent -- is not, and
|
|
156
|
+
raises `"model_info is required when model name is not a valid OpenAI
|
|
157
|
+
model"`), so this adapter always passes an explicit `model_info` here
|
|
158
|
+
too (`_GEMINI_MODEL_INFO` below), for the same reason as the Anthropic
|
|
159
|
+
path: don't trust an incomplete/stale table for a model id it might not
|
|
160
|
+
recognize. The base-url/api-key special-casing runs unconditionally
|
|
161
|
+
before that table is even consulted, so passing `model_info` explicitly
|
|
162
|
+
does not disable it -- verified by constructing the client both ways.
|
|
163
|
+
- Anything else (azure, bedrock, ollama, cohere, mistral, ...) raises a
|
|
164
|
+
clear `ValueError` naming the agent, its resolved model string, and the
|
|
165
|
+
fix options (an `openai/`, `anthropic/`, or `gemini/`-prefixed model; a
|
|
166
|
+
different alias; or a `targets.autogen.model` override).
|
|
167
|
+
|
|
168
|
+
Unlike the Claude Agent SDK adapter (M5), this target needs NO per-target
|
|
169
|
+
model overrides added to the shipped research-crew example to make it
|
|
170
|
+
buildable: the example's `fast` alias resolves to `gemini/gemini-2.5-flash`
|
|
171
|
+
and `smart` to `anthropic/claude-sonnet-5`, and researcher's own
|
|
172
|
+
`gemini/gemini-2.5-pro` is used directly -- all three are covered by the
|
|
173
|
+
native Gemini/Anthropic paths above with no escape hatch needed.
|
|
174
|
+
|
|
175
|
+
Per-target override (`targets.autogen.model` in `agent-config.yaml`):
|
|
176
|
+
always wins, and is passed through as the bare model id to
|
|
177
|
+
`OpenAIChatCompletionClient` -- the "default client" of this adapter (the
|
|
178
|
+
same one the `openai/...` provider branch above uses), with NO explicit
|
|
179
|
+
`model_info` (unlike the `anthropic/gemini` provider branches): an override
|
|
180
|
+
is assumed to already be a valid, SDK-native identifier the project author
|
|
181
|
+
vouches for, exactly like every other adapter's override handling ("already
|
|
182
|
+
SDK-native form, passed through as-is"). If that id isn't in
|
|
183
|
+
`OpenAIChatCompletionClient`'s own known-model table, the SDK's own clear
|
|
184
|
+
`model_info is required` error surfaces -- at which point the fix is the
|
|
185
|
+
same escape hatch every other adapter documents for its overrides: it needs
|
|
186
|
+
to be a model this client actually knows, or the caller composes their own
|
|
187
|
+
client outside `project.build(...)`.
|
|
188
|
+
|
|
189
|
+
model_params: `OpenAIChatCompletionClient` and `AnthropicChatCompletionClient`
|
|
190
|
+
DO NOT share one parameter set -- investigated at the level that actually
|
|
191
|
+
matters (the runtime whitelist each client filters constructor kwargs
|
|
192
|
+
through before building its `create_args`, `_create_args_from_config` in
|
|
193
|
+
each client's own module), not just each client's `CreateArguments`
|
|
194
|
+
`TypedDict` type hints (which turn out to be a red herring here: passing an
|
|
195
|
+
unsupported kwarg like `seed` to `AnthropicChatCompletionClient` does NOT
|
|
196
|
+
raise at construction time -- it is silently accepted and then silently
|
|
197
|
+
DROPPED by that filter, never reaching the Anthropic API, which is worse
|
|
198
|
+
than an error if this adapter mapped it blindly). Verified directly against
|
|
199
|
+
both real whitelists: `autogen_ext.models.openai._openai_client.
|
|
200
|
+
create_kwargs` contains `temperature`, `max_tokens`, `top_p`, `stop`,
|
|
201
|
+
`presence_penalty`, `frequency_penalty`, `seed` (no `top_k`);
|
|
202
|
+
`autogen_ext.models.anthropic._anthropic_client.anthropic_message_params`
|
|
203
|
+
contains `temperature`, `max_tokens`, `top_p`, `top_k`, `stop_sequences` (no
|
|
204
|
+
`presence_penalty`, `frequency_penalty`, `seed`, and the key is
|
|
205
|
+
`stop_sequences`, not `stop`). So this adapter maps two SEPARATE dicts,
|
|
206
|
+
`_OPENAI_MODEL_PARAM_MAP` (used for the `openai`/`gemini` provider branches
|
|
207
|
+
and the per-target override, all three of which build an
|
|
208
|
+
`OpenAIChatCompletionClient`) and `_ANTHROPIC_MODEL_PARAM_MAP` (the
|
|
209
|
+
`anthropic` provider branch only) -- unlike every flat-single-map adapter in
|
|
210
|
+
this codebase. Any key absent from whichever map applies is
|
|
211
|
+
warned-and-ignored, per the same policy every other adapter applies to keys
|
|
212
|
+
it doesn't map.
|
|
213
|
+
|
|
214
|
+
Offline construction -- a real difference from every other adapter here,
|
|
215
|
+
investigated not assumed: `OpenAIChatCompletionClient`/
|
|
216
|
+
`AnthropicChatCompletionClient.__init__` EAGERLY construct the underlying
|
|
217
|
+
`openai.AsyncOpenAI`/`anthropic.AsyncAnthropic` client right there in
|
|
218
|
+
`build()` -- and that raises immediately (`openai.OpenAIError: "Missing
|
|
219
|
+
credentials..."`) if no `api_key` is given AND the matching env var
|
|
220
|
+
(`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY`) isn't set --
|
|
221
|
+
verified directly, with the relevant env vars cleared. This is unlike
|
|
222
|
+
openai-agents' `Agent` (no client touched until `Runner.run` actually
|
|
223
|
+
executes) and unlike this project's `requires.env` mechanism (which is for
|
|
224
|
+
an agent's own tool-level env vars, e.g. `TAVILY_API_KEY` -- model-provider
|
|
225
|
+
auth has never been part of that contract for any target). Net effect:
|
|
226
|
+
`build()` for this target fails loudly on a missing provider API key all by
|
|
227
|
+
itself, with the SDK's own error text, un-wrapped -- this adapter does not
|
|
228
|
+
catch or re-word it, the same restraint every other adapter shows for
|
|
229
|
+
errors it doesn't specifically own. Tests in this codebase set fake
|
|
230
|
+
`OPENAI_API_KEY`/`ANTHROPIC_API_KEY`/`GEMINI_API_KEY` values up front for
|
|
231
|
+
exactly this reason (see test_adapter_autogen.py) -- construction never
|
|
232
|
+
makes a network call, but it does require *a* key-shaped string to exist
|
|
233
|
+
somewhere.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
from __future__ import annotations
|
|
237
|
+
|
|
238
|
+
import warnings
|
|
239
|
+
from typing import TYPE_CHECKING, Any
|
|
240
|
+
|
|
241
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
242
|
+
from autogen_agentchat.teams import Swarm
|
|
243
|
+
from autogen_core.models import ModelFamily, ModelInfo
|
|
244
|
+
from autogen_ext.models.anthropic import AnthropicChatCompletionClient
|
|
245
|
+
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
|
246
|
+
|
|
247
|
+
if TYPE_CHECKING:
|
|
248
|
+
from ..models import AgentSpec, Project
|
|
249
|
+
|
|
250
|
+
from .base import BaseAdapter
|
|
251
|
+
|
|
252
|
+
# agent-config.yaml `model_params` key -> OpenAIChatCompletionClient
|
|
253
|
+
# constructor kwarg. Used for the `openai`/`gemini` provider branches and the
|
|
254
|
+
# per-target override (see module docstring, "model_params").
|
|
255
|
+
_OPENAI_MODEL_PARAM_MAP = {
|
|
256
|
+
"temperature": "temperature",
|
|
257
|
+
"max_tokens": "max_tokens",
|
|
258
|
+
"top_p": "top_p",
|
|
259
|
+
"stop": "stop",
|
|
260
|
+
"presence_penalty": "presence_penalty",
|
|
261
|
+
"frequency_penalty": "frequency_penalty",
|
|
262
|
+
"seed": "seed",
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# agent-config.yaml `model_params` key -> AnthropicChatCompletionClient
|
|
266
|
+
# constructor kwarg. Used for the `anthropic` provider branch only -- this
|
|
267
|
+
# client's genuinely-accepted parameter set is smaller and differently named
|
|
268
|
+
# (`stop` -> `stop_sequences`) than the OpenAI-family client above (see
|
|
269
|
+
# module docstring, "model_params").
|
|
270
|
+
_ANTHROPIC_MODEL_PARAM_MAP = {
|
|
271
|
+
"temperature": "temperature",
|
|
272
|
+
"max_tokens": "max_tokens",
|
|
273
|
+
"top_p": "top_p",
|
|
274
|
+
"top_k": "top_k",
|
|
275
|
+
"stop": "stop_sequences",
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Explicit model_info for the `anthropic/...` provider branch -- bypasses
|
|
279
|
+
# autogen_ext's stale/buggy bundled Anthropic model-name table entirely (see
|
|
280
|
+
# module docstring, "Model routing"). `family: ModelFamily.UNKNOWN` is
|
|
281
|
+
# deliberate: this adapter doesn't know or assert which Claude generation a
|
|
282
|
+
# given alias/model id maps to, only that it is Anthropic-native and
|
|
283
|
+
# supports function calling (a requirement of every commonadk agent that
|
|
284
|
+
# has tools or outgoing edges).
|
|
285
|
+
_ANTHROPIC_MODEL_INFO: ModelInfo = {
|
|
286
|
+
"vision": False,
|
|
287
|
+
"function_calling": True,
|
|
288
|
+
"json_output": True,
|
|
289
|
+
"family": ModelFamily.UNKNOWN,
|
|
290
|
+
"structured_output": False,
|
|
291
|
+
"multiple_system_messages": False,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# Explicit model_info for the `gemini/...` provider branch -- bypasses
|
|
295
|
+
# autogen_ext's incomplete bundled Gemini model-name table (see module
|
|
296
|
+
# docstring, "Model routing"). Vision/structured_output reflect what every
|
|
297
|
+
# modern Gemini model actually supports; family is intentionally generic
|
|
298
|
+
# for the same reason as the Anthropic table above.
|
|
299
|
+
_GEMINI_MODEL_INFO: ModelInfo = {
|
|
300
|
+
"vision": True,
|
|
301
|
+
"function_calling": True,
|
|
302
|
+
"json_output": True,
|
|
303
|
+
"family": ModelFamily.UNKNOWN,
|
|
304
|
+
"structured_output": True,
|
|
305
|
+
"multiple_system_messages": False,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class AutoGenAdapter(BaseAdapter):
|
|
310
|
+
target = "autogen"
|
|
311
|
+
|
|
312
|
+
def build(self, project: "Project", agent_name: str) -> Any:
|
|
313
|
+
self._check_env(project, agent_name)
|
|
314
|
+
|
|
315
|
+
reachable = self._reachable_agents(project, agent_name) # agent_name first
|
|
316
|
+
|
|
317
|
+
agents: dict[str, AssistantAgent] = {}
|
|
318
|
+
for name in reachable:
|
|
319
|
+
spec = project.agents[name]
|
|
320
|
+
handoff_targets = [
|
|
321
|
+
edge.to for edge in project.graph.edges if edge.from_ == name
|
|
322
|
+
]
|
|
323
|
+
agents[name] = AssistantAgent(
|
|
324
|
+
name=spec.name,
|
|
325
|
+
model_client=self._client_for(project, spec),
|
|
326
|
+
tools=[t.func for t in spec.tools],
|
|
327
|
+
handoffs=handoff_targets,
|
|
328
|
+
system_message=spec.instructions,
|
|
329
|
+
description=spec.config.description,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
has_outgoing = any(edge.from_ == agent_name for edge in project.graph.edges)
|
|
333
|
+
if not has_outgoing:
|
|
334
|
+
# Nothing for the build root to hand off to -- handoffs only do
|
|
335
|
+
# anything inside a team, so the bare agent is the honest,
|
|
336
|
+
# directly runnable object here (see module docstring, "WHAT
|
|
337
|
+
# build() RETURNS").
|
|
338
|
+
return agents[agent_name]
|
|
339
|
+
|
|
340
|
+
participants = [agents[name] for name in reachable] # root first
|
|
341
|
+
return Swarm(participants, max_turns=len(reachable))
|
|
342
|
+
|
|
343
|
+
# -- model routing ------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
def _client_for(self, project: "Project", spec: "AgentSpec") -> Any:
|
|
346
|
+
override = spec.config.targets.get("autogen", {})
|
|
347
|
+
if "model" in override:
|
|
348
|
+
# Per-target override: passed through as the bare model id to
|
|
349
|
+
# the default client, no explicit model_info -- see module
|
|
350
|
+
# docstring, "Per-target override". Always the OpenAI-family
|
|
351
|
+
# client, so the OpenAI param map applies.
|
|
352
|
+
kwargs = self._model_param_kwargs(spec, _OPENAI_MODEL_PARAM_MAP)
|
|
353
|
+
return OpenAIChatCompletionClient(model=override["model"], **kwargs)
|
|
354
|
+
|
|
355
|
+
resolved = project.resolve_model(spec.name) # LiteLLM-format string
|
|
356
|
+
provider, sep, rest = resolved.partition("/")
|
|
357
|
+
if sep and provider == "openai":
|
|
358
|
+
kwargs = self._model_param_kwargs(spec, _OPENAI_MODEL_PARAM_MAP)
|
|
359
|
+
return OpenAIChatCompletionClient(model=rest, **kwargs)
|
|
360
|
+
if sep and provider == "anthropic":
|
|
361
|
+
kwargs = self._model_param_kwargs(spec, _ANTHROPIC_MODEL_PARAM_MAP)
|
|
362
|
+
return AnthropicChatCompletionClient(
|
|
363
|
+
model=rest, model_info=_ANTHROPIC_MODEL_INFO, **kwargs
|
|
364
|
+
)
|
|
365
|
+
if sep and provider == "gemini":
|
|
366
|
+
kwargs = self._model_param_kwargs(spec, _OPENAI_MODEL_PARAM_MAP)
|
|
367
|
+
return OpenAIChatCompletionClient(
|
|
368
|
+
model=rest, model_info=_GEMINI_MODEL_INFO, **kwargs
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
raise ValueError(
|
|
372
|
+
f"commonadk: agent {spec.name!r} resolves to model {resolved!r}, "
|
|
373
|
+
f"but the AutoGen target ('autogen') only ships native model "
|
|
374
|
+
f"clients for 'openai/...', 'anthropic/...', and 'gemini/...' "
|
|
375
|
+
f"providers (see autogen_adapter.py's module docstring, 'Model "
|
|
376
|
+
f"routing'). Fix this by either: using one of those providers "
|
|
377
|
+
f"(e.g. 'openai/gpt-4o'), changing {spec.name}'s model alias in "
|
|
378
|
+
f"config.yaml to one that resolves to a supported provider, or "
|
|
379
|
+
f"adding a `targets.autogen.model` override to "
|
|
380
|
+
f"{spec.name}/agent-config.yaml with a bare model id understood "
|
|
381
|
+
f"by autogen_ext's OpenAIChatCompletionClient."
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
def _model_param_kwargs(
|
|
385
|
+
self, spec: "AgentSpec", param_map: dict[str, str]
|
|
386
|
+
) -> dict[str, Any]:
|
|
387
|
+
kwargs: dict[str, Any] = {}
|
|
388
|
+
for key, value in spec.config.model_params.items():
|
|
389
|
+
mapped = param_map.get(key)
|
|
390
|
+
if mapped is None:
|
|
391
|
+
warnings.warn(
|
|
392
|
+
f"{spec.name}: model_params key '{key}' is not supported "
|
|
393
|
+
f"by the AutoGen adapter and will be ignored",
|
|
394
|
+
stacklevel=2,
|
|
395
|
+
)
|
|
396
|
+
continue
|
|
397
|
+
kwargs[mapped] = value
|
|
398
|
+
return kwargs
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Common interface every per-SDK adapter implements.
|
|
2
|
+
|
|
3
|
+
See plan.md ("Adapt") for the architecture this slots into: one adapter per
|
|
4
|
+
target SDK, each turning a framework-neutral `Project` + agent name into a
|
|
5
|
+
live, SDK-native agent object.
|
|
6
|
+
|
|
7
|
+
`_reachable_agents` and `_check_env` started life in M2's Google ADK adapter
|
|
8
|
+
and were hoisted here in M3 so both the Google ADK and OpenAI Agents adapters
|
|
9
|
+
share one implementation of "which agents does this build touch" and "fail
|
|
10
|
+
loudly, up front, if any of them is missing a required env var" -- the logic
|
|
11
|
+
is identical across targets, only the target name in the error message
|
|
12
|
+
differs (via `self.target`). This module stays free of any agent SDK
|
|
13
|
+
import -- `commonadk.load()` must keep working with no agent SDK installed
|
|
14
|
+
at all.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from abc import ABC, abstractmethod
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ..models import Project
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseAdapter(ABC):
|
|
27
|
+
"""One adapter per target SDK, registered in `commonadk.adapters.get_adapter`."""
|
|
28
|
+
|
|
29
|
+
target: str
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def build(self, project: "Project", agent_name: str) -> Any:
|
|
33
|
+
"""Build and return a live, SDK-native agent object for `agent_name`."""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
# -- env preflight (shared across targets) ---------------------------
|
|
37
|
+
|
|
38
|
+
def _check_env(self, project: "Project", agent_name: str) -> None:
|
|
39
|
+
"""Fail loudly, up front, if any reachable agent is missing a required env var.
|
|
40
|
+
|
|
41
|
+
Checks `agent_name` and every agent reachable from it via
|
|
42
|
+
`interactions.yaml` edges (not just its direct sub_agents/handoffs)
|
|
43
|
+
-- building the graph can transitively depend on any of them.
|
|
44
|
+
"""
|
|
45
|
+
missing_lines: list[str] = []
|
|
46
|
+
for name in self._reachable_agents(project, agent_name):
|
|
47
|
+
agent = project.agents[name]
|
|
48
|
+
missing_names = set(project.check_env(name))
|
|
49
|
+
if not missing_names:
|
|
50
|
+
continue
|
|
51
|
+
for req in agent.config.requires.env:
|
|
52
|
+
if req.name in missing_names:
|
|
53
|
+
detail = f"{req.name} ({req.description})" if req.description else req.name
|
|
54
|
+
missing_lines.append(f" - {name}: {detail}")
|
|
55
|
+
|
|
56
|
+
if missing_lines:
|
|
57
|
+
raise OSError(
|
|
58
|
+
"commonadk: missing required environment variable(s) for "
|
|
59
|
+
f"target {self.target!r} (building '{agent_name}'):\n"
|
|
60
|
+
+ "\n".join(missing_lines)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def _reachable_agents(self, project: "Project", start: str) -> list[str]:
|
|
64
|
+
"""Every agent name reachable from `start` (via edges), including `start`."""
|
|
65
|
+
seen = [start]
|
|
66
|
+
seen_set = {start}
|
|
67
|
+
i = 0
|
|
68
|
+
while i < len(seen):
|
|
69
|
+
current = seen[i]
|
|
70
|
+
i += 1
|
|
71
|
+
for edge in project.graph.edges:
|
|
72
|
+
if edge.from_ == current and edge.to not in seen_set:
|
|
73
|
+
seen_set.add(edge.to)
|
|
74
|
+
seen.append(edge.to)
|
|
75
|
+
return seen
|