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.
- agentx/__init__.py +55 -0
- agentx/cli.py +230 -0
- agentx/config.py +34 -0
- agentx/frameworks/__init__.py +5 -0
- agentx/frameworks/crewai_agent.py +52 -0
- agentx/frameworks/langchain_agent.py +43 -0
- agentx/guardrails.py +89 -0
- agentx/memory/__init__.py +4 -0
- agentx/memory/store.py +78 -0
- agentx/observability.py +103 -0
- agentx/prompts/__init__.py +8 -0
- agentx/prompts/templates.py +40 -0
- agentx/providers/__init__.py +15 -0
- agentx/providers/base.py +50 -0
- agentx/providers/factory.py +71 -0
- agentx/providers/registry.py +165 -0
- agentx/rag/__init__.py +8 -0
- agentx/rag/pipeline.py +121 -0
- agentx/reliability.py +112 -0
- agentx/scaffold/__init__.py +14 -0
- agentx/scaffold/generator.py +190 -0
- agentx/scaffold/prompts_store.py +99 -0
- agentx/scaffold/spec.py +85 -0
- agentx/scaffold/templates/Dockerfile.j2 +17 -0
- agentx/scaffold/templates/README.md.j2 +46 -0
- agentx/scaffold/templates/ci.yml.j2 +41 -0
- agentx/scaffold/templates/docker-compose.yml.j2 +9 -0
- agentx/scaffold/templates/dockerignore.j2 +11 -0
- agentx/scaffold/templates/env.example.j2 +12 -0
- agentx/scaffold/templates/evals/dataset.json.j2 +10 -0
- agentx/scaffold/templates/evals/run_evals.py.j2 +70 -0
- agentx/scaffold/templates/gitignore.j2 +8 -0
- agentx/scaffold/templates/mcp_servers.json.j2 +7 -0
- agentx/scaffold/templates/pkg/__init__.py.j2 +3 -0
- agentx/scaffold/templates/pkg/agents.py.j2 +77 -0
- agentx/scaffold/templates/pkg/config.py.j2 +25 -0
- agentx/scaffold/templates/pkg/guardrails.py.j2 +21 -0
- agentx/scaffold/templates/pkg/main.py.j2 +79 -0
- agentx/scaffold/templates/pkg/memory.py.j2 +17 -0
- agentx/scaffold/templates/pkg/observability.py.j2 +17 -0
- agentx/scaffold/templates/pkg/prompts.py.j2 +45 -0
- agentx/scaffold/templates/pkg/rag.py.j2 +37 -0
- agentx/scaffold/templates/pkg/server.py.j2 +85 -0
- agentx/scaffold/templates/pkg/tools.py.j2 +16 -0
- agentx/scaffold/templates/pyproject.toml.j2 +28 -0
- agentx/scaffold/templates/skills_seed.json.j2 +6 -0
- agentx/scaffold/wizard.py +125 -0
- agentx/skills/__init__.py +4 -0
- agentx/skills/registry.py +63 -0
- agentx/structured.py +37 -0
- agentx/tools/__init__.py +5 -0
- agentx/tools/builtin.py +45 -0
- agentx/tools/mcp.py +64 -0
- agentx_kit-0.2.0.dist-info/METADATA +289 -0
- agentx_kit-0.2.0.dist-info/RECORD +58 -0
- agentx_kit-0.2.0.dist-info/WHEEL +4 -0
- agentx_kit-0.2.0.dist-info/entry_points.txt +2 -0
- agentx_kit-0.2.0.dist-info/licenses/LICENSE +21 -0
agentx/observability.py
ADDED
|
@@ -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,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
|
+
]
|
agentx/providers/base.py
ADDED
|
@@ -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
|