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.
Files changed (41) hide show
  1. idun_agent_engine/_version.py +1 -1
  2. idun_agent_engine/agent/adk/__init__.py +5 -0
  3. idun_agent_engine/agent/adk/adk.py +296 -0
  4. idun_agent_engine/agent/base.py +7 -1
  5. idun_agent_engine/agent/haystack/haystack.py +5 -1
  6. idun_agent_engine/agent/langgraph/langgraph.py +146 -55
  7. idun_agent_engine/core/app_factory.py +9 -0
  8. idun_agent_engine/core/config_builder.py +214 -23
  9. idun_agent_engine/core/engine_config.py +1 -2
  10. idun_agent_engine/core/server_runner.py +2 -3
  11. idun_agent_engine/guardrails/__init__.py +0 -0
  12. idun_agent_engine/guardrails/base.py +24 -0
  13. idun_agent_engine/guardrails/guardrails_hub/guardrails_hub.py +101 -0
  14. idun_agent_engine/guardrails/guardrails_hub/utils.py +1 -0
  15. idun_agent_engine/mcp/__init__.py +5 -0
  16. idun_agent_engine/mcp/helpers.py +97 -0
  17. idun_agent_engine/mcp/registry.py +109 -0
  18. idun_agent_engine/observability/__init__.py +6 -2
  19. idun_agent_engine/observability/base.py +73 -12
  20. idun_agent_engine/observability/gcp_logging/__init__.py +0 -0
  21. idun_agent_engine/observability/gcp_logging/gcp_logging_handler.py +52 -0
  22. idun_agent_engine/observability/gcp_trace/__init__.py +0 -0
  23. idun_agent_engine/observability/gcp_trace/gcp_trace_handler.py +116 -0
  24. idun_agent_engine/observability/langfuse/langfuse_handler.py +17 -10
  25. idun_agent_engine/server/dependencies.py +13 -1
  26. idun_agent_engine/server/lifespan.py +83 -16
  27. idun_agent_engine/server/routers/agent.py +116 -24
  28. idun_agent_engine/server/routers/agui.py +47 -0
  29. idun_agent_engine/server/routers/base.py +55 -1
  30. idun_agent_engine/templates/__init__.py +1 -0
  31. idun_agent_engine/templates/correction.py +65 -0
  32. idun_agent_engine/templates/deep_research.py +40 -0
  33. idun_agent_engine/templates/translation.py +70 -0
  34. {idun_agent_engine-0.2.7.dist-info → idun_agent_engine-0.3.0.dist-info}/METADATA +62 -10
  35. idun_agent_engine-0.3.0.dist-info/RECORD +60 -0
  36. {idun_agent_engine-0.2.7.dist-info → idun_agent_engine-0.3.0.dist-info}/WHEEL +1 -1
  37. idun_platform_cli/groups/agent/package.py +3 -3
  38. idun_platform_cli/groups/agent/serve.py +8 -5
  39. idun_agent_engine/cli/__init__.py +0 -16
  40. idun_agent_engine-0.2.7.dist-info/RECORD +0 -43
  41. {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
- ObservabilityConfig,
4
+ ObservabilityConfigV1,
5
+ ObservabilityConfigV2,
5
6
  ObservabilityHandlerBase,
6
7
  create_observability_handler,
8
+ create_observability_handlers,
7
9
  )
8
10
 
9
11
  __all__ = [
10
- "ObservabilityConfig",
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: ObservabilityConfig | dict[str, Any] | None,
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
- if isinstance(config, ObservabilityConfig):
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: ObservabilityConfig | dict[str, Any] | None,
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
- if provider == "langfuse":
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("LANGFUSE_HOST"),
104
+ "host": os.getenv("LANGFUSE_BASE_URL"),
81
105
  "run_name": handler.get_run_name(),
82
106
  }
83
107
 
84
- if provider == "phoenix":
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
- from .phoenix_local.phoenix_local_handler import PhoenixLocalHandler
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
- handler = PhoenixLocalHandler(options)
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": "phoenix-local",
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
@@ -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("LANGFUSE_HOST")
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["LANGFUSE_HOST"] = host
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.callback import CallbackHandler
44
- from langfuse.client import Langfuse
43
+ from langfuse import get_client
44
+ from langfuse.langchain import CallbackHandler
45
45
 
46
- self._langfuse_client = Langfuse()
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
- pass
57
-
58
- self._callbacks = [CallbackHandler()]
59
- except Exception:
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
- @asynccontextmanager
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
- # Store both in app state
25
- app.state.agent = agent_instance
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
- agent_name = getattr(agent_instance, "name", "Unknown")
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
- yield
25
+ if not guardrails_obj.enabled:
26
+ return []
32
27
 
33
- # Clean up on shutdown
34
- print("🔄 Idun Agent Engine shutting down...")
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.")