agentx-kit 0.2.0__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.
Files changed (58) hide show
  1. agentx/__init__.py +55 -0
  2. agentx/cli.py +230 -0
  3. agentx/config.py +34 -0
  4. agentx/frameworks/__init__.py +5 -0
  5. agentx/frameworks/crewai_agent.py +52 -0
  6. agentx/frameworks/langchain_agent.py +43 -0
  7. agentx/guardrails.py +89 -0
  8. agentx/memory/__init__.py +4 -0
  9. agentx/memory/store.py +78 -0
  10. agentx/observability.py +103 -0
  11. agentx/prompts/__init__.py +8 -0
  12. agentx/prompts/templates.py +40 -0
  13. agentx/providers/__init__.py +15 -0
  14. agentx/providers/base.py +50 -0
  15. agentx/providers/factory.py +71 -0
  16. agentx/providers/registry.py +165 -0
  17. agentx/rag/__init__.py +8 -0
  18. agentx/rag/pipeline.py +121 -0
  19. agentx/reliability.py +112 -0
  20. agentx/scaffold/__init__.py +14 -0
  21. agentx/scaffold/generator.py +190 -0
  22. agentx/scaffold/prompts_store.py +99 -0
  23. agentx/scaffold/spec.py +85 -0
  24. agentx/scaffold/templates/Dockerfile.j2 +17 -0
  25. agentx/scaffold/templates/README.md.j2 +46 -0
  26. agentx/scaffold/templates/ci.yml.j2 +41 -0
  27. agentx/scaffold/templates/docker-compose.yml.j2 +9 -0
  28. agentx/scaffold/templates/dockerignore.j2 +11 -0
  29. agentx/scaffold/templates/env.example.j2 +12 -0
  30. agentx/scaffold/templates/evals/dataset.json.j2 +10 -0
  31. agentx/scaffold/templates/evals/run_evals.py.j2 +70 -0
  32. agentx/scaffold/templates/gitignore.j2 +8 -0
  33. agentx/scaffold/templates/mcp_servers.json.j2 +7 -0
  34. agentx/scaffold/templates/pkg/__init__.py.j2 +3 -0
  35. agentx/scaffold/templates/pkg/agents.py.j2 +77 -0
  36. agentx/scaffold/templates/pkg/config.py.j2 +25 -0
  37. agentx/scaffold/templates/pkg/guardrails.py.j2 +21 -0
  38. agentx/scaffold/templates/pkg/main.py.j2 +79 -0
  39. agentx/scaffold/templates/pkg/memory.py.j2 +17 -0
  40. agentx/scaffold/templates/pkg/observability.py.j2 +17 -0
  41. agentx/scaffold/templates/pkg/prompts.py.j2 +45 -0
  42. agentx/scaffold/templates/pkg/rag.py.j2 +37 -0
  43. agentx/scaffold/templates/pkg/server.py.j2 +85 -0
  44. agentx/scaffold/templates/pkg/tools.py.j2 +16 -0
  45. agentx/scaffold/templates/pyproject.toml.j2 +28 -0
  46. agentx/scaffold/templates/skills_seed.json.j2 +6 -0
  47. agentx/scaffold/wizard.py +125 -0
  48. agentx/skills/__init__.py +4 -0
  49. agentx/skills/registry.py +63 -0
  50. agentx/structured.py +37 -0
  51. agentx/tools/__init__.py +5 -0
  52. agentx/tools/builtin.py +45 -0
  53. agentx/tools/mcp.py +64 -0
  54. agentx_kit-0.2.0.dist-info/METADATA +289 -0
  55. agentx_kit-0.2.0.dist-info/RECORD +58 -0
  56. agentx_kit-0.2.0.dist-info/WHEEL +4 -0
  57. agentx_kit-0.2.0.dist-info/entry_points.txt +2 -0
  58. agentx_kit-0.2.0.dist-info/licenses/LICENSE +21 -0
agentx/reliability.py ADDED
@@ -0,0 +1,112 @@
1
+ """Reliability: provider fallbacks, retries, and token/cost budgets.
2
+
3
+ - ``build_resilient_chat`` wraps a primary model with retries and sequential
4
+ fallbacks to other providers/models (à la pydantic-ai's FallbackModel +
5
+ the 'circular fallback' pattern from production templates).
6
+ - ``UsageLimits`` + ``UsageTracker`` enforce per-run request/token/cost budgets
7
+ to stop runaway agent loops (à la pydantic-ai ``UsageLimits``).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from dataclasses import dataclass, field
13
+
14
+ from .providers import get_chat_model
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def build_resilient_chat(
20
+ provider: str | None = None,
21
+ model: str | None = None,
22
+ fallbacks: list[tuple[str, str]] | None = None,
23
+ retries: int = 2,
24
+ **kwargs,
25
+ ):
26
+ """Return a chat model with retry + ordered provider/model fallbacks.
27
+
28
+ ``fallbacks`` is a list of ``(provider, model)`` tried in order if the
29
+ primary fails. Built on LangChain's ``.with_retry()`` / ``.with_fallbacks()``.
30
+ """
31
+ primary = get_chat_model(provider, model, **kwargs)
32
+ if retries and retries > 1:
33
+ primary = primary.with_retry(stop_after_attempt=retries, wait_exponential_jitter=True)
34
+ if fallbacks:
35
+ alts = []
36
+ for fb_provider, fb_model in fallbacks:
37
+ try:
38
+ alt = get_chat_model(fb_provider, fb_model, **kwargs)
39
+ alts.append(alt.with_retry(stop_after_attempt=retries, wait_exponential_jitter=True))
40
+ except Exception as exc: # noqa: BLE001 - a missing extra shouldn't kill the chain
41
+ logger.warning("Skipping fallback %s/%s: %s", fb_provider, fb_model, exc)
42
+ if alts:
43
+ primary = primary.with_fallbacks(alts)
44
+ return primary
45
+
46
+
47
+ class UsageLimitExceeded(RuntimeError):
48
+ """Raised when a configured request/token/cost budget is exceeded."""
49
+
50
+
51
+ @dataclass
52
+ class UsageLimits:
53
+ max_requests: int | None = None
54
+ max_total_tokens: int | None = None
55
+ max_cost_usd: float | None = None
56
+ # USD per 1K tokens (total). Override per your provider/model pricing.
57
+ price_per_1k_tokens: float = 0.0
58
+
59
+
60
+ @dataclass
61
+ class UsageTracker:
62
+ """Accumulates usage and enforces ``UsageLimits``.
63
+
64
+ Call :meth:`record` with token counts after each model call (or attach
65
+ :meth:`as_callback` to LangChain). Raises ``UsageLimitExceeded`` on breach.
66
+ """
67
+
68
+ limits: UsageLimits = field(default_factory=UsageLimits)
69
+ requests: int = 0
70
+ total_tokens: int = 0
71
+
72
+ @property
73
+ def cost_usd(self) -> float:
74
+ return round(self.total_tokens / 1000.0 * self.limits.price_per_1k_tokens, 6)
75
+
76
+ def record(self, tokens: int = 0) -> None:
77
+ self.requests += 1
78
+ self.total_tokens += max(0, int(tokens))
79
+ self._enforce()
80
+
81
+ def _enforce(self) -> None:
82
+ lim = self.limits
83
+ if lim.max_requests is not None and self.requests > lim.max_requests:
84
+ raise UsageLimitExceeded(f"max_requests exceeded ({self.requests} > {lim.max_requests})")
85
+ if lim.max_total_tokens is not None and self.total_tokens > lim.max_total_tokens:
86
+ raise UsageLimitExceeded(f"max_total_tokens exceeded ({self.total_tokens} > {lim.max_total_tokens})")
87
+ if lim.max_cost_usd is not None and self.cost_usd > lim.max_cost_usd:
88
+ raise UsageLimitExceeded(f"max_cost_usd exceeded (${self.cost_usd} > ${lim.max_cost_usd})")
89
+
90
+ def as_callback(self):
91
+ """Return a LangChain callback handler that records token usage."""
92
+ from langchain_core.callbacks import BaseCallbackHandler
93
+
94
+ tracker = self
95
+
96
+ class _UsageCallback(BaseCallbackHandler):
97
+ def on_llm_end(self, response, **kwargs): # noqa: ANN001
98
+ tokens = 0
99
+ try:
100
+ usage = (response.llm_output or {}).get("token_usage") or {}
101
+ tokens = usage.get("total_tokens", 0)
102
+ if not tokens: # some providers attach usage to message metadata
103
+ for gen in getattr(response, "generations", []) or []:
104
+ for g in gen:
105
+ meta = getattr(getattr(g, "message", None), "usage_metadata", None)
106
+ if meta:
107
+ tokens += meta.get("total_tokens", 0)
108
+ except Exception: # noqa: BLE001
109
+ tokens = 0
110
+ tracker.record(tokens)
111
+
112
+ return _UsageCallback()
@@ -0,0 +1,14 @@
1
+ """Scaffolder: interactive wizard + project generator + prompt management."""
2
+ from .spec import AgentSpec, ProjectSpec
3
+ from .generator import GenerationResult, generate_project
4
+ from .wizard import run_wizard
5
+ from . import prompts_store
6
+
7
+ __all__ = [
8
+ "AgentSpec",
9
+ "ProjectSpec",
10
+ "GenerationResult",
11
+ "generate_project",
12
+ "run_wizard",
13
+ "prompts_store",
14
+ ]
@@ -0,0 +1,190 @@
1
+ """Project generator — renders templates into a new project and sets up uv.
2
+
3
+ ``generate_project(spec, target_dir)`` is pure/testable: it writes files and,
4
+ unless disabled, runs ``uv venv`` (creating ``.venv``) and optionally ``uv sync``.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import shutil
9
+ import subprocess
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+ from jinja2 import Environment, FileSystemLoader, StrictUndefined
14
+
15
+ from ..providers import get_spec
16
+ from . import prompts_store
17
+ from .spec import ProjectSpec
18
+
19
+ TEMPLATES_DIR = Path(__file__).parent / "templates"
20
+
21
+ # (template, output-relative-path, include?) — output paths use {pkg} placeholder.
22
+ _FILE_PLAN: list[tuple[str, str]] = [
23
+ ("pyproject.toml.j2", "pyproject.toml"),
24
+ ("README.md.j2", "README.md"),
25
+ ("env.example.j2", ".env.example"),
26
+ ("gitignore.j2", ".gitignore"),
27
+ ("pkg/__init__.py.j2", "src/{pkg}/__init__.py"),
28
+ ("pkg/config.py.j2", "src/{pkg}/config.py"),
29
+ ("pkg/prompts.py.j2", "src/{pkg}/prompts.py"),
30
+ ("pkg/agents.py.j2", "src/{pkg}/agents.py"),
31
+ ("pkg/main.py.j2", "src/{pkg}/main.py"),
32
+ ]
33
+
34
+
35
+ @dataclass
36
+ class GenerationResult:
37
+ target_dir: Path
38
+ files: list[Path]
39
+ venv_created: bool
40
+ synced: bool
41
+ messages: list[str]
42
+
43
+
44
+ def _extras(spec: ProjectSpec) -> list[str]:
45
+ """Compute the agentx extras the generated project needs."""
46
+ extras = {get_spec(spec.provider).extra}
47
+ extras.add("langgraph" if spec.framework == "langgraph" else "crewai")
48
+ if spec.use_rag:
49
+ extras.add("rag")
50
+ if spec.use_mcp:
51
+ extras.add("mcp")
52
+ if spec.observability:
53
+ extras.add("observability")
54
+ if spec.serve:
55
+ extras.add("server")
56
+ # Deterministic order for reproducible pyproject output.
57
+ order = ["langgraph", "crewai", "openai", "azure", "openrouter", "anthropic",
58
+ "google", "vertex", "bedrock", "groq", "ollama", "rag", "mcp",
59
+ "observability", "server"]
60
+ return [e for e in order if e in extras]
61
+
62
+
63
+ def _context(spec: ProjectSpec) -> dict:
64
+ provider_spec = get_spec(spec.provider)
65
+ return {
66
+ "spec": spec,
67
+ "pkg": spec.package,
68
+ "model": spec.model or provider_spec.default_model,
69
+ "provider_label": provider_spec.label,
70
+ "provider_env": list(provider_spec.env_vars),
71
+ "extras": _extras(spec),
72
+ "extras_str": ",".join(_extras(spec)),
73
+ }
74
+
75
+
76
+ def _env() -> Environment:
77
+ return Environment(
78
+ loader=FileSystemLoader(str(TEMPLATES_DIR)),
79
+ trim_blocks=True,
80
+ lstrip_blocks=True,
81
+ keep_trailing_newline=True,
82
+ undefined=StrictUndefined,
83
+ )
84
+
85
+
86
+ def _conditional_files(spec: ProjectSpec) -> list[tuple[str, str]]:
87
+ plan: list[tuple[str, str]] = []
88
+ if spec.use_rag:
89
+ plan.append(("pkg/rag.py.j2", "src/{pkg}/rag.py"))
90
+ if spec.needs_memory:
91
+ plan.append(("pkg/memory.py.j2", "src/{pkg}/memory.py"))
92
+ if spec.use_mcp:
93
+ plan.append(("pkg/tools.py.j2", "src/{pkg}/tools.py"))
94
+ plan.append(("mcp_servers.json.j2", "mcp_servers.json"))
95
+ if spec.use_skills:
96
+ plan.append(("skills_seed.json.j2", "data/skills/star-method.json"))
97
+ if spec.observability:
98
+ plan.append(("pkg/observability.py.j2", "src/{pkg}/observability.py"))
99
+ if spec.guardrails:
100
+ plan.append(("pkg/guardrails.py.j2", "src/{pkg}/guardrails.py"))
101
+ if spec.serve:
102
+ plan.append(("pkg/server.py.j2", "src/{pkg}/server.py"))
103
+ if spec.docker:
104
+ plan.append(("Dockerfile.j2", "Dockerfile"))
105
+ plan.append(("docker-compose.yml.j2", "docker-compose.yml"))
106
+ plan.append(("dockerignore.j2", ".dockerignore"))
107
+ if spec.ci:
108
+ plan.append(("ci.yml.j2", ".github/workflows/ci.yml"))
109
+ if spec.evals:
110
+ plan.append(("evals/run_evals.py.j2", "evals/run_evals.py"))
111
+ plan.append(("evals/dataset.json.j2", "evals/dataset.json"))
112
+ return plan
113
+
114
+
115
+ def _write_manifest(target: Path, spec: ProjectSpec) -> Path:
116
+ """Write agentx.json — a declarative summary of the generated project."""
117
+ import json
118
+
119
+ manifest = {
120
+ "name": spec.slug,
121
+ "framework": spec.framework,
122
+ "provider": spec.provider,
123
+ "model": spec.model or get_spec(spec.provider).default_model,
124
+ "python_version": ">=3.10,<3.14",
125
+ "agents": [a.name for a in spec.agents],
126
+ "features": {
127
+ "rag": spec.use_rag,
128
+ "memory": spec.memory,
129
+ "mcp": spec.use_mcp,
130
+ "skills": spec.use_skills,
131
+ "observability": spec.observability,
132
+ "guardrails": spec.guardrails,
133
+ "serve": spec.serve,
134
+ "docker": spec.docker,
135
+ "ci": spec.ci,
136
+ "evals": spec.evals,
137
+ },
138
+ "extras": _extras(spec),
139
+ "telemetry_opt_out": False,
140
+ }
141
+ path = target / "agentx.json"
142
+ path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
143
+ return path
144
+
145
+
146
+ def generate_project(spec: ProjectSpec, target_dir: str | Path, overwrite: bool = False) -> GenerationResult:
147
+ """Render the project for ``spec`` into ``target_dir``."""
148
+ target = Path(target_dir).expanduser().resolve()
149
+ if target.exists() and any(target.iterdir()) and not overwrite:
150
+ raise FileExistsError(f"Target directory '{target}' exists and is not empty. Use overwrite=True.")
151
+ target.mkdir(parents=True, exist_ok=True)
152
+
153
+ env = _env()
154
+ ctx = _context(spec)
155
+ written: list[Path] = []
156
+ messages: list[str] = []
157
+
158
+ for template_name, out_rel in _FILE_PLAN + _conditional_files(spec):
159
+ out_path = target / out_rel.format(pkg=spec.package)
160
+ out_path.parent.mkdir(parents=True, exist_ok=True)
161
+ rendered = env.get_template(template_name).render(**ctx)
162
+ out_path.write_text(rendered, encoding="utf-8")
163
+ written.append(out_path)
164
+
165
+ # The prompt source of truth — edited by hand or via `agentx prompt`.
166
+ written.append(prompts_store.write_prompts(target, spec))
167
+ # A single declarative manifest of the project (à la langgraph.json).
168
+ written.append(_write_manifest(target, spec))
169
+
170
+ venv_created = synced = False
171
+ uv = shutil.which("uv")
172
+ if spec.create_venv:
173
+ if not uv:
174
+ messages.append("`uv` not found on PATH — skipped .venv creation. Install uv: https://docs.astral.sh/uv/")
175
+ else:
176
+ try:
177
+ subprocess.run([uv, "venv"], cwd=target, check=True, capture_output=True, timeout=120)
178
+ venv_created = True
179
+ messages.append("Created .venv via `uv venv`.")
180
+ except Exception as exc: # noqa: BLE001
181
+ messages.append(f"`uv venv` failed: {exc!r}")
182
+ if venv_created and spec.run_sync:
183
+ try:
184
+ subprocess.run([uv, "sync"], cwd=target, check=True, timeout=900)
185
+ synced = True
186
+ messages.append("Installed dependencies via `uv sync`.")
187
+ except Exception as exc: # noqa: BLE001
188
+ messages.append(f"`uv sync` failed (run it manually): {exc!r}")
189
+
190
+ return GenerationResult(target, written, venv_created, synced, messages)
@@ -0,0 +1,99 @@
1
+ """Read/write helpers for a generated project's ``prompts.json``.
2
+
3
+ The generated project is *prompt-file driven*: ``prompts.json`` is the source of
4
+ truth for which agents exist and what their system prompts are. Editing it (by
5
+ hand or via ``agentx prompt``) changes the running project — no code edits needed.
6
+
7
+ Schema::
8
+
9
+ {
10
+ "with_rag": false,
11
+ "agents": {
12
+ "<name>": {"role": "...", "goal": "...", "system_prompt": "..."}
13
+ }
14
+ }
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from pathlib import Path
20
+
21
+ from .spec import ProjectSpec, to_snake
22
+
23
+ FILENAME = "prompts.json"
24
+
25
+
26
+ def build_prompts_data(spec: ProjectSpec) -> dict:
27
+ """Build the prompts.json payload from a ProjectSpec."""
28
+ return {
29
+ "with_rag": spec.use_rag,
30
+ "agents": {
31
+ a.name: {"role": a.role, "goal": a.goal, "system_prompt": a.system_prompt}
32
+ for a in spec.agents
33
+ },
34
+ }
35
+
36
+
37
+ def write_prompts(target_dir: str | Path, spec: ProjectSpec) -> Path:
38
+ path = Path(target_dir) / FILENAME
39
+ path.write_text(json.dumps(build_prompts_data(spec), indent=2) + "\n", encoding="utf-8")
40
+ return path
41
+
42
+
43
+ def find_prompts_file(start: str | Path | None = None) -> Path | None:
44
+ """Walk up from ``start`` (or cwd) to find a project's prompts.json."""
45
+ base = Path(start or Path.cwd()).resolve()
46
+ for d in [base, *base.parents]:
47
+ candidate = d / FILENAME
48
+ if candidate.exists():
49
+ return candidate
50
+ return None
51
+
52
+
53
+ def load(path: str | Path) -> dict:
54
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
55
+ data.setdefault("agents", {})
56
+ data.setdefault("with_rag", False)
57
+ return data
58
+
59
+
60
+ def save(path: str | Path, data: dict) -> None:
61
+ Path(path).write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
62
+
63
+
64
+ def set_prompt(path: str | Path, name: str, text: str) -> dict:
65
+ """Set/replace an existing agent's system prompt."""
66
+ data = load(path)
67
+ key = to_snake(name)
68
+ if key not in data["agents"]:
69
+ raise KeyError(f"No agent named '{key}'. Use `add` to create it.")
70
+ data["agents"][key]["system_prompt"] = text
71
+ save(path, data)
72
+ return data
73
+
74
+
75
+ def add_agent(path: str | Path, name: str, role: str = "", goal: str = "", text: str = "") -> dict:
76
+ """Add a new agent. The project picks it up automatically on next run."""
77
+ data = load(path)
78
+ key = to_snake(name)
79
+ if key in data["agents"]:
80
+ raise KeyError(f"Agent '{key}' already exists. Use `set` to change its prompt.")
81
+ data["agents"][key] = {
82
+ "role": role or f"{name} agent",
83
+ "goal": goal or "Help the user accomplish their task accurately.",
84
+ "system_prompt": text,
85
+ }
86
+ save(path, data)
87
+ return data
88
+
89
+
90
+ def remove_agent(path: str | Path, name: str) -> dict:
91
+ data = load(path)
92
+ key = to_snake(name)
93
+ if key not in data["agents"]:
94
+ raise KeyError(f"No agent named '{key}'.")
95
+ if len(data["agents"]) == 1:
96
+ raise ValueError("Cannot remove the last remaining agent.")
97
+ del data["agents"][key]
98
+ save(path, data)
99
+ return data
@@ -0,0 +1,85 @@
1
+ """``ProjectSpec`` — the single source of truth for a generation run.
2
+
3
+ Both the interactive wizard and the programmatic API produce a ``ProjectSpec``;
4
+ the generator consumes it. Keeping this validated and standalone makes the whole
5
+ scaffolder testable without a TTY.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from typing import Literal
11
+
12
+ from pydantic import BaseModel, Field, field_validator
13
+
14
+ Framework = Literal["langgraph", "crewai"]
15
+ MemoryMode = Literal["none", "short", "long", "both"]
16
+ PromptStyle = Literal["default", "custom"]
17
+
18
+
19
+ def to_snake(name: str) -> str:
20
+ s = re.sub(r"[^0-9a-zA-Z]+", "_", name.strip().lower()).strip("_")
21
+ return s or "app"
22
+
23
+
24
+ class AgentSpec(BaseModel):
25
+ name: str = "assistant"
26
+ role: str = "Helpful Assistant"
27
+ goal: str = "Help the user accomplish their task accurately."
28
+ # Optional explicit system prompt. Blank → derived from role/goal at runtime.
29
+ system_prompt: str = ""
30
+
31
+ @field_validator("name")
32
+ @classmethod
33
+ def _slug(cls, v: str) -> str:
34
+ return to_snake(v)
35
+
36
+
37
+ class ProjectSpec(BaseModel):
38
+ name: str = Field(..., description="Project name (directory + package base).")
39
+ framework: Framework = "langgraph"
40
+ provider: str = "openai"
41
+ model: str = "" # blank → provider default
42
+ agents: list[AgentSpec] = Field(default_factory=lambda: [AgentSpec()])
43
+ use_rag: bool = False
44
+ memory: MemoryMode = "none"
45
+ use_mcp: bool = False
46
+ use_skills: bool = False
47
+ prompt_style: PromptStyle = "default"
48
+ # ----- enterprise features -----
49
+ observability: bool = False # OpenTelemetry / Langfuse tracing wiring
50
+ guardrails: bool = False # input/output guardrails module
51
+ serve: bool = False # FastAPI server (REST + SSE streaming)
52
+ docker: bool = False # Dockerfile + docker-compose.yml
53
+ ci: bool = False # GitHub Actions (lint + test [+ eval])
54
+ evals: bool = False # LLM-as-judge eval harness (+ CI gate)
55
+ create_venv: bool = True
56
+ run_sync: bool = False
57
+ # When set, generated pyproject depends on agentx from this local path
58
+ # (editable) instead of PyPI — used for local dev/testing.
59
+ agentx_local_path: str | None = None
60
+
61
+ def enable_enterprise(self) -> "ProjectSpec":
62
+ """Turn on the full enterprise feature set in one call."""
63
+ self.observability = self.guardrails = self.serve = True
64
+ self.docker = self.ci = self.evals = True
65
+ return self
66
+
67
+ @property
68
+ def package(self) -> str:
69
+ return to_snake(self.name)
70
+
71
+ @property
72
+ def slug(self) -> str:
73
+ return re.sub(r"[^0-9a-zA-Z]+", "-", self.name.strip().lower()).strip("-") or "app"
74
+
75
+ @property
76
+ def needs_memory(self) -> bool:
77
+ return self.memory != "none"
78
+
79
+ @property
80
+ def use_short_memory(self) -> bool:
81
+ return self.memory in ("short", "both")
82
+
83
+ @property
84
+ def use_long_memory(self) -> bool:
85
+ return self.memory in ("long", "both")
@@ -0,0 +1,17 @@
1
+ FROM python:3.12-slim
2
+
3
+ ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
4
+ WORKDIR /app
5
+
6
+ # uv for fast, reproducible installs
7
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
8
+
9
+ COPY . .
10
+ RUN uv pip install --system .
11
+
12
+ {% if spec.serve %}
13
+ EXPOSE 8000
14
+ CMD ["uvicorn", "{{ pkg }}.server:app", "--host", "0.0.0.0", "--port", "8000"]
15
+ {% else %}
16
+ CMD ["python", "-m", "{{ pkg }}.main"]
17
+ {% endif %}
@@ -0,0 +1,46 @@
1
+ # {{ spec.slug }}
2
+
3
+ Agentic app generated by **AgentX**.
4
+
5
+ - **Framework:** {{ spec.framework }}
6
+ - **Provider:** {{ provider_label }} (`{{ model }}`)
7
+ - **Agents:** {% for a in spec.agents %}`{{ a.name }}`{% if not loop.last %}, {% endif %}{% endfor %}
8
+
9
+ - **RAG:** {{ "yes" if spec.use_rag else "no" }}
10
+ - **Memory:** {{ spec.memory }}
11
+ - **MCP tools:** {{ "yes" if spec.use_mcp else "no" }}
12
+ - **Skills:** {{ "yes" if spec.use_skills else "no" }}
13
+
14
+ ## Setup
15
+ ```bash
16
+ uv sync # install dependencies into .venv
17
+ cp .env.example .env # then fill in your credentials
18
+ ```
19
+
20
+ ## Run
21
+ ```bash
22
+ uv run {{ spec.slug }}
23
+ # or:
24
+ uv run python -m {{ pkg }}.main
25
+ ```
26
+
27
+ ## Layout
28
+ ```
29
+ src/{{ pkg }}/
30
+ ├── config.py # provider/model selection
31
+ ├── prompts.py # per-agent system prompts
32
+ ├── agents.py # agent (and crew) definitions
33
+ {% if spec.use_rag %}├── rag.py # knowledge base / retriever tool
34
+ {% endif %}
35
+ {% if spec.needs_memory %}├── memory.py # conversation / long-term memory
36
+ {% endif %}
37
+ {% if spec.use_mcp %}├── tools.py # MCP tool loading
38
+ {% endif %}
39
+ └── main.py # entry point
40
+ ```
41
+ {% if spec.use_rag %}
42
+ Add `.txt`/`.md` files under `knowledge/` to populate the RAG index.
43
+ {% endif %}
44
+ {% if spec.use_mcp %}
45
+ Edit `mcp_servers.json` to configure your MCP servers.
46
+ {% endif %}
@@ -0,0 +1,41 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - name: Install
17
+ run: uv pip install --system -e . pytest ruff
18
+ - name: Lint
19
+ run: ruff check . || true
20
+ - name: Compile
21
+ run: python -m compileall src
22
+ - name: Tests
23
+ run: pytest -q || echo "no tests yet"
24
+ {% if spec.evals %}
25
+
26
+ evals:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: astral-sh/setup-uv@v5
31
+ with:
32
+ python-version: "3.12"
33
+ - name: Install
34
+ run: uv pip install --system -e .
35
+ - name: Run eval gate
36
+ 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
+
40
+ run: python evals/run_evals.py
41
+ {% endif %}
@@ -0,0 +1,9 @@
1
+ services:
2
+ {{ spec.slug }}:
3
+ build: .
4
+ env_file: .env
5
+ {% if spec.serve %}
6
+ ports:
7
+ - "8000:8000"
8
+ {% endif %}
9
+ restart: unless-stopped
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .git/
5
+ .github/
6
+ .env
7
+ data/
8
+ .chroma/
9
+ tests/
10
+ evals/
11
+ .pytest_cache/
@@ -0,0 +1,12 @@
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
+ {% endif %}
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "input": "Say hello and briefly state what you can help with.",
4
+ "criteria": "Greets the user and clearly explains the agent's purpose/capabilities."
5
+ },
6
+ {
7
+ "input": "What are your main capabilities?",
8
+ "criteria": "Lists relevant, concrete capabilities without hallucinating unavailable features."
9
+ }
10
+ ]