agentx-kit 0.6.0__tar.gz → 0.7.0__tar.gz
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.
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/PKG-INFO +2 -2
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/pyproject.toml +2 -2
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/__init__.py +1 -1
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/cache.py +6 -3
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/cli.py +3 -1
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/generator.py +17 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/spec.py +12 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/ci.yml.j2 +9 -3
- agentx_kit-0.7.0/src/agentx/scaffold/templates/env.example.j2 +25 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/mcp_servers.json.j2 +1 -1
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/crew.py.j2 +7 -2
- agentx_kit-0.7.0/src/agentx/scaffold/templates/pkg/graph.py.j2 +172 -0
- agentx_kit-0.7.0/src/agentx/scaffold/templates/pkg/memory.py.j2 +25 -0
- agentx_kit-0.7.0/src/agentx/scaffold/templates/pkg/nodes.py.j2 +219 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/rag.py.j2 +15 -4
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/server.py.j2 +34 -9
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/state.py.j2 +2 -6
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/wizard.py +26 -0
- agentx_kit-0.6.0/src/agentx/scaffold/templates/env.example.j2 +0 -12
- agentx_kit-0.6.0/src/agentx/scaffold/templates/pkg/graph.py.j2 +0 -69
- agentx_kit-0.6.0/src/agentx/scaffold/templates/pkg/memory.py.j2 +0 -17
- agentx_kit-0.6.0/src/agentx/scaffold/templates/pkg/nodes.py.j2 +0 -76
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/.agentx/llm_cache.sqlite +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/.claude-plugin/marketplace.json +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/.github/workflows/publish.yml +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/.gitignore +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/DESIGN.md +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/LICENSE +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/README.md +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/RESEARCH.md +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/examples/README.md +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/examples/demo_local.sh +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/examples/demo_mcp.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/claude-plugin/.claude-plugin/plugin.json +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/claude-plugin/.mcp.json +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/claude-plugin/README.md +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/claude-plugin/commands/new-agent.md +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/vscode/.vscodeignore +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/vscode/README.md +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/vscode/extension.js +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/vscode/package.json +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/config.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/connector/__init__.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/connector/build.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/connector/recommend.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/connector/server.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/dashboard/__init__.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/dashboard/app.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/frameworks/__init__.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/frameworks/crewai_agent.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/frameworks/langchain_agent.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/guardrails.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/insights/__init__.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/insights/analyze.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/insights/log.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/insights/optimize.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/insights/tokens.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/memory/__init__.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/memory/store.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/observability.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/prompts/__init__.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/prompts/templates.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/providers/__init__.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/providers/base.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/providers/factory.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/providers/registry.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/rag/__init__.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/rag/pipeline.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/reliability.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/__init__.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/prompts_store.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/Dockerfile.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/README.md.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/docker-compose.yml.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/dockerignore.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/evals/dataset.json.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/evals/run_evals.py.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/gitignore.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/__init__.py.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/agents.py.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/config.py.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/guardrails.py.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/main.py.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/observability.py.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/prompts.py.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/tasks.py.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/tools.py.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pyproject.toml.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/skills_seed.json.j2 +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/skills/__init__.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/skills/registry.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/structured.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/tools/__init__.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/tools/builtin.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/tools/mcp.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_cache.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_connector.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_enterprise.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_insights.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_prompts.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_providers.py +0 -0
- {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_scaffold.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentx-kit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: An open-source, provider-agnostic agentic framework + interactive project scaffolder for LangChain and CrewAI. Pick your LLM provider, agents, RAG, memory, MCP tools and skills — generate a ready-to-run uv project.
|
|
5
5
|
Project-URL: Homepage, https://github.com/muhammadyahiya/agentx-kit
|
|
6
6
|
Project-URL: Repository, https://github.com/muhammadyahiya/agentx-kit
|
|
7
7
|
Project-URL: Issues, https://github.com/muhammadyahiya/agentx-kit/issues
|
|
8
|
-
Author:
|
|
8
|
+
Author: OptimumAI
|
|
9
9
|
License: MIT
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Keywords: agents,azure,bedrock,crewai,gemini,langchain,llm,mcp,openrouter,rag,scaffold,vertex
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
# PyPI distribution name (import name + CLI stay `agentx`; `agentx` was taken).
|
|
3
3
|
name = "agentx-kit"
|
|
4
|
-
version = "0.
|
|
4
|
+
version = "0.7.0"
|
|
5
5
|
description = "An open-source, provider-agnostic agentic framework + interactive project scaffolder for LangChain and CrewAI. Pick your LLM provider, agents, RAG, memory, MCP tools and skills — generate a ready-to-run uv project."
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
requires-python = ">=3.10,<3.14"
|
|
8
8
|
license = { text = "MIT" }
|
|
9
|
-
authors = [{ name = "
|
|
9
|
+
authors = [{ name = "OptimumAI" }]
|
|
10
10
|
keywords = ["agents", "llm", "langchain", "crewai", "rag", "mcp", "scaffold", "openrouter", "bedrock", "vertex", "azure", "gemini"]
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Development Status :: 4 - Beta",
|
|
@@ -23,6 +23,8 @@ import time
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import Any
|
|
25
25
|
|
|
26
|
+
from langchain_core.caches import BaseCache
|
|
27
|
+
|
|
26
28
|
_DEFAULT_PATH = ".agentx/llm_cache.sqlite"
|
|
27
29
|
_lock = threading.Lock()
|
|
28
30
|
|
|
@@ -39,11 +41,12 @@ def _model_from_llm_string(llm_string: str) -> str:
|
|
|
39
41
|
return "gpt-4o-mini"
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
class AgentXCache:
|
|
44
|
+
class AgentXCache(BaseCache):
|
|
43
45
|
"""A LangChain ``BaseCache`` backed by SQLite, with TTL + savings stats.
|
|
44
46
|
|
|
45
|
-
Implements ``lookup``/``update
|
|
46
|
-
``
|
|
47
|
+
Implements ``lookup``/``update``/``clear``; the async ``alookup``/``aupdate``
|
|
48
|
+
paths are inherited from ``BaseCache`` (which runs the sync methods in an
|
|
49
|
+
executor), so it works under both ``invoke`` and ``ainvoke``.
|
|
47
50
|
"""
|
|
48
51
|
|
|
49
52
|
def __init__(self, path: str | Path = _DEFAULT_PATH, ttl: int | None = None):
|
|
@@ -153,6 +153,7 @@ def new(
|
|
|
153
153
|
provider: str = typer.Option("openai", help="Provider id (with --yes)."),
|
|
154
154
|
model: str = typer.Option("", help="Model id (with --yes; blank = provider default)."),
|
|
155
155
|
agents: int = typer.Option(1, help="Number of agents (with --yes)."),
|
|
156
|
+
orchestration: str = typer.Option("supervisor", help="supervisor|sequential|parallel — how agents connect (with --yes, only for LangGraph with >1 agents)."),
|
|
156
157
|
prompt: str = typer.Option("", "--prompt", "-p", help="System prompt for the first agent (with --yes)."),
|
|
157
158
|
role: str = typer.Option("Helpful Assistant", help="Role for the first agent (with --yes)."),
|
|
158
159
|
goal: str = typer.Option("Help the user accomplish their task accurately.", help="Goal for the first agent (with --yes)."),
|
|
@@ -187,7 +188,8 @@ def new(
|
|
|
187
188
|
agent_specs.append(AgentSpec(name=a_name))
|
|
188
189
|
spec = ProjectSpec(
|
|
189
190
|
name=name or "my-agent", framework=framework, provider=provider, model=model,
|
|
190
|
-
agents=agent_specs,
|
|
191
|
+
agents=agent_specs, orchestration=orchestration,
|
|
192
|
+
use_rag=rag, memory=memory, use_mcp=mcp, use_skills=skills,
|
|
191
193
|
prompt_style="custom" if prompt else "default",
|
|
192
194
|
observability=observability, guardrails=guardrails, serve=serve,
|
|
193
195
|
docker=docker, ci=ci, evals=evals,
|
|
@@ -85,6 +85,7 @@ def _context(spec: ProjectSpec) -> dict:
|
|
|
85
85
|
"extras": _extras(spec),
|
|
86
86
|
"extras_str": ",".join(_extras(spec)),
|
|
87
87
|
"multi_agent": len(spec.agents) > 1,
|
|
88
|
+
"orchestration": spec.orchestration,
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
|
|
@@ -139,6 +140,7 @@ def _write_manifest(target: Path, spec: ProjectSpec) -> Path:
|
|
|
139
140
|
"model": spec.model or get_spec(spec.provider).default_model,
|
|
140
141
|
"python_version": ">=3.10,<3.14",
|
|
141
142
|
"agents": [a.name for a in spec.agents],
|
|
143
|
+
"orchestration": spec.orchestration,
|
|
142
144
|
"features": {
|
|
143
145
|
"rag": spec.use_rag,
|
|
144
146
|
"memory": spec.memory,
|
|
@@ -179,6 +181,21 @@ def generate_project(spec: ProjectSpec, target_dir: str | Path, overwrite: bool
|
|
|
179
181
|
out_path.write_text(rendered, encoding="utf-8")
|
|
180
182
|
written.append(out_path)
|
|
181
183
|
|
|
184
|
+
# Seed a knowledge/ directory when RAG or the MCP filesystem server needs one
|
|
185
|
+
# (the restricted MCP server points at ./knowledge and RAG indexes it).
|
|
186
|
+
if spec.use_rag or spec.use_mcp:
|
|
187
|
+
knowledge_dir = target / "knowledge"
|
|
188
|
+
knowledge_dir.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
seed = knowledge_dir / "README.md"
|
|
190
|
+
if not seed.exists():
|
|
191
|
+
seed.write_text(
|
|
192
|
+
f"# {spec.slug} knowledge base\n\n"
|
|
193
|
+
"Drop `.txt` / `.md` files here. They are indexed for RAG"
|
|
194
|
+
" and exposed (read-only) to the MCP filesystem tool.\n",
|
|
195
|
+
encoding="utf-8",
|
|
196
|
+
)
|
|
197
|
+
written.append(seed)
|
|
198
|
+
|
|
182
199
|
# The prompt source of truth — edited by hand or via `agentx prompt`.
|
|
183
200
|
written.append(prompts_store.write_prompts(target, spec))
|
|
184
201
|
# A single declarative manifest of the project (à la langgraph.json).
|
|
@@ -15,6 +15,12 @@ Framework = Literal["langgraph", "crewai"]
|
|
|
15
15
|
MemoryMode = Literal["none", "short", "long", "both"]
|
|
16
16
|
PromptStyle = Literal["default", "custom"]
|
|
17
17
|
|
|
18
|
+
# How multiple agents are wired together (LangGraph only; CrewAI always uses sequential crew).
|
|
19
|
+
# supervisor — an LLM router decides which worker acts next (dynamic, context-aware)
|
|
20
|
+
# sequential — agents run in order: agent_1 → agent_2 → … (pipeline / chain-of-thought)
|
|
21
|
+
# parallel — all agents handle the same input simultaneously; results are merged
|
|
22
|
+
OrchestrationMode = Literal["supervisor", "sequential", "parallel"]
|
|
23
|
+
|
|
18
24
|
|
|
19
25
|
def to_snake(name: str) -> str:
|
|
20
26
|
s = re.sub(r"[^0-9a-zA-Z]+", "_", name.strip().lower()).strip("_")
|
|
@@ -40,6 +46,8 @@ class ProjectSpec(BaseModel):
|
|
|
40
46
|
provider: str = "openai"
|
|
41
47
|
model: str = "" # blank → provider default
|
|
42
48
|
agents: list[AgentSpec] = Field(default_factory=lambda: [AgentSpec()])
|
|
49
|
+
# How agents are connected (only meaningful when len(agents) > 1 and framework == langgraph).
|
|
50
|
+
orchestration: OrchestrationMode = "supervisor"
|
|
43
51
|
use_rag: bool = False
|
|
44
52
|
memory: MemoryMode = "none"
|
|
45
53
|
use_mcp: bool = False
|
|
@@ -84,3 +92,7 @@ class ProjectSpec(BaseModel):
|
|
|
84
92
|
@property
|
|
85
93
|
def use_long_memory(self) -> bool:
|
|
86
94
|
return self.memory in ("long", "both")
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def multi_agent(self) -> bool:
|
|
98
|
+
return len(self.agents) > 1
|
|
@@ -33,9 +33,15 @@ jobs:
|
|
|
33
33
|
- name: Install
|
|
34
34
|
run: uv pip install --system -e .
|
|
35
35
|
- name: Run eval gate
|
|
36
|
+
# Non-blocking: the eval harness needs a reachable model (an API key or a
|
|
37
|
+
# local model server). Add your provider secret(s) below and drop
|
|
38
|
+
# `continue-on-error` to make this a hard CI gate.
|
|
39
|
+
continue-on-error: true
|
|
36
40
|
env:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
AGENTX_PROVIDER: {{ spec.provider }}
|
|
42
|
+
AGENTX_MODEL: {{ model }}
|
|
43
|
+
{% for v in provider_env %}
|
|
44
|
+
{{ v }}: {% raw %}${{ secrets.{% endraw %}{{ v }}{% raw %} }}{% endraw %}
|
|
45
|
+
{% endfor %}
|
|
40
46
|
run: python evals/run_evals.py
|
|
41
47
|
{% endif %}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# --- Provider selection (read by agentx) ---
|
|
2
|
+
AGENTX_PROVIDER={{ spec.provider }}
|
|
3
|
+
AGENTX_MODEL={{ model }}
|
|
4
|
+
AGENTX_TEMPERATURE=0.3
|
|
5
|
+
|
|
6
|
+
# --- Credentials for {{ provider_label }} ---
|
|
7
|
+
{% for v in provider_env %}
|
|
8
|
+
{{ v }}=
|
|
9
|
+
{% endfor %}
|
|
10
|
+
{% if not provider_env %}
|
|
11
|
+
# {{ provider_label }} is local — no API key required.
|
|
12
|
+
{% if spec.provider == 'ollama' %}
|
|
13
|
+
# Start it first: ollama serve && ollama pull {{ model }}
|
|
14
|
+
# OLLAMA_BASE_URL=http://localhost:11434
|
|
15
|
+
{% endif %}
|
|
16
|
+
{% endif %}
|
|
17
|
+
{% if spec.observability %}
|
|
18
|
+
|
|
19
|
+
# --- Observability (optional) ---
|
|
20
|
+
AGENTX_TELEMETRY=true
|
|
21
|
+
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
|
22
|
+
# LANGFUSE_PUBLIC_KEY=
|
|
23
|
+
# LANGFUSE_SECRET_KEY=
|
|
24
|
+
# LANGFUSE_HOST=https://cloud.langfuse.com
|
|
25
|
+
{% endif %}
|
|
@@ -13,6 +13,11 @@ def build_project_crew(user_input: str):
|
|
|
13
13
|
return build_crew(list(agents.values()), tasks)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def run_text(message: str) -> str:
|
|
17
|
-
"""Run the crew on a message and return the final text (used by main/server).
|
|
16
|
+
def run_text(message: str, thread_id: str = "default") -> str:
|
|
17
|
+
"""Run the crew on a message and return the final text (used by main/server).
|
|
18
|
+
|
|
19
|
+
``thread_id`` is accepted for API-compatibility with the server; a CrewAI
|
|
20
|
+
kickoff is stateless per call, so it is not used to partition memory here.
|
|
21
|
+
"""
|
|
22
|
+
del thread_id
|
|
18
23
|
return str(build_project_crew(message).kickoff())
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""LangGraph workflow for {{ spec.slug }}.
|
|
2
|
+
|
|
3
|
+
Exposes a module-level compiled ``graph`` (the LangGraph convention, also picked
|
|
4
|
+
up by ``langgraph dev``) and ``run_text`` / ``stream_text`` helpers.
|
|
5
|
+
|
|
6
|
+
{% if multi_agent %}
|
|
7
|
+
Orchestration: {{ orchestration }}
|
|
8
|
+
{% if orchestration == "supervisor" %} An LLM router decides which specialist to call next.{% endif %}
|
|
9
|
+
{% if orchestration == "sequential" %} Agents run in order: {% for a in spec.agents %}{{ a.name }}{% if not loop.last %} → {% endif %}{% endfor %}.{% endif %}
|
|
10
|
+
{% if orchestration == "parallel" %} All agents answer simultaneously; results are merged.{% endif %}
|
|
11
|
+
{% endif %}
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
from langgraph.checkpoint.memory import MemorySaver
|
|
19
|
+
from langgraph.graph import END, START, StateGraph
|
|
20
|
+
|
|
21
|
+
from .state import AgentState
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
RECURSION_LIMIT = 25
|
|
26
|
+
{% if not multi_agent %}
|
|
27
|
+
from langgraph.prebuilt import ToolNode, tools_condition
|
|
28
|
+
|
|
29
|
+
from .nodes import call_model
|
|
30
|
+
from .tools import get_tools
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_graph():
|
|
34
|
+
"""Build a ReAct agent graph: agent ⇄ tools loop, with checkpointed memory."""
|
|
35
|
+
builder = StateGraph(AgentState)
|
|
36
|
+
builder.add_node("agent", call_model)
|
|
37
|
+
builder.add_node("tools", ToolNode(get_tools()))
|
|
38
|
+
|
|
39
|
+
builder.add_edge(START, "agent")
|
|
40
|
+
builder.add_conditional_edges("agent", tools_condition)
|
|
41
|
+
builder.add_edge("tools", "agent")
|
|
42
|
+
|
|
43
|
+
return builder.compile(checkpointer=MemorySaver())
|
|
44
|
+
{% elif orchestration == "supervisor" %}
|
|
45
|
+
from .nodes import WORKERS, make_worker, supervisor
|
|
46
|
+
from .prompts import resolved_system_prompts
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_graph():
|
|
50
|
+
"""Supervisor graph: an LLM router picks the next specialist each turn."""
|
|
51
|
+
builder = StateGraph(AgentState)
|
|
52
|
+
builder.add_node("supervisor", supervisor)
|
|
53
|
+
for name, system_prompt in resolved_system_prompts().items():
|
|
54
|
+
builder.add_node(name, make_worker(name, system_prompt))
|
|
55
|
+
|
|
56
|
+
builder.add_edge(START, "supervisor")
|
|
57
|
+
builder.add_conditional_edges(
|
|
58
|
+
"supervisor",
|
|
59
|
+
lambda state: state.get("next", "FINISH"),
|
|
60
|
+
{**{name: name for name in WORKERS}, "FINISH": END},
|
|
61
|
+
)
|
|
62
|
+
for name in WORKERS:
|
|
63
|
+
builder.add_edge(name, "supervisor")
|
|
64
|
+
|
|
65
|
+
return builder.compile(checkpointer=MemorySaver())
|
|
66
|
+
{% elif orchestration == "sequential" %}
|
|
67
|
+
from .nodes import WORKERS, make_worker
|
|
68
|
+
from .prompts import resolved_system_prompts
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_graph():
|
|
72
|
+
"""Sequential pipeline: agents run in order, each seeing the previous output."""
|
|
73
|
+
builder = StateGraph(AgentState)
|
|
74
|
+
prompts = resolved_system_prompts()
|
|
75
|
+
names = list(prompts)
|
|
76
|
+
|
|
77
|
+
for name in names:
|
|
78
|
+
builder.add_node(name, make_worker(name, prompts[name]))
|
|
79
|
+
|
|
80
|
+
builder.add_edge(START, names[0])
|
|
81
|
+
for i in range(len(names) - 1):
|
|
82
|
+
builder.add_edge(names[i], names[i + 1])
|
|
83
|
+
builder.add_edge(names[-1], END)
|
|
84
|
+
|
|
85
|
+
return builder.compile(checkpointer=MemorySaver())
|
|
86
|
+
{% elif orchestration == "parallel" %}
|
|
87
|
+
from langgraph.types import Send
|
|
88
|
+
|
|
89
|
+
from .nodes import WORKERS, make_worker, merge_results
|
|
90
|
+
from .prompts import resolved_system_prompts
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _fan_out(state: AgentState) -> list[Send]:
|
|
94
|
+
"""Send the same user message to every worker simultaneously."""
|
|
95
|
+
return [Send(name, state) for name in WORKERS]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def build_graph():
|
|
99
|
+
"""Parallel graph: all agents answer simultaneously, then results are merged."""
|
|
100
|
+
builder = StateGraph(AgentState)
|
|
101
|
+
for name, system_prompt in resolved_system_prompts().items():
|
|
102
|
+
builder.add_node(name, make_worker(name, system_prompt))
|
|
103
|
+
builder.add_node("merge", merge_results)
|
|
104
|
+
|
|
105
|
+
builder.add_conditional_edges(START, _fan_out, WORKERS)
|
|
106
|
+
for name in WORKERS:
|
|
107
|
+
builder.add_edge(name, "merge")
|
|
108
|
+
builder.add_edge("merge", END)
|
|
109
|
+
|
|
110
|
+
return builder.compile(checkpointer=MemorySaver())
|
|
111
|
+
{% endif %}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
graph = build_graph()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _config(thread_id: str) -> dict:
|
|
118
|
+
return {"configurable": {"thread_id": thread_id}, "recursion_limit": RECURSION_LIMIT}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def arun_text(message: str, thread_id: str = "default") -> str:
|
|
122
|
+
"""Async invoke; runs async-only tools (MCP) correctly.
|
|
123
|
+
|
|
124
|
+
Pass a unique ``thread_id`` per user/session — callers that omit it share
|
|
125
|
+
one conversation history (fine for a single CLI session, wrong for a server).
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
result = await graph.ainvoke(
|
|
129
|
+
{"messages": [{"role": "user", "content": message}]}, config=_config(thread_id)
|
|
130
|
+
)
|
|
131
|
+
except Exception as exc: # noqa: BLE001
|
|
132
|
+
logger.exception("graph invocation failed")
|
|
133
|
+
return f"Sorry, something went wrong: {exc}"
|
|
134
|
+
messages = result.get("messages", [])
|
|
135
|
+
return getattr(messages[-1], "content", "") if messages else ""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
_loop: asyncio.AbstractEventLoop | None = None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _sync_loop() -> asyncio.AbstractEventLoop:
|
|
142
|
+
"""Reuse one loop across calls so MCP stdio sessions don't break on turn 2."""
|
|
143
|
+
global _loop
|
|
144
|
+
if _loop is None or _loop.is_closed():
|
|
145
|
+
_loop = asyncio.new_event_loop()
|
|
146
|
+
return _loop
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def run_text(message: str, thread_id: str = "default") -> str:
|
|
150
|
+
"""Sync wrapper for CLI / REPL use. Do NOT call from an async context."""
|
|
151
|
+
return _sync_loop().run_until_complete(arun_text(message, thread_id))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def stream_text(message: str, thread_id: str = "default"):
|
|
155
|
+
"""Yield reply tokens as the model produces them."""
|
|
156
|
+
try:
|
|
157
|
+
async for _ns, (chunk, meta) in graph.astream(
|
|
158
|
+
{"messages": [{"role": "user", "content": message}]},
|
|
159
|
+
config=_config(thread_id),
|
|
160
|
+
stream_mode="messages",
|
|
161
|
+
subgraphs=True,
|
|
162
|
+
):
|
|
163
|
+
{% if multi_agent and orchestration == "supervisor" %}
|
|
164
|
+
if meta.get("langgraph_node") == "supervisor":
|
|
165
|
+
continue
|
|
166
|
+
{% endif %}
|
|
167
|
+
text = getattr(chunk, "content", "")
|
|
168
|
+
if text:
|
|
169
|
+
yield text
|
|
170
|
+
except Exception as exc: # noqa: BLE001
|
|
171
|
+
logger.exception("graph streaming failed")
|
|
172
|
+
yield f"Sorry, something went wrong: {exc}"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Memory helpers for {{ spec.slug }} (mode: {{ spec.memory }}).
|
|
2
|
+
|
|
3
|
+
NOTE: the LangGraph graph already keeps short-term conversation memory per
|
|
4
|
+
``thread_id`` via its checkpointer (see graph.py). These helpers are optional
|
|
5
|
+
extras for when you want an explicit rolling window or durable cross-session
|
|
6
|
+
storage outside the graph — import them where you need them.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
{% if spec.use_short_memory %}
|
|
11
|
+
from agentx.memory import ConversationMemory
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def new_session_memory(max_turns: int = 12) -> ConversationMemory:
|
|
15
|
+
"""A short-term, in-process rolling window of recent turns."""
|
|
16
|
+
return ConversationMemory(max_turns=max_turns)
|
|
17
|
+
{% endif %}
|
|
18
|
+
{% if spec.use_long_memory %}
|
|
19
|
+
from agentx.memory import LongTermMemory
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def long_term(session_id: str = "default") -> LongTermMemory:
|
|
23
|
+
"""Persistent per-session memory (JSONL under data/memory/)."""
|
|
24
|
+
return LongTermMemory(f"data/memory/{session_id}.jsonl")
|
|
25
|
+
{% endif %}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Graph nodes for {{ spec.slug }}.
|
|
2
|
+
|
|
3
|
+
A node is a function ``state -> partial state``. The model is built through
|
|
4
|
+
AgentX's provider-agnostic factory but everything else is plain LangChain /
|
|
5
|
+
LangGraph, so you fully own the graph.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from langchain_core.messages import AIMessage, SystemMessage
|
|
13
|
+
|
|
14
|
+
from agentx import get_chat_model
|
|
15
|
+
from .config import MODEL, PROVIDER
|
|
16
|
+
from .prompts import resolved_system_prompts
|
|
17
|
+
from .tools import get_tools
|
|
18
|
+
{% if spec.use_skills %}from agentx.skills import get_skill_registry
|
|
19
|
+
{% endif %}
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_TOOLS = get_tools()
|
|
24
|
+
_TOOL_MAP = {t.name: t for t in _TOOLS}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _model(bind_tools: bool = True):
|
|
28
|
+
llm = get_chat_model(PROVIDER, MODEL)
|
|
29
|
+
return llm.bind_tools(_TOOLS) if bind_tools and _TOOLS else llm
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
{% if spec.use_skills %}
|
|
33
|
+
def _skills_suffix() -> str:
|
|
34
|
+
skills = get_skill_registry("data/skills").combined_instructions()
|
|
35
|
+
return ("\nApply these skills:\n" + skills) if skills else ""
|
|
36
|
+
{% endif %}
|
|
37
|
+
|
|
38
|
+
async def _exec_json_tool_call(content: str) -> str | None:
|
|
39
|
+
"""Fallback for models (e.g. llama3.2/Ollama) that emit tool calls as JSON
|
|
40
|
+
text instead of populating the structured ``tool_calls`` field.
|
|
41
|
+
|
|
42
|
+
Returns the tool result string, or None if the content isn't a tool call.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
data = json.loads(content.strip())
|
|
46
|
+
except (json.JSONDecodeError, ValueError):
|
|
47
|
+
return None
|
|
48
|
+
if not isinstance(data, dict) or "name" not in data:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
name: str = data["name"]
|
|
52
|
+
args = data.get("parameters", data.get("args", data.get("arguments", {})))
|
|
53
|
+
if not isinstance(args, dict):
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
# Match by exact name, then by description prefix (models sometimes use the
|
|
57
|
+
# description instead of the function name).
|
|
58
|
+
tool = _TOOL_MAP.get(name)
|
|
59
|
+
if tool is None:
|
|
60
|
+
for t in _TOOLS:
|
|
61
|
+
if t.description.lower().startswith(name.lower()[:30]):
|
|
62
|
+
tool = t
|
|
63
|
+
break
|
|
64
|
+
if tool is None:
|
|
65
|
+
logger.debug("json_tool_call: no tool matched name=%r", name)
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
return str(await tool.ainvoke(args))
|
|
70
|
+
except NotImplementedError:
|
|
71
|
+
try:
|
|
72
|
+
return str(tool.invoke(args))
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
logger.warning("json_tool_call execution failed: %s", exc)
|
|
75
|
+
return None
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
logger.warning("json_tool_call execution failed: %s", exc)
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
{% if not multi_agent %}
|
|
82
|
+
# ---- Single agent ----
|
|
83
|
+
_PROMPTS = resolved_system_prompts()
|
|
84
|
+
_SYSTEM = next(iter(_PROMPTS.values()), "You are a helpful assistant.")
|
|
85
|
+
{% if spec.use_skills %}_SYSTEM = _SYSTEM + _skills_suffix()
|
|
86
|
+
{% endif %}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def call_model(state) -> dict:
|
|
90
|
+
"""Agent node: call the LLM, execute tools, and return a final answer.
|
|
91
|
+
|
|
92
|
+
Handles two tool-calling patterns:
|
|
93
|
+
- Structured ``tool_calls`` field (OpenAI, Anthropic, newer Ollama models):
|
|
94
|
+
the built-in ``ToolNode`` / ``tools_condition`` picks these up normally.
|
|
95
|
+
- JSON text in ``content`` (llama3.2 and other small Ollama models):
|
|
96
|
+
detected here, executed directly, and folded back into the final reply.
|
|
97
|
+
"""
|
|
98
|
+
messages = [SystemMessage(_SYSTEM)] + state["messages"]
|
|
99
|
+
try:
|
|
100
|
+
response = await _model().ainvoke(messages)
|
|
101
|
+
except Exception as exc: # noqa: BLE001
|
|
102
|
+
logger.exception("call_model failed")
|
|
103
|
+
return {"messages": [AIMessage(content=f"Sorry, I hit an error contacting the model: {exc}")]}
|
|
104
|
+
|
|
105
|
+
# If the model supports structured tool calling, return the response as-is
|
|
106
|
+
# and let the graph's ToolNode / tools_condition handle the rest.
|
|
107
|
+
if response.tool_calls:
|
|
108
|
+
return {"messages": [response]}
|
|
109
|
+
|
|
110
|
+
# Fallback: detect JSON tool calls emitted as plain text (llama3.2 style).
|
|
111
|
+
if _TOOLS and response.content:
|
|
112
|
+
tool_result = await _exec_json_tool_call(str(response.content))
|
|
113
|
+
if tool_result is not None:
|
|
114
|
+
logger.debug("json_tool_call executed; re-invoking with result")
|
|
115
|
+
augmented = messages + [
|
|
116
|
+
response,
|
|
117
|
+
{"role": "user", "content": (
|
|
118
|
+
f"[Tool result]\n{tool_result}\n\n"
|
|
119
|
+
"Use the above result to answer the original question directly and concisely."
|
|
120
|
+
)},
|
|
121
|
+
]
|
|
122
|
+
try:
|
|
123
|
+
response = await _model(bind_tools=False).ainvoke(augmented)
|
|
124
|
+
except Exception as exc: # noqa: BLE001
|
|
125
|
+
logger.warning("re-invoke after tool result failed: %s", exc)
|
|
126
|
+
|
|
127
|
+
return {"messages": [response]}
|
|
128
|
+
{% else %}
|
|
129
|
+
# ---- Multi-agent ({{ orchestration }}) ----
|
|
130
|
+
from langgraph.prebuilt import create_react_agent
|
|
131
|
+
|
|
132
|
+
WORKERS = list(resolved_system_prompts().keys())
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def make_worker(name: str, system_prompt: str):
|
|
136
|
+
"""Build a worker as a full ReAct sub-agent (tools actually execute)."""
|
|
137
|
+
{% if spec.use_skills %} system_prompt = system_prompt + _skills_suffix()
|
|
138
|
+
{% endif %}
|
|
139
|
+
agent = create_react_agent(_model(bind_tools=False), _TOOLS, prompt=system_prompt)
|
|
140
|
+
|
|
141
|
+
async def worker(state) -> dict:
|
|
142
|
+
try:
|
|
143
|
+
result = await agent.ainvoke(
|
|
144
|
+
{"messages": state["messages"]},
|
|
145
|
+
config={"recursion_limit": 12},
|
|
146
|
+
)
|
|
147
|
+
# get last non-empty AIMessage
|
|
148
|
+
content = ""
|
|
149
|
+
for m in reversed(result["messages"]):
|
|
150
|
+
c = getattr(m, "content", "")
|
|
151
|
+
if c and isinstance(c, str):
|
|
152
|
+
content = c
|
|
153
|
+
break
|
|
154
|
+
# Fallback: execute JSON tool call emitted by the sub-agent's model
|
|
155
|
+
if not content or (content.startswith("{") and '"name"' in content):
|
|
156
|
+
tool_result = await _exec_json_tool_call(content or "")
|
|
157
|
+
if tool_result:
|
|
158
|
+
try:
|
|
159
|
+
follow_up = result["messages"] + [
|
|
160
|
+
{"role": "user", "content": (
|
|
161
|
+
f"[Tool result]\n{tool_result}\n\n"
|
|
162
|
+
"Now answer the user's original question using this result."
|
|
163
|
+
)}
|
|
164
|
+
]
|
|
165
|
+
fr = await _model(bind_tools=False).ainvoke(
|
|
166
|
+
[SystemMessage(system_prompt)] + follow_up
|
|
167
|
+
)
|
|
168
|
+
content = fr.content or content
|
|
169
|
+
except Exception as exc:
|
|
170
|
+
logger.warning("worker '%s' follow-up failed: %s", name, exc)
|
|
171
|
+
except Exception as exc: # noqa: BLE001
|
|
172
|
+
logger.exception("worker '%s' failed", name)
|
|
173
|
+
content = f"Sorry, I ({name}) hit an error: {exc}"
|
|
174
|
+
return {"messages": [AIMessage(content=content, name=name)]}
|
|
175
|
+
|
|
176
|
+
return worker
|
|
177
|
+
|
|
178
|
+
{% if orchestration == "supervisor" %}
|
|
179
|
+
_SUPERVISOR_SYSTEM = (
|
|
180
|
+
"You are a supervisor routing work between these specialists: {workers}. "
|
|
181
|
+
"Read the conversation and decide who should act NEXT to make progress.\n"
|
|
182
|
+
"Rules:\n"
|
|
183
|
+
"- Reply with ONLY one token and nothing else.\n"
|
|
184
|
+
"- The token must be exactly one of: {choices}.\n"
|
|
185
|
+
"- Pick the single most relevant specialist for an unanswered request.\n"
|
|
186
|
+
"- Reply FINISH if the latest assistant message already answers the user, "
|
|
187
|
+
"or if no specialist can help further."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def supervisor(state) -> dict:
|
|
192
|
+
"""Router node: pick the next worker (or FINISH)."""
|
|
193
|
+
choices = WORKERS + ["FINISH"]
|
|
194
|
+
sys = _SUPERVISOR_SYSTEM.format(workers=", ".join(WORKERS), choices=", ".join(choices))
|
|
195
|
+
try:
|
|
196
|
+
response = _model(bind_tools=False).invoke([SystemMessage(sys)] + state["messages"])
|
|
197
|
+
raw = (response.content or "").strip()
|
|
198
|
+
except Exception: # noqa: BLE001
|
|
199
|
+
logger.exception("supervisor routing failed; finishing")
|
|
200
|
+
return {"next": "FINISH"}
|
|
201
|
+
|
|
202
|
+
lowered = raw.lower()
|
|
203
|
+
choice = "FINISH"
|
|
204
|
+
for name in WORKERS:
|
|
205
|
+
if name.lower() in lowered:
|
|
206
|
+
choice = name
|
|
207
|
+
break
|
|
208
|
+
logger.debug("supervisor routed to %s (raw=%r)", choice, raw[:80])
|
|
209
|
+
return {"next": choice}
|
|
210
|
+
{% elif orchestration == "parallel" %}
|
|
211
|
+
def merge_results(state) -> dict:
|
|
212
|
+
"""Collect all worker outputs and present them together."""
|
|
213
|
+
worker_msgs = [m for m in state["messages"] if getattr(m, "name", None) in set(WORKERS)]
|
|
214
|
+
if not worker_msgs:
|
|
215
|
+
return {}
|
|
216
|
+
combined = "\n\n".join(f"**{m.name}**:\n{m.content}" for m in worker_msgs)
|
|
217
|
+
return {"messages": [AIMessage(content=combined)]}
|
|
218
|
+
{% endif %}
|
|
219
|
+
{% endif %}
|