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.
Files changed (60) hide show
  1. idun_agent_engine/__init__.py +24 -0
  2. idun_agent_engine/_version.py +3 -0
  3. idun_agent_engine/agent/__init__.py +10 -0
  4. idun_agent_engine/agent/adk/__init__.py +5 -0
  5. idun_agent_engine/agent/adk/adk.py +296 -0
  6. idun_agent_engine/agent/base.py +112 -0
  7. idun_agent_engine/agent/haystack/__init__.py +9 -0
  8. idun_agent_engine/agent/haystack/haystack.py +274 -0
  9. idun_agent_engine/agent/haystack/haystack_model.py +13 -0
  10. idun_agent_engine/agent/haystack/utils.py +13 -0
  11. idun_agent_engine/agent/langgraph/__init__.py +7 -0
  12. idun_agent_engine/agent/langgraph/langgraph.py +553 -0
  13. idun_agent_engine/core/__init__.py +11 -0
  14. idun_agent_engine/core/app_factory.py +73 -0
  15. idun_agent_engine/core/config_builder.py +657 -0
  16. idun_agent_engine/core/engine_config.py +21 -0
  17. idun_agent_engine/core/server_runner.py +145 -0
  18. idun_agent_engine/guardrails/__init__.py +0 -0
  19. idun_agent_engine/guardrails/base.py +24 -0
  20. idun_agent_engine/guardrails/guardrails_hub/guardrails_hub.py +101 -0
  21. idun_agent_engine/guardrails/guardrails_hub/utils.py +1 -0
  22. idun_agent_engine/mcp/__init__.py +5 -0
  23. idun_agent_engine/mcp/helpers.py +97 -0
  24. idun_agent_engine/mcp/registry.py +109 -0
  25. idun_agent_engine/observability/__init__.py +17 -0
  26. idun_agent_engine/observability/base.py +172 -0
  27. idun_agent_engine/observability/gcp_logging/__init__.py +0 -0
  28. idun_agent_engine/observability/gcp_logging/gcp_logging_handler.py +52 -0
  29. idun_agent_engine/observability/gcp_trace/__init__.py +0 -0
  30. idun_agent_engine/observability/gcp_trace/gcp_trace_handler.py +116 -0
  31. idun_agent_engine/observability/langfuse/__init__.py +5 -0
  32. idun_agent_engine/observability/langfuse/langfuse_handler.py +79 -0
  33. idun_agent_engine/observability/phoenix/__init__.py +5 -0
  34. idun_agent_engine/observability/phoenix/phoenix_handler.py +65 -0
  35. idun_agent_engine/observability/phoenix_local/__init__.py +5 -0
  36. idun_agent_engine/observability/phoenix_local/phoenix_local_handler.py +123 -0
  37. idun_agent_engine/py.typed +0 -0
  38. idun_agent_engine/server/__init__.py +5 -0
  39. idun_agent_engine/server/dependencies.py +52 -0
  40. idun_agent_engine/server/lifespan.py +106 -0
  41. idun_agent_engine/server/routers/__init__.py +5 -0
  42. idun_agent_engine/server/routers/agent.py +204 -0
  43. idun_agent_engine/server/routers/agui.py +47 -0
  44. idun_agent_engine/server/routers/base.py +114 -0
  45. idun_agent_engine/server/server_config.py +8 -0
  46. idun_agent_engine/templates/__init__.py +1 -0
  47. idun_agent_engine/templates/correction.py +65 -0
  48. idun_agent_engine/templates/deep_research.py +40 -0
  49. idun_agent_engine/templates/translation.py +70 -0
  50. idun_agent_engine-0.3.4.dist-info/METADATA +335 -0
  51. idun_agent_engine-0.3.4.dist-info/RECORD +60 -0
  52. idun_agent_engine-0.3.4.dist-info/WHEEL +4 -0
  53. idun_agent_engine-0.3.4.dist-info/entry_points.txt +2 -0
  54. idun_platform_cli/__init__.py +0 -0
  55. idun_platform_cli/groups/__init__.py +0 -0
  56. idun_platform_cli/groups/agent/__init__.py +0 -0
  57. idun_platform_cli/groups/agent/main.py +16 -0
  58. idun_platform_cli/groups/agent/package.py +70 -0
  59. idun_platform_cli/groups/agent/serve.py +107 -0
  60. 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,5 @@
1
+ """MCP utilities for Idun Agent Engine."""
2
+
3
+ from .registry import MCPClientRegistry
4
+ from .helpers import get_adk_tools_from_api, get_adk_tools_from_file
5
+ __all__ = ["MCPClientRegistry", "get_adk_tools_from_api", "get_adk_tools_from_file", "get_adk_tools"]
@@ -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