idun-agent-engine 0.1.0__py3-none-any.whl → 0.2.1__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 +2 -25
- idun_agent_engine/_version.py +1 -1
- idun_agent_engine/agent/__init__.py +10 -0
- idun_agent_engine/agent/base.py +97 -0
- idun_agent_engine/agent/haystack/__init__.py +9 -0
- idun_agent_engine/agent/haystack/haystack.py +261 -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 +429 -0
- idun_agent_engine/cli/__init__.py +16 -0
- idun_agent_engine/core/__init__.py +11 -0
- idun_agent_engine/core/app_factory.py +63 -0
- idun_agent_engine/core/config_builder.py +456 -0
- idun_agent_engine/core/engine_config.py +22 -0
- idun_agent_engine/core/server_runner.py +146 -0
- idun_agent_engine/observability/__init__.py +13 -0
- idun_agent_engine/observability/base.py +111 -0
- idun_agent_engine/observability/langfuse/__init__.py +5 -0
- idun_agent_engine/observability/langfuse/langfuse_handler.py +72 -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 -1
- idun_agent_engine/server/__init__.py +5 -0
- idun_agent_engine/server/dependencies.py +23 -0
- idun_agent_engine/server/lifespan.py +42 -0
- idun_agent_engine/server/routers/__init__.py +5 -0
- idun_agent_engine/server/routers/agent.py +68 -0
- idun_agent_engine/server/routers/base.py +60 -0
- idun_agent_engine/server/server_config.py +8 -0
- idun_agent_engine-0.2.1.dist-info/METADATA +278 -0
- idun_agent_engine-0.2.1.dist-info/RECORD +35 -0
- {idun_agent_engine-0.1.0.dist-info → idun_agent_engine-0.2.1.dist-info}/WHEEL +1 -1
- idun_agent_engine-0.1.0.dist-info/METADATA +0 -317
- idun_agent_engine-0.1.0.dist-info/RECORD +0 -6
|
@@ -0,0 +1,111 @@
|
|
|
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
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ObservabilityHandlerBase(ABC):
|
|
16
|
+
"""Abstract base class for observability handlers.
|
|
17
|
+
|
|
18
|
+
Concrete implementations must provide provider name and callbacks.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
provider: str
|
|
22
|
+
|
|
23
|
+
def __init__(self, options: dict[str, Any] | None = None) -> None:
|
|
24
|
+
"""Initialize handler with provider-specific options."""
|
|
25
|
+
self.options: dict[str, Any] = options or {}
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def get_callbacks(self) -> list[Any]:
|
|
29
|
+
"""Return a list of callbacks (can be empty)."""
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
def get_run_name(self) -> str | None:
|
|
33
|
+
"""Optional run name used by frameworks that support it."""
|
|
34
|
+
run_name = self.options.get("run_name")
|
|
35
|
+
return run_name if isinstance(run_name, str) else None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _normalize_config(
|
|
39
|
+
config: ObservabilityConfig | dict[str, Any] | None,
|
|
40
|
+
) -> dict[str, Any]:
|
|
41
|
+
if config is None:
|
|
42
|
+
return {"enabled": False}
|
|
43
|
+
if isinstance(config, ObservabilityConfig):
|
|
44
|
+
resolved = config.resolved()
|
|
45
|
+
return {
|
|
46
|
+
"provider": resolved.provider,
|
|
47
|
+
"enabled": resolved.enabled,
|
|
48
|
+
"options": resolved.options,
|
|
49
|
+
}
|
|
50
|
+
# Assume dict-like
|
|
51
|
+
provider = (config or {}).get("provider")
|
|
52
|
+
enabled = bool((config or {}).get("enabled", False))
|
|
53
|
+
options = dict((config or {}).get("options", {}))
|
|
54
|
+
return {"provider": provider, "enabled": enabled, "options": options}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_observability_handler(
|
|
58
|
+
config: ObservabilityConfig | dict[str, Any] | None,
|
|
59
|
+
) -> tuple[ObservabilityHandlerBase | None, dict[str, Any] | None]:
|
|
60
|
+
"""Factory to create an observability handler based on provider.
|
|
61
|
+
|
|
62
|
+
Accepts either an `ObservabilityConfig` or a raw dict.
|
|
63
|
+
Returns (handler, info_dict). info_dict can be attached to agent infos for debugging.
|
|
64
|
+
"""
|
|
65
|
+
normalized = _normalize_config(config)
|
|
66
|
+
provider = normalized.get("provider")
|
|
67
|
+
enabled = normalized.get("enabled", False)
|
|
68
|
+
options: dict[str, Any] = normalized.get("options", {})
|
|
69
|
+
|
|
70
|
+
if not enabled or not provider:
|
|
71
|
+
return None, {"enabled": False}
|
|
72
|
+
|
|
73
|
+
if provider == "langfuse":
|
|
74
|
+
from .langfuse.langfuse_handler import LangfuseHandler
|
|
75
|
+
|
|
76
|
+
handler = LangfuseHandler(options)
|
|
77
|
+
return handler, {
|
|
78
|
+
"enabled": True,
|
|
79
|
+
"provider": "langfuse",
|
|
80
|
+
"host": os.getenv("LANGFUSE_HOST"),
|
|
81
|
+
"run_name": handler.get_run_name(),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if provider == "phoenix":
|
|
85
|
+
from .phoenix.phoenix_handler import PhoenixHandler
|
|
86
|
+
|
|
87
|
+
handler = PhoenixHandler(options)
|
|
88
|
+
info: dict[str, Any] = {
|
|
89
|
+
"enabled": True,
|
|
90
|
+
"provider": "phoenix",
|
|
91
|
+
"collector": os.getenv("PHOENIX_COLLECTOR_ENDPOINT"),
|
|
92
|
+
}
|
|
93
|
+
project_name = getattr(handler, "project_name", None)
|
|
94
|
+
if project_name:
|
|
95
|
+
info["project_name"] = project_name
|
|
96
|
+
return handler, info
|
|
97
|
+
|
|
98
|
+
if provider == "phoenix-local":
|
|
99
|
+
from .phoenix_local.phoenix_local_handler import PhoenixLocalHandler
|
|
100
|
+
|
|
101
|
+
handler = PhoenixLocalHandler(options)
|
|
102
|
+
return handler, {
|
|
103
|
+
"enabled": True,
|
|
104
|
+
"provider": "phoenix-local",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return None, {
|
|
108
|
+
"enabled": False,
|
|
109
|
+
"provider": provider,
|
|
110
|
+
"error": "Unsupported provider",
|
|
111
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Langfuse observability handler implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from idun_agent_schema.engine.observability import _resolve_env
|
|
9
|
+
|
|
10
|
+
from ..base import ObservabilityHandlerBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LangfuseHandler(ObservabilityHandlerBase):
|
|
14
|
+
"""Langfuse handler providing LangChain callbacks and client setup."""
|
|
15
|
+
|
|
16
|
+
provider = "langfuse"
|
|
17
|
+
|
|
18
|
+
def __init__(self, options: dict[str, Any] | None = None):
|
|
19
|
+
"""Initialize handler, resolving env and preparing callbacks."""
|
|
20
|
+
super().__init__(options)
|
|
21
|
+
opts = self.options
|
|
22
|
+
|
|
23
|
+
# Resolve and set env vars as required by Langfuse
|
|
24
|
+
host = self._resolve_env(opts.get("host")) or os.getenv("LANGFUSE_HOST")
|
|
25
|
+
public_key = self._resolve_env(opts.get("public_key")) or os.getenv(
|
|
26
|
+
"LANGFUSE_PUBLIC_KEY"
|
|
27
|
+
)
|
|
28
|
+
secret_key = self._resolve_env(opts.get("secret_key")) or os.getenv(
|
|
29
|
+
"LANGFUSE_SECRET_KEY"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if host:
|
|
33
|
+
os.environ["LANGFUSE_HOST"] = host
|
|
34
|
+
if public_key:
|
|
35
|
+
os.environ["LANGFUSE_PUBLIC_KEY"] = public_key
|
|
36
|
+
if secret_key:
|
|
37
|
+
os.environ["LANGFUSE_SECRET_KEY"] = secret_key
|
|
38
|
+
|
|
39
|
+
# Instantiate callback handler lazily to avoid hard dep if not installed
|
|
40
|
+
self._callbacks: list[Any] = []
|
|
41
|
+
self._langfuse_client = None
|
|
42
|
+
try:
|
|
43
|
+
from langfuse.callback import CallbackHandler
|
|
44
|
+
from langfuse.client import Langfuse
|
|
45
|
+
|
|
46
|
+
self._langfuse_client = Langfuse()
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
if self._langfuse_client.auth_check():
|
|
50
|
+
print("Langfuse client is authenticated and ready!")
|
|
51
|
+
else:
|
|
52
|
+
print(
|
|
53
|
+
"Authentication failed. Please check your credentials and host."
|
|
54
|
+
)
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
self._callbacks = [CallbackHandler()]
|
|
59
|
+
except Exception:
|
|
60
|
+
self._callbacks = []
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _resolve_env(value: str | None) -> str | None:
|
|
64
|
+
return _resolve_env(value)
|
|
65
|
+
|
|
66
|
+
def get_callbacks(self) -> list[Any]:
|
|
67
|
+
"""Return LangChain-compatible callback handlers (if available)."""
|
|
68
|
+
return self._callbacks
|
|
69
|
+
|
|
70
|
+
def get_client(self):
|
|
71
|
+
"""Return underlying Langfuse client instance (if created)."""
|
|
72
|
+
return self._langfuse_client
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Phoenix observability handler implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from idun_agent_schema.engine.observability import _resolve_env
|
|
9
|
+
|
|
10
|
+
from ..base import ObservabilityHandlerBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PhoenixHandler(ObservabilityHandlerBase):
|
|
14
|
+
"""Phoenix handler configuring OpenTelemetry and LangChain instrumentation."""
|
|
15
|
+
|
|
16
|
+
provider = "phoenix"
|
|
17
|
+
|
|
18
|
+
def __init__(self, options: dict[str, Any] | None = None):
|
|
19
|
+
"""Initialize handler, resolving env and setting up instrumentation."""
|
|
20
|
+
super().__init__(options)
|
|
21
|
+
opts = self.options
|
|
22
|
+
|
|
23
|
+
# Resolve and set env vars as required by Phoenix
|
|
24
|
+
api_key = self._resolve_env(opts.get("api_key")) or os.getenv("PHOENIX_API_KEY")
|
|
25
|
+
collector = (
|
|
26
|
+
self._resolve_env(opts.get("collector"))
|
|
27
|
+
or self._resolve_env(opts.get("collector_endpoint"))
|
|
28
|
+
or os.getenv("PHOENIX_COLLECTOR_ENDPOINT")
|
|
29
|
+
)
|
|
30
|
+
self.project_name: str = opts.get("project_name") or "default"
|
|
31
|
+
|
|
32
|
+
if api_key:
|
|
33
|
+
os.environ["PHOENIX_API_KEY"] = api_key
|
|
34
|
+
if collector:
|
|
35
|
+
os.environ["PHOENIX_COLLECTOR_ENDPOINT"] = collector
|
|
36
|
+
|
|
37
|
+
# Some older Phoenix deployments (before 2025-06-24) require setting client headers.
|
|
38
|
+
# If not explicitly provided, set it from API key when available for backward compatibility.
|
|
39
|
+
client_headers = opts.get("client_headers")
|
|
40
|
+
if isinstance(client_headers, str) and client_headers:
|
|
41
|
+
os.environ["PHOENIX_CLIENT_HEADERS"] = client_headers
|
|
42
|
+
elif api_key and not os.getenv("PHOENIX_CLIENT_HEADERS"):
|
|
43
|
+
os.environ["PHOENIX_CLIENT_HEADERS"] = f"api_key={api_key}"
|
|
44
|
+
|
|
45
|
+
# Configure tracer provider using phoenix.otel.register
|
|
46
|
+
self._callbacks: list[Any] = []
|
|
47
|
+
try:
|
|
48
|
+
from openinference.instrumentation.langchain import LangChainInstrumentor
|
|
49
|
+
from phoenix.otel import register # type: ignore
|
|
50
|
+
|
|
51
|
+
tracer_provider = register(
|
|
52
|
+
project_name=self.project_name, auto_instrument=True
|
|
53
|
+
)
|
|
54
|
+
LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
55
|
+
except Exception:
|
|
56
|
+
# Silent failure; user may not have phoenix installed
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _resolve_env(value: str | None) -> str | None:
|
|
61
|
+
return _resolve_env(value)
|
|
62
|
+
|
|
63
|
+
def get_callbacks(self) -> list[Any]:
|
|
64
|
+
"""Return callbacks (Phoenix instruments globally; this may be empty)."""
|
|
65
|
+
return self._callbacks
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Phoenix observability handler implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from idun_agent_schema.engine.observability import _resolve_env
|
|
12
|
+
|
|
13
|
+
from ..base import ObservabilityHandlerBase
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PhoenixLocalHandler(ObservabilityHandlerBase):
|
|
19
|
+
"""Phoenix handler configuring OpenTelemetry and LangChain instrumentation."""
|
|
20
|
+
|
|
21
|
+
provider = "phoenix-local"
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
options: dict[str, Any] | None = None,
|
|
26
|
+
default_endpoint: str = "http://127.0.0.1:6006",
|
|
27
|
+
):
|
|
28
|
+
"""Initialize handler, start Phoenix via CLI, and set up instrumentation.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
options: Configuration options dictionary
|
|
32
|
+
default_endpoint: Default Phoenix collector endpoint URL
|
|
33
|
+
"""
|
|
34
|
+
logger.info("Initializing PhoenixLocalHandler")
|
|
35
|
+
|
|
36
|
+
super().__init__(options)
|
|
37
|
+
opts = self.options or {}
|
|
38
|
+
|
|
39
|
+
# Initialize instance variables
|
|
40
|
+
self._callbacks: list[Any] = []
|
|
41
|
+
self._proc: subprocess.Popen[bytes] | None = None
|
|
42
|
+
self.default_endpoint = default_endpoint
|
|
43
|
+
self.project_name: str = "default"
|
|
44
|
+
|
|
45
|
+
self._configure_collector_endpoint(opts)
|
|
46
|
+
self._start_phoenix_cli()
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from openinference.instrumentation.langchain import LangChainInstrumentor
|
|
50
|
+
from phoenix.otel import register
|
|
51
|
+
|
|
52
|
+
logger.debug("Successfully imported Phoenix dependencies")
|
|
53
|
+
|
|
54
|
+
self.project_name = opts.get("project_name") or "default"
|
|
55
|
+
logger.info(f"Using project name: {self.project_name}")
|
|
56
|
+
|
|
57
|
+
tracer_provider = register(
|
|
58
|
+
project_name=self.project_name, auto_instrument=True
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
62
|
+
|
|
63
|
+
except ImportError as e:
|
|
64
|
+
logger.error(f"Missing required Phoenix dependencies: {e}")
|
|
65
|
+
raise ImportError(f"Phoenix dependencies not found: {e}. ") from e
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Failed to set up Phoenix instrumentation: {e}")
|
|
69
|
+
raise RuntimeError(f"Phoenix instrumentation setup failed: {e}") from e
|
|
70
|
+
|
|
71
|
+
logger.info("Phoenix local handler initialized...")
|
|
72
|
+
|
|
73
|
+
def _configure_collector_endpoint(self, opts: dict[str, Any]) -> None:
|
|
74
|
+
"""Configure the Phoenix collector endpoint from various sources."""
|
|
75
|
+
logger.debug("Configuring collector endpoint")
|
|
76
|
+
|
|
77
|
+
collector = (
|
|
78
|
+
self._resolve_env(opts.get("collector"))
|
|
79
|
+
or self._resolve_env(opts.get("collector_endpoint"))
|
|
80
|
+
or os.getenv("PHOENIX_COLLECTOR_ENDPOINT")
|
|
81
|
+
or self.default_endpoint
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
logger.info(f"Setting Phoenix collector endpoint to: {collector}")
|
|
85
|
+
os.environ["PHOENIX_COLLECTOR_ENDPOINT"] = collector
|
|
86
|
+
self.collector_endpoint = collector
|
|
87
|
+
|
|
88
|
+
def _start_phoenix_cli(self) -> None:
|
|
89
|
+
"""Start pheonix subprocess."""
|
|
90
|
+
try:
|
|
91
|
+
cmd = "phoenix serve"
|
|
92
|
+
logger.debug(f"Executing command: {cmd}")
|
|
93
|
+
|
|
94
|
+
self._proc = subprocess.Popen(
|
|
95
|
+
shlex.split(cmd),
|
|
96
|
+
stdout=subprocess.DEVNULL,
|
|
97
|
+
stderr=subprocess.DEVNULL,
|
|
98
|
+
start_new_session=True,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
logger.info(f"Phoenix server started with PID: {self._proc.pid}")
|
|
102
|
+
|
|
103
|
+
except FileNotFoundError as e:
|
|
104
|
+
logger.error(f"Phoenix CLI not found. Make sure Phoenix is installed : {e}")
|
|
105
|
+
self._proc = None
|
|
106
|
+
|
|
107
|
+
except subprocess.SubprocessError as e:
|
|
108
|
+
logger.error(f"Failed to start Phoenix CLI subprocess: {e}")
|
|
109
|
+
self._proc = None
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error(f"Unexpected error starting Phoenix CLI: {e}")
|
|
113
|
+
self._proc = None
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _resolve_env(value: str | None) -> str | None:
|
|
117
|
+
"""Resolve environment variable value."""
|
|
118
|
+
return _resolve_env(value)
|
|
119
|
+
|
|
120
|
+
def get_callbacks(self) -> list[Any]:
|
|
121
|
+
"""Return callbacks (Phoenix instruments globally; this may be empty)."""
|
|
122
|
+
logger.debug("Getting callbacks (Phoenix uses global instrumentation)")
|
|
123
|
+
return self._callbacks
|
idun_agent_engine/py.typed
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# Marker file to indicate that this package is PEP 561 typed
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Dependency injection helpers for FastAPI routes."""
|
|
2
|
+
|
|
3
|
+
from fastapi import Request
|
|
4
|
+
|
|
5
|
+
from ..core.config_builder import ConfigBuilder
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def get_agent(request: Request):
|
|
9
|
+
"""Return the pre-initialized agent instance from the app state.
|
|
10
|
+
|
|
11
|
+
Falls back to loading from the default config if not present (e.g., tests).
|
|
12
|
+
"""
|
|
13
|
+
if hasattr(request.app.state, "agent"):
|
|
14
|
+
return request.app.state.agent
|
|
15
|
+
else:
|
|
16
|
+
# This is a fallback for cases where the lifespan event did not run,
|
|
17
|
+
# like in some testing scenarios.
|
|
18
|
+
# Consider logging a warning here.
|
|
19
|
+
print("⚠️ Agent not found in app state, initializing fallback agent...")
|
|
20
|
+
|
|
21
|
+
app_config = ConfigBuilder.load_from_file()
|
|
22
|
+
agent = await ConfigBuilder.initialize_agent_from_config(app_config)
|
|
23
|
+
return agent
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Server lifespan management utilities.
|
|
2
|
+
|
|
3
|
+
Initializes the agent at startup and cleans up resources on shutdown.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
|
|
11
|
+
from ..core.config_builder import ConfigBuilder
|
|
12
|
+
|
|
13
|
+
|
|
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
|
|
20
|
+
|
|
21
|
+
# Use ConfigBuilder's centralized agent initialization
|
|
22
|
+
agent_instance = await ConfigBuilder.initialize_agent_from_config(engine_config)
|
|
23
|
+
|
|
24
|
+
# Store both in app state
|
|
25
|
+
app.state.agent = agent_instance
|
|
26
|
+
app.state.config = engine_config
|
|
27
|
+
|
|
28
|
+
agent_name = getattr(agent_instance, "name", "Unknown")
|
|
29
|
+
print(f"✅ Agent '{agent_name}' initialized and ready to serve!")
|
|
30
|
+
|
|
31
|
+
yield
|
|
32
|
+
|
|
33
|
+
# Clean up on shutdown
|
|
34
|
+
print("🔄 Idun Agent Engine shutting down...")
|
|
35
|
+
agent = getattr(app.state, "agent", None)
|
|
36
|
+
if agent is not None:
|
|
37
|
+
close_fn = getattr(agent, "close", None)
|
|
38
|
+
if callable(close_fn):
|
|
39
|
+
result = close_fn()
|
|
40
|
+
if inspect.isawaitable(result):
|
|
41
|
+
await result
|
|
42
|
+
print("✅ Agent resources cleaned up successfully.")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Agent routes for invoking and streaming agent responses."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|
7
|
+
from fastapi.responses import StreamingResponse
|
|
8
|
+
from idun_agent_schema.engine.api import ChatRequest, ChatResponse
|
|
9
|
+
|
|
10
|
+
from idun_agent_engine.agent.base import BaseAgent
|
|
11
|
+
from idun_agent_engine.server.dependencies import get_agent
|
|
12
|
+
|
|
13
|
+
logging.basicConfig(
|
|
14
|
+
format="%(asctime)s %(levelname)-8s %(message)s",
|
|
15
|
+
level=logging.INFO,
|
|
16
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
agent_router = APIRouter()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@agent_router.get("/config")
|
|
24
|
+
async def get_config(request: Request):
|
|
25
|
+
"""Get the current agent configuration."""
|
|
26
|
+
logger.debug("Fetching agent config..")
|
|
27
|
+
if not hasattr(request.app.state, "engine_config"):
|
|
28
|
+
logger.error("Error retrieving the engine config from the api. ")
|
|
29
|
+
raise HTTPException(
|
|
30
|
+
status_code=status.HTTP_404_NOT_FOUND, detail="Configuration not available"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
config = request.app.state.engine_config.agent
|
|
34
|
+
logger.info(f"Fetched config for agent: {config}")
|
|
35
|
+
return {"config": config}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@agent_router.post("/invoke", response_model=ChatResponse)
|
|
39
|
+
async def invoke(
|
|
40
|
+
request: ChatRequest,
|
|
41
|
+
agent: Annotated[BaseAgent, Depends(get_agent)],
|
|
42
|
+
):
|
|
43
|
+
"""Process a chat message with the agent without streaming."""
|
|
44
|
+
try:
|
|
45
|
+
message = {"query": request.query, "session_id": request.session_id}
|
|
46
|
+
response_content = await agent.invoke(message)
|
|
47
|
+
|
|
48
|
+
return ChatResponse(session_id=request.session_id, response=response_content)
|
|
49
|
+
except Exception as e: # noqa: BLE001
|
|
50
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@agent_router.post("/stream")
|
|
54
|
+
async def stream(
|
|
55
|
+
request: ChatRequest,
|
|
56
|
+
agent: Annotated[BaseAgent, Depends(get_agent)],
|
|
57
|
+
):
|
|
58
|
+
"""Process a message with the agent, streaming ag-ui events."""
|
|
59
|
+
try:
|
|
60
|
+
|
|
61
|
+
async def event_stream():
|
|
62
|
+
message = {"query": request.query, "session_id": request.session_id}
|
|
63
|
+
async for event in agent.stream(message):
|
|
64
|
+
yield f"data: {event.model_dump_json()}\n\n"
|
|
65
|
+
|
|
66
|
+
return StreamingResponse(event_stream(), media_type="text/event-stream")
|
|
67
|
+
except Exception as e: # noqa: BLE001
|
|
68
|
+
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Base routes for service health and landing info."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from ..._version import __version__
|
|
6
|
+
|
|
7
|
+
base_router = APIRouter()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@base_router.get("/health")
|
|
11
|
+
def health_check():
|
|
12
|
+
"""Health check endpoint for monitoring and load balancers."""
|
|
13
|
+
return {"status": "healthy", "engine_version": __version__}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Add a root endpoint with helpful information
|
|
17
|
+
@base_router.get("/")
|
|
18
|
+
def read_root():
|
|
19
|
+
"""Root endpoint with basic information about the service."""
|
|
20
|
+
return {
|
|
21
|
+
"message": "Welcome to your Idun Agent Engine server!",
|
|
22
|
+
"docs": "/docs",
|
|
23
|
+
"health": "/health",
|
|
24
|
+
"agent_endpoints": {"invoke": "/agent/invoke", "stream": "/agent/stream"},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# # Add info endpoint for detailed server and agent information
|
|
29
|
+
# @base_router.get("/info")
|
|
30
|
+
# def get_info(request: Request):
|
|
31
|
+
# """Get detailed information about the server and loaded agent."""
|
|
32
|
+
# info = {
|
|
33
|
+
# "engine": {
|
|
34
|
+
# "name": "Idun Agent Engine",
|
|
35
|
+
# "version": __version__,
|
|
36
|
+
# "description": "A framework for building and deploying conversational AI agents"
|
|
37
|
+
# },
|
|
38
|
+
# "server": {
|
|
39
|
+
# "status": "running",
|
|
40
|
+
# "endpoints": {
|
|
41
|
+
# "health": "/health",
|
|
42
|
+
# "docs": "/docs",
|
|
43
|
+
# "redoc": "/redoc",
|
|
44
|
+
# "agent_invoke": "/agent/invoke",
|
|
45
|
+
# "agent_stream": "/agent/stream"
|
|
46
|
+
# }
|
|
47
|
+
# }
|
|
48
|
+
# }
|
|
49
|
+
|
|
50
|
+
# # Add agent information if available in app state
|
|
51
|
+
# if hasattr(request.app.state, "config") and request.app.state.config:
|
|
52
|
+
# config = request.app.state.config
|
|
53
|
+
# info["agent"] = {
|
|
54
|
+
# "type": config.agent.type,
|
|
55
|
+
# "name": config.agent.config.get("name", "Unknown"),
|
|
56
|
+
# "status": "loaded"
|
|
57
|
+
# }
|
|
58
|
+
# info["server"]["port"] = config.server.api.port
|
|
59
|
+
|
|
60
|
+
# return info
|