idun-agent-engine 0.3.4__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.
- idun_agent_engine/__init__.py +24 -0
- idun_agent_engine/_version.py +3 -0
- idun_agent_engine/agent/__init__.py +10 -0
- idun_agent_engine/agent/adk/__init__.py +5 -0
- idun_agent_engine/agent/adk/adk.py +296 -0
- idun_agent_engine/agent/base.py +112 -0
- idun_agent_engine/agent/haystack/__init__.py +9 -0
- idun_agent_engine/agent/haystack/haystack.py +274 -0
- idun_agent_engine/agent/haystack/haystack_model.py +13 -0
- idun_agent_engine/agent/haystack/utils.py +13 -0
- idun_agent_engine/agent/langgraph/__init__.py +7 -0
- idun_agent_engine/agent/langgraph/langgraph.py +553 -0
- idun_agent_engine/core/__init__.py +11 -0
- idun_agent_engine/core/app_factory.py +73 -0
- idun_agent_engine/core/config_builder.py +657 -0
- idun_agent_engine/core/engine_config.py +21 -0
- idun_agent_engine/core/server_runner.py +145 -0
- idun_agent_engine/guardrails/__init__.py +0 -0
- idun_agent_engine/guardrails/base.py +24 -0
- idun_agent_engine/guardrails/guardrails_hub/guardrails_hub.py +101 -0
- idun_agent_engine/guardrails/guardrails_hub/utils.py +1 -0
- idun_agent_engine/mcp/__init__.py +5 -0
- idun_agent_engine/mcp/helpers.py +97 -0
- idun_agent_engine/mcp/registry.py +109 -0
- idun_agent_engine/observability/__init__.py +17 -0
- idun_agent_engine/observability/base.py +172 -0
- idun_agent_engine/observability/gcp_logging/__init__.py +0 -0
- idun_agent_engine/observability/gcp_logging/gcp_logging_handler.py +52 -0
- idun_agent_engine/observability/gcp_trace/__init__.py +0 -0
- idun_agent_engine/observability/gcp_trace/gcp_trace_handler.py +116 -0
- idun_agent_engine/observability/langfuse/__init__.py +5 -0
- idun_agent_engine/observability/langfuse/langfuse_handler.py +79 -0
- idun_agent_engine/observability/phoenix/__init__.py +5 -0
- idun_agent_engine/observability/phoenix/phoenix_handler.py +65 -0
- idun_agent_engine/observability/phoenix_local/__init__.py +5 -0
- idun_agent_engine/observability/phoenix_local/phoenix_local_handler.py +123 -0
- idun_agent_engine/py.typed +0 -0
- idun_agent_engine/server/__init__.py +5 -0
- idun_agent_engine/server/dependencies.py +52 -0
- idun_agent_engine/server/lifespan.py +106 -0
- idun_agent_engine/server/routers/__init__.py +5 -0
- idun_agent_engine/server/routers/agent.py +204 -0
- idun_agent_engine/server/routers/agui.py +47 -0
- idun_agent_engine/server/routers/base.py +114 -0
- idun_agent_engine/server/server_config.py +8 -0
- idun_agent_engine/templates/__init__.py +1 -0
- idun_agent_engine/templates/correction.py +65 -0
- idun_agent_engine/templates/deep_research.py +40 -0
- idun_agent_engine/templates/translation.py +70 -0
- idun_agent_engine-0.3.4.dist-info/METADATA +335 -0
- idun_agent_engine-0.3.4.dist-info/RECORD +60 -0
- idun_agent_engine-0.3.4.dist-info/WHEEL +4 -0
- idun_agent_engine-0.3.4.dist-info/entry_points.txt +2 -0
- idun_platform_cli/__init__.py +0 -0
- idun_platform_cli/groups/__init__.py +0 -0
- idun_platform_cli/groups/agent/__init__.py +0 -0
- idun_platform_cli/groups/agent/main.py +16 -0
- idun_platform_cli/groups/agent/package.py +70 -0
- idun_platform_cli/groups/agent/serve.py +107 -0
- idun_platform_cli/main.py +14 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Server Runner for Idun Agent Engine.
|
|
2
|
+
|
|
3
|
+
This module provides convenient functions to run FastAPI applications created with
|
|
4
|
+
the Idun Agent Engine. It handles common deployment scenarios and provides sensible defaults.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import uvicorn
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_server(
|
|
12
|
+
app: FastAPI,
|
|
13
|
+
host: str = "0.0.0.0",
|
|
14
|
+
port: int = 8000,
|
|
15
|
+
reload: bool = False,
|
|
16
|
+
log_level: str = "info",
|
|
17
|
+
workers: int | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Run a FastAPI application created with Idun Agent Engine.
|
|
20
|
+
|
|
21
|
+
This is a convenience function that wraps uvicorn.run() with sensible defaults
|
|
22
|
+
for serving agent applications. It automatically handles common deployment scenarios.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
app: The FastAPI application created with create_app()
|
|
26
|
+
host: Host to bind the server to. Defaults to "0.0.0.0" (all interfaces)
|
|
27
|
+
port: Port to bind the server to. Defaults to 8000
|
|
28
|
+
reload: Enable auto-reload for development. Defaults to False
|
|
29
|
+
log_level: Logging level. Defaults to "info"
|
|
30
|
+
workers: Number of worker processes. If None, uses single process
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
from idun_agent_engine import create_app, run_server
|
|
34
|
+
|
|
35
|
+
# Create your app
|
|
36
|
+
app = create_app("config.yaml")
|
|
37
|
+
|
|
38
|
+
# Run in development mode
|
|
39
|
+
run_server(app, reload=True)
|
|
40
|
+
|
|
41
|
+
# Run in production mode
|
|
42
|
+
run_server(app, workers=4)
|
|
43
|
+
"""
|
|
44
|
+
print(f"🌐 Starting Idun Agent Engine server on http://{host}:{port}...")
|
|
45
|
+
print(f"📚 API documentation available at http://{host}:{port}/docs")
|
|
46
|
+
|
|
47
|
+
if reload and workers:
|
|
48
|
+
print(
|
|
49
|
+
"⚠️ Warning: reload=True is incompatible with workers > 1. Disabling reload."
|
|
50
|
+
)
|
|
51
|
+
reload = False
|
|
52
|
+
|
|
53
|
+
print("Config: ", app.state.engine_config)
|
|
54
|
+
uvicorn.run(
|
|
55
|
+
app,
|
|
56
|
+
host=host,
|
|
57
|
+
port=port,
|
|
58
|
+
log_level=log_level,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def run_server_from_config(config_path: str = "config.yaml", **kwargs) -> None:
|
|
63
|
+
"""Create and run a server directly from a configuration file.
|
|
64
|
+
|
|
65
|
+
This is the most convenient way to start a server - it combines create_app()
|
|
66
|
+
and run_server() in a single function call using ConfigBuilder.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
config_path: Path to the configuration YAML file
|
|
70
|
+
**kwargs: Additional arguments passed to run_server()
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
# Run server directly from config
|
|
74
|
+
run_server_from_config("my_agent.yaml", port=8080, reload=True)
|
|
75
|
+
"""
|
|
76
|
+
from .app_factory import create_app
|
|
77
|
+
from .config_builder import ConfigBuilder
|
|
78
|
+
|
|
79
|
+
# Load configuration using ConfigBuilder
|
|
80
|
+
engine_config = ConfigBuilder.load_from_file(config_path)
|
|
81
|
+
|
|
82
|
+
# Create app with the loaded config
|
|
83
|
+
app = create_app(engine_config=engine_config)
|
|
84
|
+
|
|
85
|
+
# Extract port from config if not overridden
|
|
86
|
+
if "port" not in kwargs:
|
|
87
|
+
kwargs["port"] = engine_config.server.api.port
|
|
88
|
+
|
|
89
|
+
# Show configuration info
|
|
90
|
+
print(f"🔧 Loaded configuration from {config_path}")
|
|
91
|
+
# Best-effort: handle both dict-like and model access
|
|
92
|
+
agent_name = (
|
|
93
|
+
engine_config.agent.config.get("name") # type: ignore[call-arg, index]
|
|
94
|
+
if hasattr(engine_config.agent.config, "get")
|
|
95
|
+
else getattr(engine_config.agent.config, "name", "Unknown")
|
|
96
|
+
)
|
|
97
|
+
print(f"🤖 Agent: {agent_name} ({engine_config.agent.type})")
|
|
98
|
+
|
|
99
|
+
run_server(app, **kwargs)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def run_server_from_builder(config_builder, **kwargs) -> None:
|
|
103
|
+
"""Create and run a server directly from a ConfigBuilder instance.
|
|
104
|
+
|
|
105
|
+
This allows for programmatic configuration with immediate server startup.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
config_builder: ConfigBuilder instance (can be built or unbuilt)
|
|
109
|
+
**kwargs: Additional arguments passed to run_server()
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
from idun_agent_engine import ConfigBuilder
|
|
113
|
+
|
|
114
|
+
builder = (ConfigBuilder()
|
|
115
|
+
.with_langgraph_agent(name="My Agent", graph_definition="agent.py:graph")
|
|
116
|
+
.with_api_port(8080))
|
|
117
|
+
|
|
118
|
+
run_server_from_builder(builder, reload=True)
|
|
119
|
+
"""
|
|
120
|
+
from .app_factory import create_app
|
|
121
|
+
|
|
122
|
+
# Build the configuration if it's a ConfigBuilder instance
|
|
123
|
+
if hasattr(config_builder, "build"):
|
|
124
|
+
engine_config = config_builder.build()
|
|
125
|
+
else:
|
|
126
|
+
# Assume it's already an EngineConfig
|
|
127
|
+
engine_config = config_builder
|
|
128
|
+
|
|
129
|
+
# Create app with the config
|
|
130
|
+
app = create_app(engine_config=engine_config)
|
|
131
|
+
|
|
132
|
+
# Extract port from config if not overridden
|
|
133
|
+
if "port" not in kwargs:
|
|
134
|
+
kwargs["port"] = engine_config.server.api.port
|
|
135
|
+
|
|
136
|
+
# Show configuration info
|
|
137
|
+
print("🔧 Using programmatic configuration")
|
|
138
|
+
agent_name = (
|
|
139
|
+
engine_config.agent.config.get("name") # type: ignore[call-arg, index]
|
|
140
|
+
if hasattr(engine_config.agent.config, "get")
|
|
141
|
+
else getattr(engine_config.agent.config, "name", "Unknown")
|
|
142
|
+
)
|
|
143
|
+
print(f"🤖 Agent: {agent_name} ({engine_config.agent.type})")
|
|
144
|
+
|
|
145
|
+
run_server(app, **kwargs)
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from idun_agent_schema.engine.guardrails import Guardrail
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseGuardrail(ABC):
|
|
8
|
+
"""Base class for different guardrail providers."""
|
|
9
|
+
|
|
10
|
+
# TODO: output
|
|
11
|
+
|
|
12
|
+
def __init__(self, config: Guardrail) -> None:
|
|
13
|
+
if not isinstance(config, Guardrail):
|
|
14
|
+
raise TypeError(
|
|
15
|
+
f"The Guardrail must be a `Guardrail` schema type, received instead: {type(config)}"
|
|
16
|
+
)
|
|
17
|
+
self._guardrail_config = config
|
|
18
|
+
# config for the specific guardrails type. currently, can only be guardrails_hub config
|
|
19
|
+
self._instance_config: dict[str, Any] = None
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def validate(self, input: str) -> bool:
|
|
23
|
+
"""Used for validating user input, or LLM output."""
|
|
24
|
+
pass
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Guardrails."""
|
|
2
|
+
|
|
3
|
+
from guardrails import Guard
|
|
4
|
+
from idun_agent_schema.engine.guardrails import Guardrail as GuardrailSchema
|
|
5
|
+
from idun_agent_schema.engine.guardrails_type import (
|
|
6
|
+
GuardrailType,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from ..base import BaseGuardrail
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_guard_instance(name: str) -> Guard:
|
|
13
|
+
"""Returns a map of guard type -> guard instance."""
|
|
14
|
+
if name == "BAN_LIST":
|
|
15
|
+
from guardrails.hub import BanList
|
|
16
|
+
|
|
17
|
+
return BanList
|
|
18
|
+
|
|
19
|
+
elif name == "NSFW":
|
|
20
|
+
from guardrails.hub import NSFWText
|
|
21
|
+
|
|
22
|
+
return NSFWText
|
|
23
|
+
|
|
24
|
+
elif name == "COMPETITOR_CHECK":
|
|
25
|
+
from guardrails.hub import CompetitorCheck
|
|
26
|
+
|
|
27
|
+
return CompetitorCheck
|
|
28
|
+
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError(f"Guard {name} not found.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GuardrailsHubGuard(BaseGuardrail):
|
|
34
|
+
"""Class for managing guardrails from `guardrailsai`'s hub."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: GuardrailSchema, position: str) -> None:
|
|
37
|
+
super().__init__(config)
|
|
38
|
+
|
|
39
|
+
self._guard_type = self._guardrail_config.type
|
|
40
|
+
self._guard_config = self._guardrail_config.config
|
|
41
|
+
|
|
42
|
+
if self._guard_type == GuardrailType.GUARDRAILS_HUB:
|
|
43
|
+
self._guard_url = self._guardrail_config.config["guard_url"]
|
|
44
|
+
|
|
45
|
+
self.reject_message: str = self._guard_config["reject_message"]
|
|
46
|
+
self._install_model()
|
|
47
|
+
self._guard: Guard | None = self.setup_guard()
|
|
48
|
+
self.position: str = position
|
|
49
|
+
|
|
50
|
+
def _install_model(self) -> None:
|
|
51
|
+
import subprocess
|
|
52
|
+
|
|
53
|
+
from guardrails import install
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
api_key = self._guardrail_config.config["api_key"]
|
|
57
|
+
subprocess.run(
|
|
58
|
+
[
|
|
59
|
+
"guardrails",
|
|
60
|
+
"configure",
|
|
61
|
+
"--token",
|
|
62
|
+
api_key,
|
|
63
|
+
"--disable-remote-inferencing", # TODO: maybe provide this as feat
|
|
64
|
+
"--disable-metrics",
|
|
65
|
+
],
|
|
66
|
+
check=True,
|
|
67
|
+
)
|
|
68
|
+
print(f"Installing model: {self._guard_url}..")
|
|
69
|
+
install(self._guard_url, quiet=True, install_local_models=True)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise OSError(f"Cannot install model {self._guard_url}: {e}") from e
|
|
72
|
+
|
|
73
|
+
def setup_guard(self) -> Guard | None:
|
|
74
|
+
"""Installs and configures the guard based on its yaml config."""
|
|
75
|
+
if self._guard_type == GuardrailType.GUARDRAILS_HUB:
|
|
76
|
+
self._install_model()
|
|
77
|
+
guard_name = self._guardrail_config.config.get("guard")
|
|
78
|
+
guard = get_guard_instance(guard_name)
|
|
79
|
+
if guard is None:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Guard: {self.guard_type} is not yet supported, or does not exist."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
guard_instance_params = self._guardrail_config.config.get(
|
|
85
|
+
"guard_config", {}
|
|
86
|
+
)
|
|
87
|
+
guard_instance = guard(**guard_instance_params)
|
|
88
|
+
for param, value in self._guardrail_config.config["guard_config"].items():
|
|
89
|
+
setattr(guard, param, value)
|
|
90
|
+
return guard_instance
|
|
91
|
+
elif self._guard_type == GuardrailType.CUSTOM_LLM:
|
|
92
|
+
raise NotImplementedError("Support for CUSTOM_LLM not yet provided.")
|
|
93
|
+
|
|
94
|
+
def validate(self, input: str) -> bool:
|
|
95
|
+
"""TODO."""
|
|
96
|
+
main_guard = Guard().use(self._guard)
|
|
97
|
+
try:
|
|
98
|
+
main_guard.validate(input)
|
|
99
|
+
return True
|
|
100
|
+
except Exception:
|
|
101
|
+
return False
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utils module."""
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
import yaml
|
|
4
|
+
import requests
|
|
5
|
+
import os
|
|
6
|
+
from idun_agent_engine.mcp.registry import MCPClientRegistry
|
|
7
|
+
from idun_agent_schema.engine.mcp_server import MCPServer
|
|
8
|
+
|
|
9
|
+
def _get_toolsets_from_data(config_data: dict[str, Any]) -> list[Any]:
|
|
10
|
+
"""Internal helper to extract toolsets from config dictionary."""
|
|
11
|
+
# Handle both snake_case and camelCase for mcp_servers
|
|
12
|
+
# Note: logic in ConfigBuilder suggests looking inside 'engine_config' if present,
|
|
13
|
+
# but this helper expects the dictionary containing 'mcp_servers' directly
|
|
14
|
+
# or performs the search itself.
|
|
15
|
+
|
|
16
|
+
mcp_configs_data = config_data.get("mcp_servers") or config_data.get("mcpServers")
|
|
17
|
+
|
|
18
|
+
if not mcp_configs_data:
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
mcp_configs = [MCPServer.model_validate(c) for c in mcp_configs_data]
|
|
22
|
+
registry = MCPClientRegistry(mcp_configs)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
return registry.get_adk_toolsets()
|
|
26
|
+
except ImportError:
|
|
27
|
+
raise
|
|
28
|
+
|
|
29
|
+
def get_adk_tools_from_file(config_path: str | Path) -> list[Any]:
|
|
30
|
+
"""
|
|
31
|
+
Loads MCP configurations from a YAML file and returns a list of ADK toolsets.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
config_path: Path to the configuration YAML file.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of initialized ADK McpToolset instances.
|
|
38
|
+
"""
|
|
39
|
+
path = Path(config_path)
|
|
40
|
+
if not path.exists():
|
|
41
|
+
raise FileNotFoundError(f"Configuration file not found at {path}")
|
|
42
|
+
|
|
43
|
+
with open(path) as f:
|
|
44
|
+
config_data = yaml.safe_load(f)
|
|
45
|
+
|
|
46
|
+
# Check if wrapped in engine_config (common pattern in idun)
|
|
47
|
+
if "engine_config" in config_data:
|
|
48
|
+
config_data = config_data["engine_config"]
|
|
49
|
+
|
|
50
|
+
return _get_toolsets_from_data(config_data)
|
|
51
|
+
|
|
52
|
+
def get_adk_tools_from_api() -> list[Any]:
|
|
53
|
+
"""
|
|
54
|
+
Fetches configuration from the Idun Manager API and returns a list of ADK toolsets.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
agent_api_key: The API key for authentication.
|
|
58
|
+
manager_url: The base URL of the Idun Manager (e.g. http://localhost:8000).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
List of initialized ADK McpToolset instances.
|
|
62
|
+
"""
|
|
63
|
+
api_key = os.environ.get("IDUN_AGENT_API_KEY")
|
|
64
|
+
manager_host = os.environ.get("IDUN_MANAGER_HOST")
|
|
65
|
+
headers = {"auth": f"Bearer {api_key}"}
|
|
66
|
+
url = f"{manager_host.rstrip('/')}/api/v1/agents/config"
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
response = requests.get(url=url, headers=headers)
|
|
70
|
+
response.raise_for_status()
|
|
71
|
+
|
|
72
|
+
config_data = yaml.safe_load(response.text)
|
|
73
|
+
|
|
74
|
+
# Config from API is typically wrapped in engine_config
|
|
75
|
+
if "engine_config" in config_data:
|
|
76
|
+
config_data = config_data["engine_config"]
|
|
77
|
+
|
|
78
|
+
return _get_toolsets_from_data(config_data)
|
|
79
|
+
|
|
80
|
+
except requests.RequestException as e:
|
|
81
|
+
raise ValueError(f"Failed to fetch config from API: {e}") from e
|
|
82
|
+
except yaml.YAMLError as e:
|
|
83
|
+
raise ValueError(f"Failed to parse config YAML: {e}") from e
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_adk_tools() -> list[Any]:
|
|
87
|
+
"""
|
|
88
|
+
Fetches configuration from the Idun Manager API and returns a list of ADK toolsets.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
agent_api_key: The API key for authentication.
|
|
92
|
+
manager_url: The base URL of the Idun Manager (e.g. http://localhost:8000).
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
List of initialized ADK McpToolset instances.
|
|
96
|
+
"""
|
|
97
|
+
return get_adk_tools_from_api()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Registry for MCP server clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, cast, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
|
8
|
+
from langchain_mcp_adapters.sessions import Connection
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from google.adk.tools import McpToolset
|
|
12
|
+
from mcp import StdioServerParameters
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from google.adk.tools import McpToolset
|
|
16
|
+
from mcp import StdioServerParameters
|
|
17
|
+
except ImportError:
|
|
18
|
+
McpToolset = None # type: ignore
|
|
19
|
+
StdioServerParameters = None # type: ignore
|
|
20
|
+
|
|
21
|
+
from idun_agent_schema.engine.mcp_server import MCPServer
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MCPClientRegistry:
|
|
25
|
+
"""Wraps `MultiServerMCPClient` with convenience helpers."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, configs: list[MCPServer] | None = None) -> None:
|
|
28
|
+
self._configs = configs or []
|
|
29
|
+
self._client: MultiServerMCPClient | None = None
|
|
30
|
+
|
|
31
|
+
if self._configs:
|
|
32
|
+
connections: dict[str, Connection] = {
|
|
33
|
+
config.name: cast(Connection, config.as_connection_dict())
|
|
34
|
+
for config in self._configs
|
|
35
|
+
}
|
|
36
|
+
self._client = MultiServerMCPClient(connections)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def enabled(self) -> bool:
|
|
40
|
+
"""Return True if at least one MCP server is configured."""
|
|
41
|
+
return self._client is not None
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def client(self) -> MultiServerMCPClient:
|
|
45
|
+
"""Return the underlying MultiServerMCPClient."""
|
|
46
|
+
if not self._client:
|
|
47
|
+
raise RuntimeError("No MCP servers configured.")
|
|
48
|
+
return self._client
|
|
49
|
+
|
|
50
|
+
def available_servers(self) -> list[str]:
|
|
51
|
+
"""Return the list of configured MCP server names."""
|
|
52
|
+
if not self._client:
|
|
53
|
+
return []
|
|
54
|
+
return list(self._client.connections.keys())
|
|
55
|
+
|
|
56
|
+
def _ensure_server(self, name: str) -> None:
|
|
57
|
+
if not self._client:
|
|
58
|
+
raise RuntimeError("MCP client registry is not enabled.")
|
|
59
|
+
if name not in self._client.connections:
|
|
60
|
+
available = ", ".join(self._client.connections.keys()) or "none"
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"MCP server '{name}' is not configured. Available: {available}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def get_client(self, name: str | None = None) -> MultiServerMCPClient:
|
|
66
|
+
"""Return the MCP client, optionally ensuring a named server exists."""
|
|
67
|
+
if name:
|
|
68
|
+
self._ensure_server(name)
|
|
69
|
+
return self.client
|
|
70
|
+
|
|
71
|
+
def get_session(self, name: str):
|
|
72
|
+
"""Return an async context manager for the given server session."""
|
|
73
|
+
self._ensure_server(name)
|
|
74
|
+
return self.client.session(name)
|
|
75
|
+
|
|
76
|
+
async def get_tools(self, name: str | None = None) -> list[Any]:
|
|
77
|
+
"""Load tools from all servers or a specific one."""
|
|
78
|
+
if not self._client:
|
|
79
|
+
raise RuntimeError("MCP client registry is not enabled.")
|
|
80
|
+
return await self._client.get_tools(server_name=name)
|
|
81
|
+
|
|
82
|
+
def get_adk_toolsets(self) -> list["McpToolset"]:
|
|
83
|
+
"""Return a list of Google ADK McpToolset instances for configured servers."""
|
|
84
|
+
if McpToolset is None or StdioServerParameters is None:
|
|
85
|
+
raise ImportError("google-adk and mcp packages are required for ADK toolsets.")
|
|
86
|
+
|
|
87
|
+
toolsets = []
|
|
88
|
+
for config in self._configs:
|
|
89
|
+
if config.transport == "stdio":
|
|
90
|
+
if not config.command:
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
server_params = StdioServerParameters(
|
|
94
|
+
command=config.command,
|
|
95
|
+
args=config.args,
|
|
96
|
+
env=config.env,
|
|
97
|
+
cwd=config.cwd,
|
|
98
|
+
encoding=config.encoding or "utf-8",
|
|
99
|
+
encoding_error_handler=config.encoding_error_handler or "strict",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
toolset = McpToolset(
|
|
103
|
+
# name=config.name,
|
|
104
|
+
connection_params=server_params
|
|
105
|
+
)
|
|
106
|
+
toolsets.append(toolset)
|
|
107
|
+
# TODO: Add support for SSE/HTTP transports when available in ADK/MCP
|
|
108
|
+
|
|
109
|
+
return toolsets
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Observability package providing provider-agnostic tracing interfaces."""
|
|
2
|
+
|
|
3
|
+
from .base import (
|
|
4
|
+
ObservabilityConfigV1,
|
|
5
|
+
ObservabilityConfigV2,
|
|
6
|
+
ObservabilityHandlerBase,
|
|
7
|
+
create_observability_handler,
|
|
8
|
+
create_observability_handlers,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ObservabilityConfigV1",
|
|
13
|
+
"ObservabilityConfigV2",
|
|
14
|
+
"ObservabilityHandlerBase",
|
|
15
|
+
"create_observability_handler",
|
|
16
|
+
"create_observability_handlers",
|
|
17
|
+
]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Observability base classes and factory functions.
|
|
2
|
+
|
|
3
|
+
Defines the provider-agnostic interface and a factory to create handlers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from idun_agent_schema.engine.observability import ObservabilityConfig as ObservabilityConfigV1
|
|
13
|
+
from idun_agent_schema.engine.observability_v2 import (
|
|
14
|
+
ObservabilityConfig as ObservabilityConfigV2,
|
|
15
|
+
ObservabilityProvider,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ObservabilityHandlerBase(ABC):
|
|
20
|
+
"""Abstract base class for observability handlers.
|
|
21
|
+
|
|
22
|
+
Concrete implementations must provide provider name and callbacks.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
provider: str
|
|
26
|
+
|
|
27
|
+
def __init__(self, options: dict[str, Any] | None = None) -> None:
|
|
28
|
+
"""Initialize handler with provider-specific options."""
|
|
29
|
+
self.options: dict[str, Any] = options or {}
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def get_callbacks(self) -> list[Any]:
|
|
33
|
+
"""Return a list of callbacks (can be empty)."""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
def get_run_name(self) -> str | None:
|
|
37
|
+
"""Optional run name used by frameworks that support it."""
|
|
38
|
+
run_name = self.options.get("run_name")
|
|
39
|
+
return run_name if isinstance(run_name, str) else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _normalize_config(
|
|
43
|
+
config: ObservabilityConfigV1 | ObservabilityConfigV2 | dict[str, Any] | None,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
if config is None:
|
|
46
|
+
return {"enabled": False}
|
|
47
|
+
|
|
48
|
+
if isinstance(config, ObservabilityConfigV2):
|
|
49
|
+
if not config.enabled:
|
|
50
|
+
return {"enabled": False}
|
|
51
|
+
|
|
52
|
+
provider = config.provider.value if hasattr(config.provider, "value") else config.provider
|
|
53
|
+
options = config.config.model_dump()
|
|
54
|
+
return {
|
|
55
|
+
"provider": provider,
|
|
56
|
+
"enabled": config.enabled,
|
|
57
|
+
"options": options,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if isinstance(config, ObservabilityConfigV1):
|
|
61
|
+
resolved = config.resolved()
|
|
62
|
+
return {
|
|
63
|
+
"provider": resolved.provider,
|
|
64
|
+
"enabled": resolved.enabled,
|
|
65
|
+
"options": resolved.options,
|
|
66
|
+
}
|
|
67
|
+
# Assume dict-like
|
|
68
|
+
provider = (config or {}).get("provider")
|
|
69
|
+
enabled = bool((config or {}).get("enabled", False))
|
|
70
|
+
options = dict((config or {}).get("options", {}))
|
|
71
|
+
return {"provider": provider, "enabled": enabled, "options": options}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create_observability_handler(
|
|
75
|
+
config: ObservabilityConfigV1 | ObservabilityConfigV2 | dict[str, Any] | None,
|
|
76
|
+
) -> tuple[ObservabilityHandlerBase | None, dict[str, Any] | None]:
|
|
77
|
+
"""Factory to create an observability handler based on provider.
|
|
78
|
+
|
|
79
|
+
Accepts either an `ObservabilityConfig` (V1 or V2) or a raw dict.
|
|
80
|
+
Returns (handler, info_dict). info_dict can be attached to agent infos for debugging.
|
|
81
|
+
"""
|
|
82
|
+
normalized = _normalize_config(config)
|
|
83
|
+
provider = normalized.get("provider")
|
|
84
|
+
enabled = normalized.get("enabled", False)
|
|
85
|
+
options: dict[str, Any] = normalized.get("options", {})
|
|
86
|
+
|
|
87
|
+
if not enabled or not provider:
|
|
88
|
+
return None, {"enabled": False}
|
|
89
|
+
|
|
90
|
+
# Ensure provider is string comparison
|
|
91
|
+
if hasattr(provider, "value"):
|
|
92
|
+
provider = provider.value
|
|
93
|
+
|
|
94
|
+
# Case-insensitive check for provider
|
|
95
|
+
provider_upper = str(provider).upper()
|
|
96
|
+
|
|
97
|
+
if provider_upper == ObservabilityProvider.LANGFUSE:
|
|
98
|
+
from .langfuse.langfuse_handler import LangfuseHandler
|
|
99
|
+
|
|
100
|
+
handler = LangfuseHandler(options)
|
|
101
|
+
return handler, {
|
|
102
|
+
"enabled": True,
|
|
103
|
+
"provider": "langfuse",
|
|
104
|
+
"host": os.getenv("LANGFUSE_BASE_URL"),
|
|
105
|
+
"run_name": handler.get_run_name(),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if provider_upper == ObservabilityProvider.PHOENIX:
|
|
109
|
+
from .phoenix.phoenix_handler import PhoenixHandler
|
|
110
|
+
|
|
111
|
+
handler = PhoenixHandler(options)
|
|
112
|
+
info: dict[str, Any] = {
|
|
113
|
+
"enabled": True,
|
|
114
|
+
"provider": "phoenix",
|
|
115
|
+
"collector": os.getenv("PHOENIX_COLLECTOR_ENDPOINT"),
|
|
116
|
+
}
|
|
117
|
+
project_name = getattr(handler, "project_name", None)
|
|
118
|
+
if project_name:
|
|
119
|
+
info["project_name"] = project_name
|
|
120
|
+
return handler, info
|
|
121
|
+
|
|
122
|
+
# if provider == "phoenix-local":
|
|
123
|
+
# from .phoenix_local.phoenix_local_handler import PhoenixLocalHandler
|
|
124
|
+
#
|
|
125
|
+
# handler = PhoenixLocalHandler(options)
|
|
126
|
+
# return handler, {
|
|
127
|
+
# "enabled": True,
|
|
128
|
+
# "provider": "phoenix-local",
|
|
129
|
+
# }
|
|
130
|
+
|
|
131
|
+
if provider_upper == ObservabilityProvider.GCP_LOGGING:
|
|
132
|
+
from .gcp_logging.gcp_logging_handler import GCPLoggingHandler
|
|
133
|
+
|
|
134
|
+
handler = GCPLoggingHandler(options)
|
|
135
|
+
return handler, {
|
|
136
|
+
"enabled": True,
|
|
137
|
+
"provider": "gcp_logging",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if provider_upper == ObservabilityProvider.GCP_TRACE:
|
|
141
|
+
from .gcp_trace.gcp_trace_handler import GCPTraceHandler
|
|
142
|
+
|
|
143
|
+
handler = GCPTraceHandler(options)
|
|
144
|
+
return handler, {
|
|
145
|
+
"enabled": True,
|
|
146
|
+
"provider": "gcp_trace",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return None, {
|
|
150
|
+
"enabled": False,
|
|
151
|
+
"provider": provider,
|
|
152
|
+
"error": "Unsupported provider",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
def create_observability_handlers(
|
|
156
|
+
configs: list[ObservabilityConfigV2 | ObservabilityConfigV1] | None,
|
|
157
|
+
) -> tuple[list[ObservabilityHandlerBase], list[dict[str, Any]]]:
|
|
158
|
+
"""Create multiple observability handlers from a list of configs."""
|
|
159
|
+
handlers = []
|
|
160
|
+
infos = []
|
|
161
|
+
|
|
162
|
+
if not configs:
|
|
163
|
+
return [], []
|
|
164
|
+
|
|
165
|
+
for config in configs:
|
|
166
|
+
handler, info = create_observability_handler(config)
|
|
167
|
+
if handler:
|
|
168
|
+
handlers.append(handler)
|
|
169
|
+
if info:
|
|
170
|
+
infos.append(info)
|
|
171
|
+
|
|
172
|
+
return handlers, infos
|
|
File without changes
|