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,103 @@
1
+ """Observability: OpenTelemetry GenAI tracing with pluggable backends.
2
+
3
+ Follows the 2025–2026 convergence on OpenTelemetry `gen_ai.*` semantic
4
+ conventions. Everything is optional and lazy — if the extras aren't installed,
5
+ calls are graceful no-ops. Telemetry honours an explicit opt-out.
6
+
7
+ Usage in an app::
8
+
9
+ from agentx.observability import setup_tracing, get_callbacks
10
+ setup_tracing("my-service") # instrument once at startup
11
+ llm.invoke(msg, config={"callbacks": get_callbacks()})
12
+
13
+ Env:
14
+ AGENTX_TELEMETRY=false # global kill-switch (also OTEL_SDK_DISABLED=true)
15
+ OTEL_EXPORTER_OTLP_ENDPOINT=... # send OTel spans here (Phoenix/Tempo/etc.)
16
+ LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY [/ LANGFUSE_HOST]
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ import os
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ _INSTRUMENTED = False
26
+
27
+
28
+ def telemetry_enabled() -> bool:
29
+ """True unless explicitly disabled via AGENTX_TELEMETRY or OTEL_SDK_DISABLED."""
30
+ if os.getenv("OTEL_SDK_DISABLED", "").lower() == "true":
31
+ return False
32
+ return os.getenv("AGENTX_TELEMETRY", "true").lower() not in ("false", "0", "no")
33
+
34
+
35
+ def setup_tracing(service_name: str = "agentx") -> bool:
36
+ """Instrument LangChain with OpenTelemetry (OpenInference). Returns success.
37
+
38
+ No-op (returns False) if telemetry is disabled or the optional extras
39
+ (`agentx-kit[observability]`) aren't installed.
40
+ """
41
+ global _INSTRUMENTED
42
+ if _INSTRUMENTED:
43
+ return True
44
+ if not telemetry_enabled():
45
+ logger.info("Telemetry disabled; skipping tracing setup.")
46
+ return False
47
+ try:
48
+ from openinference.instrumentation.langchain import LangChainInstrumentor
49
+ from opentelemetry import trace
50
+ from opentelemetry.sdk.resources import Resource
51
+ from opentelemetry.sdk.trace import TracerProvider
52
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
53
+ except ImportError:
54
+ logger.info("OpenTelemetry extras not installed; run `pip install 'agentx-kit[observability]'`.")
55
+ return False
56
+
57
+ try:
58
+ provider = TracerProvider(resource=Resource.create({"service.name": service_name}))
59
+ endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
60
+ if endpoint:
61
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
62
+
63
+ provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
64
+ else:
65
+ from opentelemetry.sdk.trace.export import ConsoleSpanExporter
66
+
67
+ provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
68
+ trace.set_tracer_provider(provider)
69
+ LangChainInstrumentor().instrument()
70
+ _INSTRUMENTED = True
71
+ logger.info("OpenTelemetry tracing enabled for '%s' (endpoint=%s).", service_name, endpoint or "console")
72
+ return True
73
+ except Exception as exc: # noqa: BLE001
74
+ logger.warning("Tracing setup failed: %s", exc)
75
+ return False
76
+
77
+
78
+ def langfuse_callbacks() -> list:
79
+ """Return [Langfuse CallbackHandler] if Langfuse is installed + configured."""
80
+ if not telemetry_enabled():
81
+ return []
82
+ if not (os.getenv("LANGFUSE_PUBLIC_KEY") and os.getenv("LANGFUSE_SECRET_KEY")):
83
+ return []
84
+ try:
85
+ from langfuse.langchain import CallbackHandler
86
+
87
+ return [CallbackHandler()]
88
+ except ImportError:
89
+ try:
90
+ from langfuse.callback import CallbackHandler # older layout
91
+
92
+ return [CallbackHandler()]
93
+ except ImportError:
94
+ logger.info("Langfuse not installed; `pip install langfuse` to enable.")
95
+ return []
96
+ except Exception as exc: # noqa: BLE001
97
+ logger.warning("Langfuse callback init failed: %s", exc)
98
+ return []
99
+
100
+
101
+ def get_callbacks() -> list:
102
+ """LangChain callbacks to pass via ``config={'callbacks': get_callbacks()}``."""
103
+ return langfuse_callbacks()
@@ -0,0 +1,8 @@
1
+ """Reusable, override-able prompt templates for agents."""
2
+ from .templates import (
3
+ AGENT_SYSTEM_PROMPT,
4
+ RAG_SYSTEM_PROMPT,
5
+ render_agent_system,
6
+ )
7
+
8
+ __all__ = ["AGENT_SYSTEM_PROMPT", "RAG_SYSTEM_PROMPT", "render_agent_system"]
@@ -0,0 +1,40 @@
1
+ """Prompt templates and a small renderer.
2
+
3
+ Kept as plain strings (not framework-specific) so they work with both LangChain
4
+ and CrewAI. ``render_agent_system`` lets callers inject role, goal and optional
5
+ skills/RAG guidance without pulling in a templating engine at runtime.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ AGENT_SYSTEM_PROMPT = (
10
+ "You are {role}.\n"
11
+ "Your goal: {goal}\n"
12
+ "Guidelines:\n"
13
+ "- Be precise, concise, and grounded in available context/tools.\n"
14
+ "- If you are unsure, say so rather than inventing facts.\n"
15
+ "- Prefer using your tools over guessing.\n"
16
+ )
17
+
18
+ RAG_SYSTEM_PROMPT = (
19
+ "Answer the user using ONLY the retrieved context below. "
20
+ "If the answer is not in the context, say you don't know.\n\n"
21
+ "Context:\n{context}\n"
22
+ )
23
+
24
+ _SKILLS_BLOCK = "\nApply these skills/standards:\n{skills}\n"
25
+ _RAG_BLOCK = "\nYou have a knowledge base; retrieve before answering when relevant.\n"
26
+
27
+
28
+ def render_agent_system(
29
+ role: str,
30
+ goal: str,
31
+ skills: str | None = None,
32
+ with_rag: bool = False,
33
+ ) -> str:
34
+ """Render an agent system prompt with optional skills + RAG hints."""
35
+ out = AGENT_SYSTEM_PROMPT.format(role=role, goal=goal)
36
+ if skills:
37
+ out += _SKILLS_BLOCK.format(skills=skills)
38
+ if with_rag:
39
+ out += _RAG_BLOCK
40
+ return out
@@ -0,0 +1,15 @@
1
+ """Provider subsystem: a registry + factory for building LLM clients."""
2
+ from .base import ProviderError, ProviderSpec
3
+ from .factory import get_chat_model, get_crewai_llm, list_providers
4
+ from .registry import all_specs, canonical_ids, get_spec
5
+
6
+ __all__ = [
7
+ "ProviderError",
8
+ "ProviderSpec",
9
+ "get_chat_model",
10
+ "get_crewai_llm",
11
+ "list_providers",
12
+ "all_specs",
13
+ "canonical_ids",
14
+ "get_spec",
15
+ ]
@@ -0,0 +1,50 @@
1
+ """Provider abstractions: ``ProviderSpec`` and shared helpers.
2
+
3
+ A ``ProviderSpec`` is pure metadata + two builder callables. Builders import
4
+ their provider SDK lazily so installing one extra never forces the others.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import importlib
9
+ import os
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Callable
12
+
13
+
14
+ class ProviderError(RuntimeError):
15
+ """Raised when a provider cannot be constructed (missing package / creds)."""
16
+
17
+
18
+ def require(module: str, extra: str) -> Any:
19
+ """Import ``module`` or raise a helpful, actionable error naming the extra."""
20
+ try:
21
+ return importlib.import_module(module)
22
+ except ImportError as exc: # pragma: no cover - exercised via factory error path
23
+ raise ProviderError(
24
+ f"Missing dependency '{module}'. Install it with:\n"
25
+ f" uv pip install 'agentx-kit[{extra}]'\n"
26
+ f"(or: pip install {module})"
27
+ ) from exc
28
+
29
+
30
+ def warn_missing_env(spec: "ProviderSpec") -> list[str]:
31
+ """Return the subset of a provider's env vars that are not set."""
32
+ return [v for v in spec.env_vars if not os.getenv(v)]
33
+
34
+
35
+ # A builder takes (model, **kwargs) and returns a LangChain BaseChatModel.
36
+ ChatBuilder = Callable[..., Any]
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class ProviderSpec:
41
+ id: str
42
+ label: str
43
+ extra: str # pip extra: agentx-kit[<extra>]
44
+ packages: tuple[str, ...] # importable module names the extra provides
45
+ default_model: str
46
+ env_vars: tuple[str, ...] # standard credential env vars (informational)
47
+ crewai_prefix: str # LiteLLM/CrewAI model prefix, e.g. "openrouter/"
48
+ build_chat: ChatBuilder # () -> langchain BaseChatModel
49
+ notes: str = ""
50
+ aliases: tuple[str, ...] = field(default_factory=tuple)
@@ -0,0 +1,71 @@
1
+ """Public factory functions for building LLM clients across frameworks."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import Any
6
+
7
+ from ..config import get_settings
8
+ from .base import ProviderError, ProviderSpec, warn_missing_env
9
+ from .registry import all_specs, get_spec
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def list_providers() -> list[ProviderSpec]:
15
+ """Return all registered provider specs (canonical, no aliases)."""
16
+ return all_specs()
17
+
18
+
19
+ def get_chat_model(provider: str | None = None, model: str | None = None, **kwargs: Any):
20
+ """Build a LangChain ``BaseChatModel`` for any supported provider.
21
+
22
+ Args:
23
+ provider: provider id (e.g. "openai", "bedrock", "openrouter"). Defaults
24
+ to ``AGENTX_PROVIDER`` / settings.
25
+ model: model id; falls back to the provider's default.
26
+ **kwargs: passed through to the underlying chat class (temperature, etc.).
27
+ """
28
+ s = get_settings()
29
+ provider = provider or s.default_provider
30
+ spec = get_spec(provider)
31
+ model = model or s.default_model or spec.default_model
32
+
33
+ missing = warn_missing_env(spec)
34
+ if missing:
35
+ logger.warning(
36
+ "Provider '%s' is missing env vars: %s. The call may fail to authenticate.",
37
+ spec.id, ", ".join(missing),
38
+ )
39
+ return spec.build_chat(model, **kwargs)
40
+
41
+
42
+ def get_crewai_llm(provider: str | None = None, model: str | None = None, **kwargs: Any):
43
+ """Build a CrewAI ``LLM`` for any supported provider.
44
+
45
+ CrewAI routes through LiteLLM, so we map the provider to its LiteLLM prefix
46
+ (e.g. ``openrouter/`` , ``bedrock/`` , ``gemini/``) and pass base_url/api_key
47
+ where relevant. Requires ``agentx-kit[crewai]``.
48
+ """
49
+ s = get_settings()
50
+ provider = provider or s.default_provider
51
+ spec = get_spec(provider)
52
+ model = model or s.default_model or spec.default_model
53
+
54
+ try:
55
+ from crewai import LLM # type: ignore
56
+ except ImportError as exc:
57
+ raise ProviderError(
58
+ "CrewAI is not installed. Install it with:\n"
59
+ " uv pip install 'agentx-kit[crewai]'"
60
+ ) from exc
61
+
62
+ # Avoid double-prefixing if the caller already passed e.g. "openrouter/...".
63
+ litellm_model = model if "/" in model and model.startswith(spec.crewai_prefix) else f"{spec.crewai_prefix}{model}"
64
+
65
+ params: dict[str, Any] = {"model": litellm_model, "temperature": kwargs.pop("temperature", s.temperature)}
66
+ if spec.id == "openrouter":
67
+ params["base_url"] = s.openrouter_base_url
68
+ if spec.id == "ollama":
69
+ params["base_url"] = s.ollama_base_url
70
+ params.update(kwargs)
71
+ return LLM(**params)
@@ -0,0 +1,165 @@
1
+ """The provider registry — every supported LLM backend in one place.
2
+
3
+ Each entry's ``build_chat`` returns a LangChain ``BaseChatModel``. Builders pull
4
+ generic defaults (temperature, timeout) from settings and accept ``**kwargs``
5
+ overrides; provider credentials come from each SDK's standard env vars.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from typing import Any
11
+
12
+ from ..config import get_settings
13
+ from .base import ProviderSpec, require
14
+
15
+
16
+ def _common(kwargs: dict) -> dict:
17
+ s = get_settings()
18
+ kwargs.setdefault("temperature", s.temperature)
19
+ return kwargs
20
+
21
+
22
+ # --------------------------------------------------------------------------- #
23
+ # Builders (lazy imports inside each)
24
+ # --------------------------------------------------------------------------- #
25
+ def _build_openai(model: str | None = None, **kwargs: Any):
26
+ mod = require("langchain_openai", "openai")
27
+ return mod.ChatOpenAI(model=model or "gpt-4o-mini", **_common(kwargs))
28
+
29
+
30
+ def _build_azure(model: str | None = None, **kwargs: Any):
31
+ mod = require("langchain_openai", "azure")
32
+ kwargs.setdefault("api_version", os.getenv("AZURE_OPENAI_API_VERSION", "2024-06-01"))
33
+ kwargs.setdefault("azure_endpoint", os.getenv("AZURE_OPENAI_ENDPOINT"))
34
+ # On Azure, `model` is the *deployment* name.
35
+ return mod.AzureChatOpenAI(azure_deployment=model or os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4o"), **_common(kwargs))
36
+
37
+
38
+ def _build_openrouter(model: str | None = None, **kwargs: Any):
39
+ mod = require("langchain_openai", "openrouter")
40
+ s = get_settings()
41
+ kwargs.setdefault("base_url", s.openrouter_base_url)
42
+ kwargs.setdefault("api_key", os.getenv("OPENROUTER_API_KEY"))
43
+ return mod.ChatOpenAI(model=model or "openai/gpt-4o-mini", **_common(kwargs))
44
+
45
+
46
+ def _build_anthropic(model: str | None = None, **kwargs: Any):
47
+ mod = require("langchain_anthropic", "anthropic")
48
+ return mod.ChatAnthropic(model=model or "claude-3-5-sonnet-latest", **_common(kwargs))
49
+
50
+
51
+ def _build_gemini(model: str | None = None, **kwargs: Any):
52
+ mod = require("langchain_google_genai", "google")
53
+ return mod.ChatGoogleGenerativeAI(model=model or "gemini-1.5-flash", **_common(kwargs))
54
+
55
+
56
+ def _build_vertex(model: str | None = None, **kwargs: Any):
57
+ mod = require("langchain_google_vertexai", "vertex")
58
+ kwargs.setdefault("project", os.getenv("GOOGLE_CLOUD_PROJECT"))
59
+ kwargs.setdefault("location", os.getenv("GOOGLE_CLOUD_LOCATION", "us-central1"))
60
+ return mod.ChatVertexAI(model=model or "gemini-1.5-flash", **_common(kwargs))
61
+
62
+
63
+ def _build_bedrock(model: str | None = None, **kwargs: Any):
64
+ mod = require("langchain_aws", "bedrock")
65
+ kwargs.setdefault("region_name", os.getenv("AWS_REGION", os.getenv("AWS_DEFAULT_REGION", "us-east-1")))
66
+ return mod.ChatBedrockConverse(model=model or "anthropic.claude-3-5-sonnet-20240620-v1:0", **_common(kwargs))
67
+
68
+
69
+ def _build_groq(model: str | None = None, **kwargs: Any):
70
+ mod = require("langchain_groq", "groq")
71
+ return mod.ChatGroq(model=model or "llama-3.3-70b-versatile", **_common(kwargs))
72
+
73
+
74
+ def _build_ollama(model: str | None = None, **kwargs: Any):
75
+ mod = require("langchain_ollama", "ollama")
76
+ kwargs.setdefault("base_url", get_settings().ollama_base_url)
77
+ return mod.ChatOllama(model=model or "llama3.2", **_common(kwargs))
78
+
79
+
80
+ # --------------------------------------------------------------------------- #
81
+ # Registry
82
+ # --------------------------------------------------------------------------- #
83
+ _REGISTRY: dict[str, ProviderSpec] = {}
84
+
85
+
86
+ def _register(spec: ProviderSpec) -> None:
87
+ _REGISTRY[spec.id] = spec
88
+ for alias in spec.aliases:
89
+ _REGISTRY[alias] = spec
90
+
91
+
92
+ _register(ProviderSpec(
93
+ id="openai", label="OpenAI", extra="openai", packages=("langchain_openai",),
94
+ default_model="gpt-4o-mini", env_vars=("OPENAI_API_KEY",), crewai_prefix="openai/",
95
+ build_chat=_build_openai,
96
+ ))
97
+ _register(ProviderSpec(
98
+ id="azure", label="Azure OpenAI", extra="azure", packages=("langchain_openai",),
99
+ default_model="gpt-4o", env_vars=("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT"),
100
+ crewai_prefix="azure/", build_chat=_build_azure, aliases=("azure_openai",),
101
+ notes="`model` is your Azure *deployment* name; set AZURE_OPENAI_API_VERSION/ENDPOINT.",
102
+ ))
103
+ _register(ProviderSpec(
104
+ id="openrouter", label="OpenRouter", extra="openrouter", packages=("langchain_openai",),
105
+ default_model="openai/gpt-4o-mini", env_vars=("OPENROUTER_API_KEY",),
106
+ crewai_prefix="openrouter/", build_chat=_build_openrouter,
107
+ notes="OpenAI-compatible gateway to 200+ models; model id like 'anthropic/claude-3.5-sonnet'.",
108
+ ))
109
+ _register(ProviderSpec(
110
+ id="anthropic", label="Anthropic (Claude)", extra="anthropic", packages=("langchain_anthropic",),
111
+ default_model="claude-3-5-sonnet-latest", env_vars=("ANTHROPIC_API_KEY",),
112
+ crewai_prefix="anthropic/", build_chat=_build_anthropic, aliases=("claude",),
113
+ ))
114
+ _register(ProviderSpec(
115
+ id="gemini", label="Google Gemini (AI Studio)", extra="google",
116
+ packages=("langchain_google_genai",), default_model="gemini-1.5-flash",
117
+ env_vars=("GOOGLE_API_KEY",), crewai_prefix="gemini/", build_chat=_build_gemini,
118
+ aliases=("google", "google_genai"),
119
+ ))
120
+ _register(ProviderSpec(
121
+ id="vertexai", label="Google Vertex AI", extra="vertex",
122
+ packages=("langchain_google_vertexai",), default_model="gemini-1.5-flash",
123
+ env_vars=("GOOGLE_CLOUD_PROJECT", "GOOGLE_APPLICATION_CREDENTIALS"),
124
+ crewai_prefix="vertex_ai/", build_chat=_build_vertex, aliases=("vertex",),
125
+ notes="Uses ADC / service-account; set GOOGLE_CLOUD_PROJECT and credentials.",
126
+ ))
127
+ _register(ProviderSpec(
128
+ id="bedrock", label="Amazon Bedrock", extra="bedrock", packages=("langchain_aws",),
129
+ default_model="anthropic.claude-3-5-sonnet-20240620-v1:0",
130
+ env_vars=("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"), crewai_prefix="bedrock/",
131
+ build_chat=_build_bedrock, aliases=("aws",),
132
+ notes="Uses standard AWS credential chain; set AWS_REGION.",
133
+ ))
134
+ _register(ProviderSpec(
135
+ id="groq", label="Groq", extra="groq", packages=("langchain_groq",),
136
+ default_model="llama-3.3-70b-versatile", env_vars=("GROQ_API_KEY",),
137
+ crewai_prefix="groq/", build_chat=_build_groq,
138
+ ))
139
+ _register(ProviderSpec(
140
+ id="ollama", label="Ollama (local)", extra="ollama", packages=("langchain_ollama",),
141
+ default_model="llama3.2", env_vars=(), crewai_prefix="ollama/",
142
+ build_chat=_build_ollama, notes="Runs locally; no API key. `ollama serve` + pull a model.",
143
+ ))
144
+
145
+
146
+ def get_spec(provider: str) -> ProviderSpec:
147
+ key = (provider or "").strip().lower()
148
+ if key not in _REGISTRY:
149
+ raise KeyError(
150
+ f"Unknown provider '{provider}'. Known: {', '.join(sorted(canonical_ids()))}"
151
+ )
152
+ return _REGISTRY[key]
153
+
154
+
155
+ def canonical_ids() -> list[str]:
156
+ """Distinct canonical provider ids (excludes aliases), in registration order."""
157
+ seen: list[str] = []
158
+ for spec in _REGISTRY.values():
159
+ if spec.id not in seen:
160
+ seen.append(spec.id)
161
+ return seen
162
+
163
+
164
+ def all_specs() -> list[ProviderSpec]:
165
+ return [_REGISTRY[i] for i in canonical_ids()]
agentx/rag/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """RAG: chunk documents, index them, retrieve relevant passages.
2
+
3
+ Uses Chroma when ``agentx-kit[rag]`` is installed; otherwise falls back to a
4
+ dependency-free in-memory keyword retriever so RAG works out of the box.
5
+ """
6
+ from .pipeline import RagIndex, build_index_from_texts, make_retriever_tool
7
+
8
+ __all__ = ["RagIndex", "build_index_from_texts", "make_retriever_tool"]
agentx/rag/pipeline.py ADDED
@@ -0,0 +1,121 @@
1
+ """A small, swappable RAG pipeline.
2
+
3
+ Strategy:
4
+ * Split text into chunks (LangChain splitter if available, else a simple splitter).
5
+ * Index with Chroma + embeddings when ``[rag]`` is installed.
6
+ * Otherwise fall back to an in-memory keyword retriever (no deps), so the
7
+ generated project runs immediately and can be upgraded later.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import re
13
+ from collections import Counter
14
+ from dataclasses import dataclass, field
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _split(text: str, chunk_size: int = 800, overlap: int = 120) -> list[str]:
20
+ try:
21
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
22
+
23
+ splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=overlap)
24
+ return [c for c in splitter.split_text(text) if c.strip()]
25
+ except ImportError:
26
+ chunks, start = [], 0
27
+ while start < len(text):
28
+ chunks.append(text[start:start + chunk_size])
29
+ start += chunk_size - overlap
30
+ return [c for c in chunks if c.strip()]
31
+
32
+
33
+ _TOKEN = re.compile(r"[a-z0-9]+")
34
+
35
+
36
+ def _tokens(s: str) -> list[str]:
37
+ return [t for t in _TOKEN.findall(s.lower()) if len(t) > 1]
38
+
39
+
40
+ @dataclass
41
+ class RagIndex:
42
+ """Holds chunks and answers similarity queries.
43
+
44
+ If a vector store is available it is used; otherwise a keyword scorer ranks
45
+ chunks. The public ``search`` API is identical either way.
46
+ """
47
+
48
+ chunks: list[str] = field(default_factory=list)
49
+ _store: object | None = None
50
+
51
+ def search(self, query: str, k: int = 4) -> list[str]:
52
+ if self._store is not None:
53
+ try:
54
+ docs = self._store.similarity_search(query, k=k)
55
+ return [d.page_content for d in docs]
56
+ except Exception as exc: # noqa: BLE001
57
+ logger.warning("Vector search failed, falling back to keyword: %s", exc)
58
+ # Keyword fallback.
59
+ if not self.chunks:
60
+ return []
61
+ q = Counter(_tokens(query))
62
+ scored = [(sum(_tokens(c).count(t) for t in q), c) for c in self.chunks]
63
+ scored.sort(key=lambda x: x[0], reverse=True)
64
+ top = [c for s, c in scored if s > 0][:k]
65
+ return top or self.chunks[:k]
66
+
67
+ def context(self, query: str, k: int = 4) -> str:
68
+ return "\n---\n".join(self.search(query, k))
69
+
70
+
71
+ def build_index_from_texts(texts: list[str], persist_dir: str | None = None, embeddings=None) -> RagIndex:
72
+ """Build a ``RagIndex`` from raw texts; uses Chroma if installed."""
73
+ chunks: list[str] = []
74
+ for t in texts:
75
+ chunks.extend(_split(t))
76
+
77
+ store = None
78
+ try:
79
+ from langchain_chroma import Chroma # type: ignore
80
+
81
+ if embeddings is None:
82
+ from langchain_core.embeddings import Embeddings # noqa: F401
83
+ embeddings = _default_embeddings()
84
+ if embeddings is not None:
85
+ store = Chroma.from_texts(chunks, embedding=embeddings, persist_directory=persist_dir)
86
+ except ImportError:
87
+ logger.info("Chroma not installed; using in-memory keyword retriever. Install 'agentx-kit[rag]' to upgrade.")
88
+ except Exception as exc: # noqa: BLE001
89
+ logger.warning("Vector index build failed (%s); using keyword retriever.", exc)
90
+
91
+ return RagIndex(chunks=chunks, _store=store)
92
+
93
+
94
+ def _default_embeddings():
95
+ """Best-effort embeddings: try a local/OpenAI embedder, else None (keyword mode)."""
96
+ try:
97
+ from langchain_openai import OpenAIEmbeddings # type: ignore
98
+ import os
99
+
100
+ if os.getenv("OPENAI_API_KEY"):
101
+ return OpenAIEmbeddings(model="text-embedding-3-small")
102
+ except ImportError:
103
+ pass
104
+ try:
105
+ from langchain_ollama import OllamaEmbeddings # type: ignore
106
+
107
+ return OllamaEmbeddings(model="nomic-embed-text")
108
+ except ImportError:
109
+ return None
110
+
111
+
112
+ def make_retriever_tool(index: RagIndex):
113
+ """Expose a ``RagIndex`` as a LangChain retrieval ``@tool``."""
114
+ from langchain_core.tools import tool
115
+
116
+ @tool
117
+ def knowledge_base(query: str) -> str:
118
+ """Search the project's knowledge base and return relevant passages."""
119
+ return index.context(query)
120
+
121
+ return knowledge_base