idun-agent-engine 0.2.7__py3-none-any.whl → 0.3.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.
- idun_agent_engine/_version.py +1 -1
- idun_agent_engine/agent/adk/__init__.py +5 -0
- idun_agent_engine/agent/adk/adk.py +296 -0
- idun_agent_engine/agent/base.py +7 -1
- idun_agent_engine/agent/haystack/haystack.py +5 -1
- idun_agent_engine/agent/langgraph/langgraph.py +146 -55
- idun_agent_engine/core/app_factory.py +9 -0
- idun_agent_engine/core/config_builder.py +214 -23
- idun_agent_engine/core/engine_config.py +1 -2
- idun_agent_engine/core/server_runner.py +2 -3
- 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 +6 -2
- idun_agent_engine/observability/base.py +73 -12
- 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/langfuse_handler.py +17 -10
- idun_agent_engine/server/dependencies.py +13 -1
- idun_agent_engine/server/lifespan.py +83 -16
- idun_agent_engine/server/routers/agent.py +116 -24
- idun_agent_engine/server/routers/agui.py +47 -0
- idun_agent_engine/server/routers/base.py +55 -1
- 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.2.7.dist-info → idun_agent_engine-0.3.0.dist-info}/METADATA +62 -10
- idun_agent_engine-0.3.0.dist-info/RECORD +60 -0
- {idun_agent_engine-0.2.7.dist-info → idun_agent_engine-0.3.0.dist-info}/WHEEL +1 -1
- idun_platform_cli/groups/agent/package.py +3 -3
- idun_platform_cli/groups/agent/serve.py +8 -5
- idun_agent_engine/cli/__init__.py +0 -16
- idun_agent_engine-0.2.7.dist-info/RECORD +0 -43
- {idun_agent_engine-0.2.7.dist-info → idun_agent_engine-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"""Observability package providing provider-agnostic tracing interfaces."""
|
|
2
2
|
|
|
3
3
|
from .base import (
|
|
4
|
-
|
|
4
|
+
ObservabilityConfigV1,
|
|
5
|
+
ObservabilityConfigV2,
|
|
5
6
|
ObservabilityHandlerBase,
|
|
6
7
|
create_observability_handler,
|
|
8
|
+
create_observability_handlers,
|
|
7
9
|
)
|
|
8
10
|
|
|
9
11
|
__all__ = [
|
|
10
|
-
"
|
|
12
|
+
"ObservabilityConfigV1",
|
|
13
|
+
"ObservabilityConfigV2",
|
|
11
14
|
"ObservabilityHandlerBase",
|
|
12
15
|
"create_observability_handler",
|
|
16
|
+
"create_observability_handlers",
|
|
13
17
|
]
|
|
@@ -9,7 +9,11 @@ import os
|
|
|
9
9
|
from abc import ABC, abstractmethod
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
from idun_agent_schema.engine.observability import ObservabilityConfig
|
|
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
|
+
)
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class ObservabilityHandlerBase(ABC):
|
|
@@ -36,11 +40,24 @@ class ObservabilityHandlerBase(ABC):
|
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
def _normalize_config(
|
|
39
|
-
config:
|
|
43
|
+
config: ObservabilityConfigV1 | ObservabilityConfigV2 | dict[str, Any] | None,
|
|
40
44
|
) -> dict[str, Any]:
|
|
41
45
|
if config is None:
|
|
42
46
|
return {"enabled": False}
|
|
43
|
-
|
|
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):
|
|
44
61
|
resolved = config.resolved()
|
|
45
62
|
return {
|
|
46
63
|
"provider": resolved.provider,
|
|
@@ -55,11 +72,11 @@ def _normalize_config(
|
|
|
55
72
|
|
|
56
73
|
|
|
57
74
|
def create_observability_handler(
|
|
58
|
-
config:
|
|
75
|
+
config: ObservabilityConfigV1 | ObservabilityConfigV2 | dict[str, Any] | None,
|
|
59
76
|
) -> tuple[ObservabilityHandlerBase | None, dict[str, Any] | None]:
|
|
60
77
|
"""Factory to create an observability handler based on provider.
|
|
61
78
|
|
|
62
|
-
Accepts either an `ObservabilityConfig` or a raw dict.
|
|
79
|
+
Accepts either an `ObservabilityConfig` (V1 or V2) or a raw dict.
|
|
63
80
|
Returns (handler, info_dict). info_dict can be attached to agent infos for debugging.
|
|
64
81
|
"""
|
|
65
82
|
normalized = _normalize_config(config)
|
|
@@ -70,18 +87,25 @@ def create_observability_handler(
|
|
|
70
87
|
if not enabled or not provider:
|
|
71
88
|
return None, {"enabled": False}
|
|
72
89
|
|
|
73
|
-
|
|
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:
|
|
74
98
|
from .langfuse.langfuse_handler import LangfuseHandler
|
|
75
99
|
|
|
76
100
|
handler = LangfuseHandler(options)
|
|
77
101
|
return handler, {
|
|
78
102
|
"enabled": True,
|
|
79
103
|
"provider": "langfuse",
|
|
80
|
-
"host": os.getenv("
|
|
104
|
+
"host": os.getenv("LANGFUSE_BASE_URL"),
|
|
81
105
|
"run_name": handler.get_run_name(),
|
|
82
106
|
}
|
|
83
107
|
|
|
84
|
-
if
|
|
108
|
+
if provider_upper == ObservabilityProvider.PHOENIX:
|
|
85
109
|
from .phoenix.phoenix_handler import PhoenixHandler
|
|
86
110
|
|
|
87
111
|
handler = PhoenixHandler(options)
|
|
@@ -95,13 +119,31 @@ def create_observability_handler(
|
|
|
95
119
|
info["project_name"] = project_name
|
|
96
120
|
return handler, info
|
|
97
121
|
|
|
98
|
-
if provider == "phoenix-local":
|
|
99
|
-
|
|
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
|
+
# }
|
|
100
130
|
|
|
101
|
-
|
|
131
|
+
if provider_upper == ObservabilityProvider.GCP_LOGGING:
|
|
132
|
+
from .gcp_logging.gcp_logging_handler import GCPLoggingHandler
|
|
133
|
+
|
|
134
|
+
handler = GCPLoggingHandler(options)
|
|
102
135
|
return handler, {
|
|
103
136
|
"enabled": True,
|
|
104
|
-
"provider": "
|
|
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",
|
|
105
147
|
}
|
|
106
148
|
|
|
107
149
|
return None, {
|
|
@@ -109,3 +151,22 @@ def create_observability_handler(
|
|
|
109
151
|
"provider": provider,
|
|
110
152
|
"error": "Unsupported provider",
|
|
111
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
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""GCP Logging observability handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..base import ObservabilityHandlerBase
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GCPLoggingHandler(ObservabilityHandlerBase):
|
|
14
|
+
"""GCP Logging handler."""
|
|
15
|
+
|
|
16
|
+
provider = "gcp_logging"
|
|
17
|
+
|
|
18
|
+
def __init__(self, options: dict[str, Any] | None = None):
|
|
19
|
+
"""Initialize handler."""
|
|
20
|
+
super().__init__(options)
|
|
21
|
+
self.options = options or {}
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import google.cloud.logging
|
|
25
|
+
except ImportError as e:
|
|
26
|
+
logger.error("GCP Logging dependencies not found: %s", e)
|
|
27
|
+
raise ImportError(
|
|
28
|
+
"Please install 'google-cloud-logging' to use GCP Logging."
|
|
29
|
+
) from e
|
|
30
|
+
|
|
31
|
+
project_id = self.options.get("project_id")
|
|
32
|
+
# If project_id is explicitly provided, use it, otherwise client will auto-detect
|
|
33
|
+
if project_id:
|
|
34
|
+
client = google.cloud.logging.Client(project=project_id)
|
|
35
|
+
else:
|
|
36
|
+
client = google.cloud.logging.Client()
|
|
37
|
+
|
|
38
|
+
# Get logging configuration options
|
|
39
|
+
log_level = self.options.get("severity", "INFO").upper()
|
|
40
|
+
level = getattr(logging, log_level, logging.INFO)
|
|
41
|
+
|
|
42
|
+
# Setup logging handler
|
|
43
|
+
# This attaches a CloudLoggingHandler to the root python logger
|
|
44
|
+
client.setup_logging(log_level=level)
|
|
45
|
+
|
|
46
|
+
logger.info("GCP Logging initialized for project: %s", client.project)
|
|
47
|
+
|
|
48
|
+
def get_callbacks(self) -> list[Any]:
|
|
49
|
+
"""Return callbacks."""
|
|
50
|
+
# GCP Logging hooks into the standard python logging module,
|
|
51
|
+
# so no explicit LangChain callbacks are needed.
|
|
52
|
+
return []
|
|
File without changes
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""GCP Trace observability handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from opentelemetry import trace
|
|
9
|
+
from opentelemetry.sdk.resources import Resource
|
|
10
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
11
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
12
|
+
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
|
|
13
|
+
|
|
14
|
+
from ..base import ObservabilityHandlerBase
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GCPTraceHandler(ObservabilityHandlerBase):
|
|
20
|
+
"""GCP Trace handler."""
|
|
21
|
+
|
|
22
|
+
provider = "gcp_trace"
|
|
23
|
+
|
|
24
|
+
def __init__(self, options: dict[str, Any] | None = None):
|
|
25
|
+
"""Initialize handler."""
|
|
26
|
+
super().__init__(options)
|
|
27
|
+
self.options = options or {}
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from openinference.instrumentation.langchain import LangChainInstrumentor
|
|
31
|
+
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
|
|
32
|
+
except ImportError as e:
|
|
33
|
+
logger.error("GCP Trace dependencies not found: %s", e)
|
|
34
|
+
raise ImportError(
|
|
35
|
+
"Please install 'opentelemetry-exporter-gcp-trace' and 'openinference-instrumentation-langchain' to use GCP Trace."
|
|
36
|
+
) from e
|
|
37
|
+
|
|
38
|
+
project_id = self.options.get("project_id")
|
|
39
|
+
if not project_id:
|
|
40
|
+
project_id = None
|
|
41
|
+
|
|
42
|
+
# Initialize exporter
|
|
43
|
+
exporter = CloudTraceSpanExporter(
|
|
44
|
+
project_id=project_id,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Initialize sampler
|
|
48
|
+
sampling_rate = float(self.options.get("sampling_rate", 1.0))
|
|
49
|
+
sampler = TraceIdRatioBased(sampling_rate)
|
|
50
|
+
|
|
51
|
+
# Initialize resource
|
|
52
|
+
resource_attributes = {}
|
|
53
|
+
trace_name = self.options.get("trace_name")
|
|
54
|
+
if trace_name:
|
|
55
|
+
resource_attributes["service.name"] = trace_name
|
|
56
|
+
|
|
57
|
+
resource = Resource.create(resource_attributes)
|
|
58
|
+
|
|
59
|
+
# Initialize tracer provider
|
|
60
|
+
tracer_provider = TracerProvider(
|
|
61
|
+
sampler=sampler,
|
|
62
|
+
resource=resource,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Add span processor
|
|
66
|
+
flush_interval = int(self.options.get("flush_interval", 5))
|
|
67
|
+
span_processor = BatchSpanProcessor(
|
|
68
|
+
exporter, schedule_delay_millis=flush_interval * 1000
|
|
69
|
+
)
|
|
70
|
+
tracer_provider.add_span_processor(span_processor)
|
|
71
|
+
|
|
72
|
+
# Set global tracer provider
|
|
73
|
+
trace.set_tracer_provider(tracer_provider)
|
|
74
|
+
|
|
75
|
+
# Instrument LangChain with OpenInference
|
|
76
|
+
LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
77
|
+
|
|
78
|
+
# Instrument Guardrails
|
|
79
|
+
try:
|
|
80
|
+
from openinference.instrumentation.guardrails import GuardrailsInstrumentor
|
|
81
|
+
|
|
82
|
+
GuardrailsInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
83
|
+
except ImportError:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# Instrument VertexAI
|
|
87
|
+
try:
|
|
88
|
+
from openinference.instrumentation.vertexai import VertexAIInstrumentor
|
|
89
|
+
|
|
90
|
+
VertexAIInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
91
|
+
except ImportError:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
# TODO: GCP GoogleADKInstrumentor is n conflist with langfuse, so we don't need to instrument it here
|
|
95
|
+
# Instrument Google ADK
|
|
96
|
+
# try:
|
|
97
|
+
# from openinference.instrumentation.google_adk import GoogleADKInstrumentor
|
|
98
|
+
|
|
99
|
+
# GoogleADKInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
100
|
+
# except ImportError:
|
|
101
|
+
# pass
|
|
102
|
+
|
|
103
|
+
# Instrument MCP
|
|
104
|
+
try:
|
|
105
|
+
from openinference.instrumentation.mcp import MCPInstrumentor
|
|
106
|
+
|
|
107
|
+
MCPInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
108
|
+
except ImportError:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
logger.info("GCP Trace initialized for project: %s", project_id or "auto-detected")
|
|
112
|
+
|
|
113
|
+
def get_callbacks(self) -> list[Any]:
|
|
114
|
+
"""Return callbacks."""
|
|
115
|
+
# OpenTelemetry instrumentation uses global tracer provider, so no explicit callbacks needed here
|
|
116
|
+
return []
|
|
@@ -21,7 +21,7 @@ class LangfuseHandler(ObservabilityHandlerBase):
|
|
|
21
21
|
opts = self.options
|
|
22
22
|
|
|
23
23
|
# Resolve and set env vars as required by Langfuse
|
|
24
|
-
host = self._resolve_env(opts.get("host")) or os.getenv("
|
|
24
|
+
host = self._resolve_env(opts.get("host")) or os.getenv("LANGFUSE_BASE_URL")
|
|
25
25
|
public_key = self._resolve_env(opts.get("public_key")) or os.getenv(
|
|
26
26
|
"LANGFUSE_PUBLIC_KEY"
|
|
27
27
|
)
|
|
@@ -30,7 +30,7 @@ class LangfuseHandler(ObservabilityHandlerBase):
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
if host:
|
|
33
|
-
os.environ["
|
|
33
|
+
os.environ["LANGFUSE_BASE_URL"] = host
|
|
34
34
|
if public_key:
|
|
35
35
|
os.environ["LANGFUSE_PUBLIC_KEY"] = public_key
|
|
36
36
|
if secret_key:
|
|
@@ -40,10 +40,11 @@ class LangfuseHandler(ObservabilityHandlerBase):
|
|
|
40
40
|
self._callbacks: list[Any] = []
|
|
41
41
|
self._langfuse_client = None
|
|
42
42
|
try:
|
|
43
|
-
from langfuse
|
|
44
|
-
from langfuse.
|
|
43
|
+
from langfuse import get_client
|
|
44
|
+
from langfuse.langchain import CallbackHandler
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
# Initialize client for auth check
|
|
47
|
+
self._langfuse_client = get_client()
|
|
47
48
|
|
|
48
49
|
try:
|
|
49
50
|
if self._langfuse_client.auth_check():
|
|
@@ -52,11 +53,17 @@ class LangfuseHandler(ObservabilityHandlerBase):
|
|
|
52
53
|
print(
|
|
53
54
|
"Authentication failed. Please check your credentials and host."
|
|
54
55
|
)
|
|
55
|
-
except Exception:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"Error during Langfuse client authentication: {e}")
|
|
58
|
+
|
|
59
|
+
# Initialize callback handler
|
|
60
|
+
# We pass the resolved credentials explicitly to ensure they are used
|
|
61
|
+
# even if env vars were not successfully set or read.
|
|
62
|
+
self._callbacks = [
|
|
63
|
+
CallbackHandler()
|
|
64
|
+
]
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(f"Failed to initialize Langfuse callback/client: {e}")
|
|
60
67
|
self._callbacks = []
|
|
61
68
|
|
|
62
69
|
@staticmethod
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""Dependency injection helpers for FastAPI routes."""
|
|
2
2
|
|
|
3
|
-
from fastapi import Request
|
|
3
|
+
from fastapi import HTTPException, Request, status
|
|
4
4
|
|
|
5
5
|
from ..core.config_builder import ConfigBuilder
|
|
6
|
+
from ..mcp import MCPClientRegistry
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
async def get_agent(request: Request):
|
|
@@ -38,3 +39,14 @@ async def get_copilotkit_agent(request: Request):
|
|
|
38
39
|
app_config = ConfigBuilder.load_from_file()
|
|
39
40
|
copilotkit_agent = await ConfigBuilder.initialize_agent_from_config(app_config)
|
|
40
41
|
return copilotkit_agent
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_mcp_registry(request: Request) -> MCPClientRegistry:
|
|
45
|
+
"""Return the configured MCP registry if available."""
|
|
46
|
+
registry: MCPClientRegistry | None = getattr(request.app.state, "mcp_registry", None)
|
|
47
|
+
if registry is None or not registry.enabled:
|
|
48
|
+
raise HTTPException(
|
|
49
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
50
|
+
detail="MCP servers are not configured for this engine.",
|
|
51
|
+
)
|
|
52
|
+
return registry
|
|
@@ -4,34 +4,34 @@ Initializes the agent at startup and cleans up resources on shutdown.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import inspect
|
|
7
|
+
from collections.abc import Sequence
|
|
7
8
|
from contextlib import asynccontextmanager
|
|
8
9
|
|
|
9
10
|
from fastapi import FastAPI
|
|
10
11
|
|
|
11
12
|
from ..core.config_builder import ConfigBuilder
|
|
13
|
+
from ..mcp import MCPClientRegistry
|
|
12
14
|
|
|
15
|
+
from idun_agent_schema.engine.guardrails import Guardrails, Guardrail
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
async def lifespan(app: FastAPI):
|
|
16
|
-
"""FastAPI lifespan context to initialize and teardown the agent."""
|
|
17
|
-
# Load config and initialize agent on startup
|
|
18
|
-
print("Server starting up...")
|
|
19
|
-
engine_config = app.state.engine_config
|
|
17
|
+
from ..guardrails.base import BaseGuardrail
|
|
20
18
|
|
|
21
|
-
# Use ConfigBuilder's centralized agent initialization
|
|
22
|
-
agent_instance = await ConfigBuilder.initialize_agent_from_config(engine_config)
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
app.state.config = engine_config
|
|
20
|
+
def _parse_guardrails(guardrails_obj: Guardrails) -> Sequence[BaseGuardrail]:
|
|
21
|
+
"""Adds the position of the guardrails (input/output) and returns the lift of updated guardrails."""
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
print(f"✅ Agent '{agent_name}' initialized and ready to serve!")
|
|
23
|
+
from ..guardrails.guardrails_hub.guardrails_hub import GuardrailsHubGuard as GHGuard
|
|
30
24
|
|
|
31
|
-
|
|
25
|
+
if not guardrails_obj.enabled:
|
|
26
|
+
return []
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
return [GHGuard(guard, position="input") for guard in guardrails_obj.input] + [
|
|
29
|
+
GHGuard(guard, position="output") for guard in guardrails_obj.output
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def cleanup_agent(app: FastAPI):
|
|
34
|
+
"""Clean up agent resources."""
|
|
35
35
|
agent = getattr(app.state, "agent", None)
|
|
36
36
|
if agent is not None:
|
|
37
37
|
close_fn = getattr(agent, "close", None)
|
|
@@ -39,4 +39,71 @@ async def lifespan(app: FastAPI):
|
|
|
39
39
|
result = close_fn()
|
|
40
40
|
if inspect.isawaitable(result):
|
|
41
41
|
await result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def configure_app(app: FastAPI, engine_config):
|
|
45
|
+
"""Initialize the agent, MCP registry, guardrails, and app state with the given engine config."""
|
|
46
|
+
guardrails_obj = engine_config.guardrails
|
|
47
|
+
guardrails = _parse_guardrails(guardrails_obj) if guardrails_obj else []
|
|
48
|
+
|
|
49
|
+
print("guardrails: ", guardrails)
|
|
50
|
+
|
|
51
|
+
# # Initialize MCP Registry first
|
|
52
|
+
# mcp_registry = MCPClientRegistry(engine_config.mcp_servers)
|
|
53
|
+
# app.state.mcp_registry = mcp_registry
|
|
54
|
+
|
|
55
|
+
# Use ConfigBuilder's centralized agent initialization, passing the registry
|
|
56
|
+
try:
|
|
57
|
+
agent_instance = await ConfigBuilder.initialize_agent_from_config(
|
|
58
|
+
engine_config
|
|
59
|
+
)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Error retrieving agent instance from ConfigBuilder: {e}"
|
|
63
|
+
) from e
|
|
64
|
+
|
|
65
|
+
app.state.agent = agent_instance
|
|
66
|
+
app.state.config = engine_config
|
|
67
|
+
app.state.engine_config = engine_config
|
|
68
|
+
|
|
69
|
+
app.state.guardrails = guardrails # TODO: to reactivate
|
|
70
|
+
agent_name = getattr(agent_instance, "name", "Unknown")
|
|
71
|
+
print(f"✅ Agent '{agent_name}' initialized and ready to serve!")
|
|
72
|
+
|
|
73
|
+
# Setup AGUI routes if the agent is a LangGraph agent
|
|
74
|
+
from ..agent.langgraph.langgraph import LanggraphAgent
|
|
75
|
+
from ..agent.adk.adk import AdkAgent
|
|
76
|
+
# from ..server.routers.agui import setup_agui_router
|
|
77
|
+
|
|
78
|
+
if isinstance(agent_instance, (LanggraphAgent, AdkAgent)):
|
|
79
|
+
try:
|
|
80
|
+
# compiled_graph = getattr(agent_instance, "agent_instance")
|
|
81
|
+
# app.state.copilotkit_agent = setup_agui_router(app, agent_instance) # TODO: agent_instance is a compiled graph (duplicate agent_instance name not clear)
|
|
82
|
+
app.state.copilotkit_agent = agent_instance.copilotkit_agent_instance
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"⚠️ Warning: Failed to setup AGUI routes: {e}")
|
|
85
|
+
# Continue even if AGUI setup fails
|
|
86
|
+
|
|
87
|
+
# if app.state.mcp_registry.enabled:
|
|
88
|
+
# servers = ", ".join(app.state.mcp_registry.available_servers())
|
|
89
|
+
# print(f"🔌 MCP servers ready: {servers}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@asynccontextmanager
|
|
94
|
+
async def lifespan(app: FastAPI):
|
|
95
|
+
"""FastAPI lifespan context to initialize and teardown the agent."""
|
|
96
|
+
|
|
97
|
+
# Load config and initialize agent on startup
|
|
98
|
+
print("Server starting up...")
|
|
99
|
+
if not app.state.engine_config:
|
|
100
|
+
raise ValueError("Error: No Engine configuration found.")
|
|
101
|
+
|
|
102
|
+
await configure_app(app, app.state.engine_config)
|
|
103
|
+
|
|
104
|
+
yield
|
|
105
|
+
|
|
106
|
+
# Clean up on shutdown
|
|
107
|
+
print("🔄 Idun Agent Engine shutting down...")
|
|
108
|
+
await cleanup_agent(app)
|
|
42
109
|
print("✅ Agent resources cleaned up successfully.")
|