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
@@ -0,0 +1,70 @@
1
+ """LLM-as-judge eval harness for {{ spec.slug }}.
2
+
3
+ Runs each case in evals/dataset.json through the agent, scores the output with a
4
+ judge LLM (0-1) against the case's criteria, and exits non-zero if the mean
5
+ score is below THRESHOLD — so it can gate CI.
6
+
7
+ python evals/run_evals.py
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from dotenv import load_dotenv
17
+
18
+ from agentx import get_chat_model
19
+ {% if spec.framework == 'langgraph' %}
20
+ from agentx.frameworks import run_agent
21
+ from {{ pkg }}.agents import build_agents
22
+ {% else %}
23
+ from {{ pkg }}.agents import build_project_crew
24
+ {% endif %}
25
+
26
+ THRESHOLD = 0.6
27
+ DATASET = Path(__file__).parent / "dataset.json"
28
+
29
+ JUDGE_PROMPT = (
30
+ "You are a strict evaluator. Given a user INPUT, the agent's OUTPUT, and the "
31
+ "success CRITERIA, reply with ONLY a number between 0 and 1 (one decimal) for "
32
+ "how well OUTPUT satisfies CRITERIA.\n\n"
33
+ "INPUT: {input}\nOUTPUT: {output}\nCRITERIA: {criteria}\nSCORE:"
34
+ )
35
+
36
+
37
+ def _agent_reply(message: str) -> str:
38
+ {% if spec.framework == 'langgraph' %}
39
+ agent = next(iter(build_agents().values()))
40
+ return run_agent(agent, message)
41
+ {% else %}
42
+ return str(build_project_crew(message).kickoff())
43
+ {% endif %}
44
+
45
+
46
+ def _score(judge, case: dict, output: str) -> float:
47
+ text = judge.invoke(
48
+ JUDGE_PROMPT.format(input=case["input"], output=output, criteria=case.get("criteria", ""))
49
+ ).content
50
+ match = re.search(r"(\d(?:\.\d+)?)", text or "")
51
+ return max(0.0, min(1.0, float(match.group(1)))) if match else 0.0
52
+
53
+
54
+ def main() -> int:
55
+ load_dotenv()
56
+ cases = json.loads(DATASET.read_text(encoding="utf-8"))
57
+ judge = get_chat_model()
58
+ scores = []
59
+ for case in cases:
60
+ output = _agent_reply(case["input"])
61
+ score = _score(judge, case, output)
62
+ scores.append(score)
63
+ print(f"[{score:.1f}] {case['input'][:60]}")
64
+ mean = sum(scores) / len(scores) if scores else 0.0
65
+ print(f"\nMean score: {mean:.2f} (threshold {THRESHOLD})")
66
+ return 0 if mean >= THRESHOLD else 1
67
+
68
+
69
+ if __name__ == "__main__":
70
+ sys.exit(main())
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .env
5
+ *.egg-info/
6
+ .pytest_cache/
7
+ .chroma/
8
+ data/
@@ -0,0 +1,7 @@
1
+ {
2
+ "filesystem": {
3
+ "command": "npx",
4
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
5
+ "transport": "stdio"
6
+ }
7
+ }
@@ -0,0 +1,3 @@
1
+ """{{ spec.slug }} — generated by AgentX."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,77 @@
1
+ {% if spec.framework == 'langgraph' %}
2
+ """Agent definitions (LangGraph) — built dynamically from prompts.json."""
3
+ from __future__ import annotations
4
+
5
+ from agentx.frameworks import build_react_agent
6
+ from agentx.tools import make_web_search_tool
7
+ {% if spec.use_skills %}from agentx.skills import get_skill_registry
8
+ {% endif %}
9
+ from .config import MODEL, PROVIDER
10
+ from .prompts import resolved_system_prompts
11
+ {% if spec.use_rag %}from .rag import get_knowledge_tool
12
+ {% endif %}
13
+ {% if spec.use_mcp %}from .tools import get_mcp_tools
14
+ {% endif %}
15
+
16
+
17
+ def _build_tools():
18
+ tools = [make_web_search_tool()]
19
+ {% if spec.use_rag %}
20
+ tools.append(get_knowledge_tool())
21
+ {% endif %}
22
+ {% if spec.use_mcp %}
23
+ tools.extend(get_mcp_tools())
24
+ {% endif %}
25
+ return tools
26
+
27
+
28
+ def build_agents():
29
+ """Return {name: compiled LangGraph agent} — one per entry in prompts.json."""
30
+ tools = _build_tools()
31
+ {% if spec.use_skills %}
32
+ skills = get_skill_registry("data/skills").combined_instructions()
33
+ {% else %}
34
+ skills = ""
35
+ {% endif %}
36
+ agents = {}
37
+ for name, prompt in resolved_system_prompts().items():
38
+ if skills:
39
+ prompt = prompt + "\nApply these skills:\n" + skills
40
+ agents[name] = build_react_agent(
41
+ provider=PROVIDER, model=MODEL, tools=tools, system_prompt=prompt,
42
+ )
43
+ return agents
44
+ {% else %}
45
+ """Agent + crew definitions (CrewAI) — built dynamically from prompts.json."""
46
+ from __future__ import annotations
47
+
48
+ from crewai import Task
49
+
50
+ from agentx.frameworks import build_crew, build_crewai_agent
51
+ from .config import MODEL, PROVIDER
52
+ from .prompts import agents_meta, resolved_system_prompts
53
+
54
+
55
+ def build_agents():
56
+ """Return {name: CrewAI Agent} — one per entry in prompts.json."""
57
+ prompts = resolved_system_prompts()
58
+ agents = {}
59
+ for name, meta in agents_meta().items():
60
+ agents[name] = build_crewai_agent(
61
+ role=meta.get("role", name),
62
+ goal=meta.get("goal", "Help the user accomplish their task accurately."),
63
+ backstory=prompts.get(name, ""),
64
+ provider=PROVIDER, model=MODEL,
65
+ )
66
+ return agents
67
+
68
+
69
+ def build_project_crew(user_input: str):
70
+ """Assemble a sequential crew where each agent works the user's request."""
71
+ agents = build_agents()
72
+ tasks = [
73
+ Task(description=user_input, expected_output="A helpful, complete response.", agent=agent)
74
+ for agent in agents.values()
75
+ ]
76
+ return build_crew(list(agents.values()), tasks)
77
+ {% endif %}
@@ -0,0 +1,25 @@
1
+ """Typed configuration for {{ spec.slug }} (12-factor, pydantic-settings).
2
+
3
+ Reads from environment / .env. Override at runtime, e.g. AGENTX_MODEL=gpt-4o.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from pydantic_settings import BaseSettings, SettingsConfigDict
8
+
9
+
10
+ class Settings(BaseSettings):
11
+ model_config = SettingsConfigDict(env_prefix="AGENTX_", env_file=".env", extra="ignore")
12
+
13
+ provider: str = "{{ spec.provider }}"
14
+ model: str = "{{ model }}"
15
+ temperature: float = 0.3
16
+ {% if spec.observability %}
17
+ telemetry: bool = True
18
+ {% endif %}
19
+
20
+
21
+ settings = Settings()
22
+
23
+ # Convenience aliases used across the project.
24
+ PROVIDER = settings.provider
25
+ MODEL = settings.model
@@ -0,0 +1,21 @@
1
+ """Guardrails for {{ spec.slug }} — input/output validation + PII redaction.
2
+
3
+ Customise BANNED_WORDS or compose your own guards. For jailbreak/moderation,
4
+ wire in Guardrails-AI or NeMo Guardrails here.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from agentx.guardrails import GuardResult, apply_guards, default_input_guards, default_output_guards
9
+
10
+ # Words/phrases to reject in user input (case-insensitive).
11
+ BANNED_WORDS: list[str] = []
12
+
13
+
14
+ def guard_input(text: str) -> GuardResult:
15
+ """Validate + lightly sanitise user input before it reaches the model."""
16
+ return apply_guards(text, default_input_guards(banned=BANNED_WORDS))
17
+
18
+
19
+ def guard_output(text: str) -> GuardResult:
20
+ """Redact PII and cap length on model output before returning to the user."""
21
+ return apply_guards(text, default_output_guards(redact=True))
@@ -0,0 +1,79 @@
1
+ """Entry point for {{ spec.slug }}."""
2
+ from __future__ import annotations
3
+
4
+ from dotenv import load_dotenv
5
+ {% if spec.use_short_memory %}from .memory import session_memory
6
+ {% endif %}
7
+ {% if spec.observability %}from .observability import init_observability
8
+ {% endif %}
9
+ {% if spec.guardrails %}from .guardrails import guard_input, guard_output
10
+ {% endif %}
11
+ {% if spec.framework == 'langgraph' %}
12
+ from agentx.frameworks import run_agent
13
+ from .agents import build_agents
14
+
15
+
16
+ def main() -> None:
17
+ load_dotenv()
18
+ {% if spec.observability %}
19
+ init_observability()
20
+ {% endif %}
21
+ agents = build_agents()
22
+ agent_name, agent = next(iter(agents.items()))
23
+ print("🧬 {{ spec.slug }} ready. Type 'quit' to exit.\n")
24
+ while True:
25
+ try:
26
+ user = input("you > ").strip()
27
+ except (EOFError, KeyboardInterrupt):
28
+ break
29
+ if user.lower() in {"quit", "exit"}:
30
+ break
31
+ if not user:
32
+ continue
33
+ {% if spec.guardrails %}
34
+ user = guard_input(user).text
35
+ {% endif %}
36
+ {% if spec.use_short_memory %}
37
+ session_memory.add_user(user)
38
+ {% endif %}
39
+ reply = run_agent(agent, user)
40
+ {% if spec.guardrails %}
41
+ reply = guard_output(reply).text
42
+ {% endif %}
43
+ {% if spec.use_short_memory %}
44
+ session_memory.add_ai(reply)
45
+ {% endif %}
46
+ print(f"\n{agent_name} > {reply}\n")
47
+ {% else %}
48
+ from .agents import build_project_crew
49
+
50
+
51
+ def main() -> None:
52
+ load_dotenv()
53
+ {% if spec.observability %}
54
+ init_observability()
55
+ {% endif %}
56
+ print("🧬 {{ spec.slug }} (CrewAI). Type 'quit' to exit.\n")
57
+ while True:
58
+ try:
59
+ user = input("you > ").strip()
60
+ except (EOFError, KeyboardInterrupt):
61
+ break
62
+ if user.lower() in {"quit", "exit"}:
63
+ break
64
+ if not user:
65
+ continue
66
+ {% if spec.guardrails %}
67
+ user = guard_input(user).text
68
+ {% endif %}
69
+ crew = build_project_crew(user)
70
+ result = str(crew.kickoff())
71
+ {% if spec.guardrails %}
72
+ result = guard_output(result).text
73
+ {% endif %}
74
+ print(f"\ncrew > {result}\n")
75
+ {% endif %}
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()
@@ -0,0 +1,17 @@
1
+ """Memory for {{ spec.slug }} (mode: {{ spec.memory }})."""
2
+ from __future__ import annotations
3
+
4
+ {% if spec.use_short_memory %}
5
+ from agentx.memory import ConversationMemory
6
+
7
+ # Short-term, in-process rolling window of recent turns.
8
+ session_memory = ConversationMemory(max_turns=12)
9
+ {% endif %}
10
+ {% if spec.use_long_memory %}
11
+ from agentx.memory import LongTermMemory
12
+
13
+
14
+ def long_term(session_id: str = "default") -> LongTermMemory:
15
+ """Persistent per-session memory (JSONL under data/memory/)."""
16
+ return LongTermMemory(f"data/memory/{session_id}.jsonl")
17
+ {% endif %}
@@ -0,0 +1,17 @@
1
+ """Observability wiring for {{ spec.slug }}.
2
+
3
+ OpenTelemetry GenAI tracing (+ optional Langfuse). Configure via env:
4
+ OTEL_EXPORTER_OTLP_ENDPOINT, LANGFUSE_PUBLIC_KEY/LANGFUSE_SECRET_KEY.
5
+ Disable entirely with AGENTX_TELEMETRY=false.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from agentx.observability import get_callbacks, setup_tracing
10
+
11
+
12
+ def init_observability(service_name: str = "{{ spec.slug }}") -> bool:
13
+ """Instrument tracing once at startup. Safe no-op if extras absent/disabled."""
14
+ return setup_tracing(service_name)
15
+
16
+
17
+ __all__ = ["init_observability", "get_callbacks"]
@@ -0,0 +1,45 @@
1
+ """Prompt loading for {{ spec.slug }}.
2
+
3
+ Prompts live in `prompts.json` at the project root — the single source of truth
4
+ for which agents exist and their system prompts. Edit that file directly, or use
5
+ the CLI:
6
+
7
+ agentx prompt list
8
+ agentx prompt set <agent> --text "You are ..."
9
+ agentx prompt add <agent> --role "..." --goal "..." --text "..."
10
+
11
+ A blank `system_prompt` is auto-derived from the agent's role + goal.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from pathlib import Path
17
+
18
+ from agentx.prompts import render_agent_system
19
+
20
+ PROMPTS_FILE = Path(__file__).resolve().parents[2] / "prompts.json"
21
+
22
+
23
+ def load_prompts() -> dict:
24
+ if PROMPTS_FILE.exists():
25
+ return json.loads(PROMPTS_FILE.read_text(encoding="utf-8"))
26
+ return {"with_rag": {{ spec.use_rag }}, "agents": {}}
27
+
28
+
29
+ def resolved_system_prompts() -> dict[str, str]:
30
+ """Return {agent_name: system_prompt}, deriving blanks from role/goal."""
31
+ data = load_prompts()
32
+ with_rag = bool(data.get("with_rag", False))
33
+ out: dict[str, str] = {}
34
+ for name, meta in data.get("agents", {}).items():
35
+ explicit = (meta.get("system_prompt") or "").strip()
36
+ out[name] = explicit or render_agent_system(
37
+ role=meta.get("role", name),
38
+ goal=meta.get("goal", "Help the user accomplish their task accurately."),
39
+ with_rag=with_rag,
40
+ )
41
+ return out
42
+
43
+
44
+ def agents_meta() -> dict[str, dict]:
45
+ return load_prompts().get("agents", {})
@@ -0,0 +1,37 @@
1
+ """RAG knowledge base for {{ spec.slug }}.
2
+
3
+ Drop `.txt` / `.md` files into the project's `knowledge/` folder; they are
4
+ chunked and indexed on first use. Uses Chroma when installed, else a built-in
5
+ keyword retriever.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ from agentx.rag import build_index_from_texts, make_retriever_tool
12
+
13
+ _KNOWLEDGE_DIR = Path(__file__).resolve().parents[2] / "knowledge"
14
+ _index = None
15
+
16
+
17
+ def _load_texts() -> list[str]:
18
+ texts: list[str] = []
19
+ if _KNOWLEDGE_DIR.exists():
20
+ for fp in _KNOWLEDGE_DIR.glob("**/*"):
21
+ if fp.suffix.lower() in {".txt", ".md"}:
22
+ texts.append(fp.read_text(encoding="utf-8"))
23
+ return texts or [
24
+ "Placeholder knowledge base. Add .txt or .md files under the 'knowledge/' folder."
25
+ ]
26
+
27
+
28
+ def get_index():
29
+ global _index
30
+ if _index is None:
31
+ _index = build_index_from_texts(_load_texts(), persist_dir=".chroma")
32
+ return _index
33
+
34
+
35
+ def get_knowledge_tool():
36
+ """A LangChain tool that searches the knowledge base."""
37
+ return make_retriever_tool(get_index())
@@ -0,0 +1,85 @@
1
+ """FastAPI server for {{ spec.slug }} — REST + SSE streaming.
2
+
3
+ Run: uv run uvicorn {{ pkg }}.server:app --reload --port 8000
4
+ Endpoints: GET /health, POST /chat, GET /chat/stream?message=...
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+
10
+ from dotenv import load_dotenv
11
+ from fastapi import FastAPI
12
+ from fastapi.responses import StreamingResponse
13
+ from pydantic import BaseModel
14
+
15
+ load_dotenv()
16
+ {% if spec.observability %}
17
+ from .observability import init_observability
18
+
19
+ init_observability()
20
+ {% endif %}
21
+ {% if spec.guardrails %}
22
+ from .guardrails import guard_input, guard_output
23
+ {% endif %}
24
+ {% if spec.framework == 'langgraph' %}
25
+ from agentx.frameworks import run_agent
26
+ from .agents import build_agents
27
+
28
+ _agent_cache = None
29
+
30
+
31
+ def _reply(message: str) -> str:
32
+ global _agent_cache
33
+ if _agent_cache is None:
34
+ _agent_cache = next(iter(build_agents().values()))
35
+ return run_agent(_agent_cache, message)
36
+ {% else %}
37
+ from .agents import build_project_crew
38
+
39
+
40
+ def _reply(message: str) -> str:
41
+ return str(build_project_crew(message).kickoff())
42
+ {% endif %}
43
+
44
+ app = FastAPI(title="{{ spec.slug }}", version="0.1.0")
45
+
46
+
47
+ class ChatIn(BaseModel):
48
+ message: str
49
+
50
+
51
+ @app.get("/health")
52
+ def health() -> dict:
53
+ return {"status": "ok", "service": "{{ spec.slug }}"}
54
+
55
+
56
+ @app.post("/chat")
57
+ def chat(body: ChatIn) -> dict:
58
+ message = body.message
59
+ {% if spec.guardrails %}
60
+ message = guard_input(message).text
61
+ {% endif %}
62
+ reply = _reply(message)
63
+ {% if spec.guardrails %}
64
+ reply = guard_output(reply).text
65
+ {% endif %}
66
+ return {"reply": reply}
67
+
68
+
69
+ @app.get("/chat/stream")
70
+ def chat_stream(message: str) -> StreamingResponse:
71
+ """Server-Sent Events stream of the reply (token-chunked)."""
72
+ {% if spec.guardrails %}
73
+ message = guard_input(message).text
74
+ {% endif %}
75
+
76
+ def _events():
77
+ reply = _reply(message)
78
+ {% if spec.guardrails %}
79
+ reply = guard_output(reply).text
80
+ {% endif %}
81
+ for chunk in reply.split(" "):
82
+ yield f"data: {json.dumps({'token': chunk + ' '})}\n\n"
83
+ yield "data: [DONE]\n\n"
84
+
85
+ return StreamingResponse(_events(), media_type="text/event-stream")
@@ -0,0 +1,16 @@
1
+ """MCP tools for {{ spec.slug }}.
2
+
3
+ Configure servers in `mcp_servers.json` at the project root. Tools load lazily
4
+ and degrade gracefully (returns [] if MCP isn't available).
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ from agentx.tools import load_mcp_tools
11
+
12
+ _CONFIG = Path(__file__).resolve().parents[2] / "mcp_servers.json"
13
+
14
+
15
+ def get_mcp_tools():
16
+ return load_mcp_tools(_CONFIG)
@@ -0,0 +1,28 @@
1
+ [project]
2
+ name = "{{ spec.slug }}"
3
+ version = "0.1.0"
4
+ description = "Agentic app generated by AgentX ({{ spec.framework }} · {{ provider_label }})."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10,<3.14"
7
+ dependencies = [
8
+ "agentx-kit[{{ extras_str }}]>=0.1.0",
9
+ "python-dotenv>=1.0.0",
10
+ "ddgs>=9.0.0",
11
+ ]
12
+
13
+ [project.scripts]
14
+ {{ spec.slug }} = "{{ pkg }}.main:main"
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/{{ pkg }}"]
22
+
23
+ {% if spec.agentx_local_path %}
24
+ [tool.uv.sources]
25
+ agentx-kit = { path = "{{ spec.agentx_local_path }}", editable = true }
26
+ {% endif %}
27
+ [tool.pytest.ini_options]
28
+ pythonpath = ["src"]
@@ -0,0 +1,6 @@
1
+ {
2
+ "slug": "star-method",
3
+ "name": "STAR Method",
4
+ "description": "Structure answers as Situation, Task, Action, Result.",
5
+ "instructions": "When answering behavioural or experience questions, structure the response using the STAR method: Situation, Task, Action, Result. Be specific and quantify impact where possible."
6
+ }
@@ -0,0 +1,125 @@
1
+ """Interactive wizard — collects a ``ProjectSpec`` one option at a time.
2
+
3
+ Uses ``questionary`` for arrow-key selection. Every prompt has a sensible
4
+ default so power users can blast through with Enter.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import questionary
9
+
10
+ from ..providers import all_specs, get_spec
11
+ from .spec import AgentSpec, ProjectSpec
12
+
13
+ _FRAMEWORKS = [
14
+ ("LangGraph (LangChain)", "langgraph"),
15
+ ("CrewAI", "crewai"),
16
+ ]
17
+ _MEMORY = [
18
+ ("None", "none"),
19
+ ("Short-term (windowed buffer)", "short"),
20
+ ("Long-term (persistent JSONL)", "long"),
21
+ ("Both", "both"),
22
+ ]
23
+
24
+
25
+ def _select(message: str, choices: list[tuple[str, str]], default_value: str) -> str:
26
+ options = [questionary.Choice(title=label, value=value) for label, value in choices]
27
+ default = next((o for o in options if o.value == default_value), options[0])
28
+ return questionary.select(message, choices=options, default=default).ask()
29
+
30
+
31
+ def run_wizard(name: str | None = None) -> ProjectSpec | None:
32
+ """Run the interactive flow; returns a ProjectSpec, or None if cancelled."""
33
+ questionary.print("🧬 AgentX — new project\n", style="bold fg:cyan")
34
+
35
+ name = name or questionary.text("Project name:", default="my-agent").ask()
36
+ if not name:
37
+ return None
38
+
39
+ framework = _select("Agent framework:", _FRAMEWORKS, "langgraph")
40
+ if framework is None:
41
+ return None
42
+
43
+ # Provider + model
44
+ provider_choices = [
45
+ (f"{s.label} · needs: {', '.join(s.env_vars) or 'no key (local)'}", s.id)
46
+ for s in all_specs()
47
+ ]
48
+ provider = _select("LLM provider:", provider_choices, "openai")
49
+ if provider is None:
50
+ return None
51
+ pspec = get_spec(provider)
52
+ model = questionary.text(
53
+ f"Model id ({pspec.label}):", default=pspec.default_model
54
+ ).ask() or pspec.default_model
55
+
56
+ # Agents
57
+ n_str = questionary.text("How many agents?", default="1").ask() or "1"
58
+ try:
59
+ n_agents = max(1, min(10, int(n_str)))
60
+ except ValueError:
61
+ n_agents = 1
62
+ agents: list[AgentSpec] = []
63
+ for i in range(n_agents):
64
+ questionary.print(f"\nAgent {i + 1} of {n_agents}", style="bold")
65
+ a_name = questionary.text(" name:", default=f"agent_{i + 1}" if n_agents > 1 else "assistant").ask()
66
+ a_role = questionary.text(" role:", default="Helpful Assistant").ask()
67
+ a_goal = questionary.text(" goal:", default="Help the user accomplish their task accurately.").ask()
68
+ a_prompt = questionary.text(
69
+ " system prompt (optional — blank = auto from role/goal):", default="",
70
+ multiline=True,
71
+ ).ask()
72
+ agents.append(AgentSpec(
73
+ name=a_name or f"agent_{i + 1}",
74
+ role=a_role or "Assistant",
75
+ goal=a_goal or "Help the user.",
76
+ system_prompt=(a_prompt or "").strip(),
77
+ ))
78
+
79
+ # Capabilities — one by one
80
+ use_rag = questionary.confirm("Add a RAG module (knowledge base)?", default=False).ask()
81
+ memory = _select("Agent memory:", _MEMORY, "none")
82
+ use_mcp = questionary.confirm("Integrate MCP tools?", default=False).ask()
83
+ use_skills = questionary.confirm("Add a skills registry?", default=False).ask()
84
+ custom_prompts = questionary.confirm("Scaffold custom prompt templates (vs defaults)?", default=False).ask()
85
+
86
+ # Enterprise pack — bundle or pick individually
87
+ enterprise = questionary.confirm(
88
+ "Enable the enterprise pack (tracing, guardrails, FastAPI, Docker, CI, evals)?",
89
+ default=False,
90
+ ).ask()
91
+ if enterprise:
92
+ observability = guardrails = serve = docker = ci = evals = True
93
+ else:
94
+ observability = questionary.confirm(" • OpenTelemetry/Langfuse observability?", default=False).ask()
95
+ guardrails = questionary.confirm(" • Input/output guardrails?", default=False).ask()
96
+ serve = questionary.confirm(" • FastAPI server (REST + SSE)?", default=False).ask()
97
+ docker = questionary.confirm(" • Dockerfile + docker-compose?", default=False).ask()
98
+ ci = questionary.confirm(" • GitHub Actions CI?", default=False).ask()
99
+ evals = questionary.confirm(" • LLM-as-judge eval harness?", default=False).ask()
100
+
101
+ create_venv = questionary.confirm("Create a .venv with `uv` now?", default=True).ask()
102
+ run_sync = False
103
+ if create_venv:
104
+ run_sync = questionary.confirm("Install dependencies now (`uv sync`)? (needs network)", default=False).ask()
105
+
106
+ return ProjectSpec(
107
+ name=name,
108
+ framework=framework,
109
+ provider=provider,
110
+ model=model,
111
+ agents=agents,
112
+ use_rag=bool(use_rag),
113
+ memory=memory or "none",
114
+ use_mcp=bool(use_mcp),
115
+ use_skills=bool(use_skills),
116
+ prompt_style="custom" if custom_prompts else "default",
117
+ observability=bool(observability),
118
+ guardrails=bool(guardrails),
119
+ serve=bool(serve),
120
+ docker=bool(docker),
121
+ ci=bool(ci),
122
+ evals=bool(evals),
123
+ create_venv=bool(create_venv),
124
+ run_sync=bool(run_sync),
125
+ )