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.
Files changed (102) hide show
  1. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/PKG-INFO +2 -2
  2. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/pyproject.toml +2 -2
  3. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/__init__.py +1 -1
  4. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/cache.py +6 -3
  5. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/cli.py +3 -1
  6. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/generator.py +17 -0
  7. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/spec.py +12 -0
  8. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/ci.yml.j2 +9 -3
  9. agentx_kit-0.7.0/src/agentx/scaffold/templates/env.example.j2 +25 -0
  10. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/mcp_servers.json.j2 +1 -1
  11. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/crew.py.j2 +7 -2
  12. agentx_kit-0.7.0/src/agentx/scaffold/templates/pkg/graph.py.j2 +172 -0
  13. agentx_kit-0.7.0/src/agentx/scaffold/templates/pkg/memory.py.j2 +25 -0
  14. agentx_kit-0.7.0/src/agentx/scaffold/templates/pkg/nodes.py.j2 +219 -0
  15. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/rag.py.j2 +15 -4
  16. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/server.py.j2 +34 -9
  17. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/state.py.j2 +2 -6
  18. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/wizard.py +26 -0
  19. agentx_kit-0.6.0/src/agentx/scaffold/templates/env.example.j2 +0 -12
  20. agentx_kit-0.6.0/src/agentx/scaffold/templates/pkg/graph.py.j2 +0 -69
  21. agentx_kit-0.6.0/src/agentx/scaffold/templates/pkg/memory.py.j2 +0 -17
  22. agentx_kit-0.6.0/src/agentx/scaffold/templates/pkg/nodes.py.j2 +0 -76
  23. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/.agentx/llm_cache.sqlite +0 -0
  24. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/.claude-plugin/marketplace.json +0 -0
  25. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/.github/workflows/publish.yml +0 -0
  26. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/.gitignore +0 -0
  27. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/DESIGN.md +0 -0
  28. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/LICENSE +0 -0
  29. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/README.md +0 -0
  30. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/RESEARCH.md +0 -0
  31. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/examples/README.md +0 -0
  32. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/examples/demo_local.sh +0 -0
  33. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/examples/demo_mcp.py +0 -0
  34. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/claude-plugin/.claude-plugin/plugin.json +0 -0
  35. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/claude-plugin/.mcp.json +0 -0
  36. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/claude-plugin/README.md +0 -0
  37. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/claude-plugin/commands/new-agent.md +0 -0
  38. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/vscode/.vscodeignore +0 -0
  39. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/vscode/README.md +0 -0
  40. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/vscode/extension.js +0 -0
  41. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/integrations/vscode/package.json +0 -0
  42. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/config.py +0 -0
  43. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/connector/__init__.py +0 -0
  44. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/connector/build.py +0 -0
  45. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/connector/recommend.py +0 -0
  46. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/connector/server.py +0 -0
  47. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/dashboard/__init__.py +0 -0
  48. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/dashboard/app.py +0 -0
  49. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/frameworks/__init__.py +0 -0
  50. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/frameworks/crewai_agent.py +0 -0
  51. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/frameworks/langchain_agent.py +0 -0
  52. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/guardrails.py +0 -0
  53. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/insights/__init__.py +0 -0
  54. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/insights/analyze.py +0 -0
  55. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/insights/log.py +0 -0
  56. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/insights/optimize.py +0 -0
  57. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/insights/tokens.py +0 -0
  58. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/memory/__init__.py +0 -0
  59. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/memory/store.py +0 -0
  60. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/observability.py +0 -0
  61. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/prompts/__init__.py +0 -0
  62. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/prompts/templates.py +0 -0
  63. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/providers/__init__.py +0 -0
  64. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/providers/base.py +0 -0
  65. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/providers/factory.py +0 -0
  66. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/providers/registry.py +0 -0
  67. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/rag/__init__.py +0 -0
  68. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/rag/pipeline.py +0 -0
  69. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/reliability.py +0 -0
  70. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/__init__.py +0 -0
  71. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/prompts_store.py +0 -0
  72. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/Dockerfile.j2 +0 -0
  73. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/README.md.j2 +0 -0
  74. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/docker-compose.yml.j2 +0 -0
  75. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/dockerignore.j2 +0 -0
  76. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/evals/dataset.json.j2 +0 -0
  77. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/evals/run_evals.py.j2 +0 -0
  78. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/gitignore.j2 +0 -0
  79. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/__init__.py.j2 +0 -0
  80. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/agents.py.j2 +0 -0
  81. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/config.py.j2 +0 -0
  82. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/guardrails.py.j2 +0 -0
  83. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/main.py.j2 +0 -0
  84. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/observability.py.j2 +0 -0
  85. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/prompts.py.j2 +0 -0
  86. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/tasks.py.j2 +0 -0
  87. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pkg/tools.py.j2 +0 -0
  88. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/pyproject.toml.j2 +0 -0
  89. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/scaffold/templates/skills_seed.json.j2 +0 -0
  90. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/skills/__init__.py +0 -0
  91. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/skills/registry.py +0 -0
  92. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/structured.py +0 -0
  93. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/tools/__init__.py +0 -0
  94. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/tools/builtin.py +0 -0
  95. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/src/agentx/tools/mcp.py +0 -0
  96. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_cache.py +0 -0
  97. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_connector.py +0 -0
  98. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_enterprise.py +0 -0
  99. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_insights.py +0 -0
  100. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_prompts.py +0 -0
  101. {agentx_kit-0.6.0 → agentx_kit-0.7.0}/tests/test_providers.py +0 -0
  102. {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.6.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: AgentX
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.6.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 = "AgentX" }]
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",
@@ -16,7 +16,7 @@ is enough to get started.
16
16
  """
17
17
  from __future__ import annotations
18
18
 
19
- __version__ = "0.6.0"
19
+ __version__ = "0.7.0"
20
20
 
21
21
  from .providers import ( # noqa: E402
22
22
  ProviderSpec,
@@ -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`` (and ``aclear``) so it can be passed to
46
- ``langchain_core.globals.set_llm_cache``.
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, use_rag=rag, memory=memory, use_mcp=mcp, use_skills=skills,
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
- # Add your provider key as a repo secret to enable the gate.
38
- OPENAI_API_KEY: {% raw %}${{ secrets.OPENAI_API_KEY }}{% endraw %}
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 %}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "filesystem": {
3
3
  "command": "npx",
4
- "args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
4
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "./knowledge"],
5
5
  "transport": "stdio"
6
6
  }
7
7
  }
@@ -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 %}