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.
Files changed (37) hide show
  1. idun_agent_engine/__init__.py +2 -25
  2. idun_agent_engine/_version.py +1 -1
  3. idun_agent_engine/agent/__init__.py +10 -0
  4. idun_agent_engine/agent/base.py +97 -0
  5. idun_agent_engine/agent/haystack/__init__.py +9 -0
  6. idun_agent_engine/agent/haystack/haystack.py +261 -0
  7. idun_agent_engine/agent/haystack/haystack_model.py +13 -0
  8. idun_agent_engine/agent/haystack/utils.py +13 -0
  9. idun_agent_engine/agent/langgraph/__init__.py +7 -0
  10. idun_agent_engine/agent/langgraph/langgraph.py +429 -0
  11. idun_agent_engine/cli/__init__.py +16 -0
  12. idun_agent_engine/core/__init__.py +11 -0
  13. idun_agent_engine/core/app_factory.py +63 -0
  14. idun_agent_engine/core/config_builder.py +456 -0
  15. idun_agent_engine/core/engine_config.py +22 -0
  16. idun_agent_engine/core/server_runner.py +146 -0
  17. idun_agent_engine/observability/__init__.py +13 -0
  18. idun_agent_engine/observability/base.py +111 -0
  19. idun_agent_engine/observability/langfuse/__init__.py +5 -0
  20. idun_agent_engine/observability/langfuse/langfuse_handler.py +72 -0
  21. idun_agent_engine/observability/phoenix/__init__.py +5 -0
  22. idun_agent_engine/observability/phoenix/phoenix_handler.py +65 -0
  23. idun_agent_engine/observability/phoenix_local/__init__.py +5 -0
  24. idun_agent_engine/observability/phoenix_local/phoenix_local_handler.py +123 -0
  25. idun_agent_engine/py.typed +0 -1
  26. idun_agent_engine/server/__init__.py +5 -0
  27. idun_agent_engine/server/dependencies.py +23 -0
  28. idun_agent_engine/server/lifespan.py +42 -0
  29. idun_agent_engine/server/routers/__init__.py +5 -0
  30. idun_agent_engine/server/routers/agent.py +68 -0
  31. idun_agent_engine/server/routers/base.py +60 -0
  32. idun_agent_engine/server/server_config.py +8 -0
  33. idun_agent_engine-0.2.1.dist-info/METADATA +278 -0
  34. idun_agent_engine-0.2.1.dist-info/RECORD +35 -0
  35. {idun_agent_engine-0.1.0.dist-info → idun_agent_engine-0.2.1.dist-info}/WHEEL +1 -1
  36. idun_agent_engine-0.1.0.dist-info/METADATA +0 -317
  37. idun_agent_engine-0.1.0.dist-info/RECORD +0 -6
@@ -1,30 +1,8 @@
1
- """Idun Agent Engine - A framework for building and deploying conversational AI agents.
1
+ """Idun Agent Engine public API.
2
2
 
3
- This Engine provides a unified interface for different agent frameworks (LangGraph, CrewAI, etc.)
4
- and automatically generates a production-ready FastAPI server for your agents.
5
-
6
- Quick Start:
7
- from idun_agent_engine import ConfigBuilder, create_app, run_server
8
-
9
- # Method 1: Using ConfigBuilder (Recommended)
10
- config = (ConfigBuilder()
11
- .with_langgraph_agent(name="My Agent", graph_definition="agent.py:graph")
12
- .build())
13
- app = create_app(engine_config=config)
14
- run_server(app)
15
-
16
- # Method 2: Using YAML config file
17
- app = create_app(config_path="config.yaml")
18
- run_server(app, port=8000)
19
-
20
- # Method 3: One-liner from config file
21
- from idun_agent_engine.core.server_runner import run_server_from_config
22
- run_server_from_config("config.yaml")
23
-
24
- For more advanced usage, see the documentation.
3
+ Exports top-level helpers for convenience imports in examples and user code.
25
4
  """
26
5
 
27
- # Version information - import from separate module to avoid circular imports
28
6
  from ._version import __version__
29
7
  from .agent.base import BaseAgent
30
8
  from .core.app_factory import create_app
@@ -35,7 +13,6 @@ from .core.server_runner import (
35
13
  run_server_from_config,
36
14
  )
37
15
 
38
- # Main public API
39
16
  __all__ = [
40
17
  "create_app",
41
18
  "run_server",
@@ -1,3 +1,3 @@
1
1
  """Version information for Idun Agent Engine."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.1"
@@ -0,0 +1,10 @@
1
+ """Agent package exposing common base types.
2
+
3
+ Re-exports:
4
+ - BaseAgent: abstract base for all agents
5
+ - BaseAgentConfig: base model for agent configuration
6
+ """
7
+
8
+ from .base import BaseAgent
9
+
10
+ __all__ = ["BaseAgent"]
@@ -0,0 +1,97 @@
1
+ """Agent base interfaces.
2
+
3
+ Defines the abstract `BaseAgent` used by all agent implementations.
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from collections.abc import AsyncGenerator
8
+ from typing import Any
9
+
10
+ from idun_agent_schema.engine.agent import BaseAgentConfig
11
+
12
+
13
+ class BaseAgent[ConfigType: BaseAgentConfig](ABC):
14
+ """Abstract base for agents pluggable into the Idun Agent Engine.
15
+
16
+ Implements the public protocol that concrete agent adapters must follow.
17
+ """
18
+
19
+ _configuration: ConfigType
20
+
21
+ @property
22
+ @abstractmethod
23
+ def id(self) -> str:
24
+ """Unique identifier for the agent instance."""
25
+ pass
26
+
27
+ @property
28
+ @abstractmethod
29
+ def agent_type(self) -> str:
30
+ """Type or category of the agent (e.g., 'LangGraph', 'ADK')."""
31
+ pass
32
+
33
+ @property
34
+ @abstractmethod
35
+ def agent_instance(self) -> Any:
36
+ """Get the underlying agent instance from the specific framework.
37
+
38
+ This might be set after initialization.
39
+ """
40
+ pass
41
+
42
+ @property
43
+ def configuration(self) -> ConfigType:
44
+ """Return current configuration settings for the agent.
45
+
46
+ This is typically the configuration used during initialization.
47
+ """
48
+ return self._configuration
49
+
50
+ @property
51
+ @abstractmethod
52
+ def infos(self) -> dict[str, Any]:
53
+ """General information about the agent instance (e.g., version, status, metadata)."""
54
+ pass
55
+
56
+ @abstractmethod
57
+ async def initialize(self, config: dict[str, Any]) -> None:
58
+ """Initialize the agent with a given configuration.
59
+
60
+ This method should set up the underlying agent framework instance.
61
+
62
+ Args:
63
+ config: A dictionary containing the agent's configuration.
64
+ """
65
+ pass
66
+
67
+ @abstractmethod
68
+ async def invoke(self, message: Any) -> Any:
69
+ """Process a single input message and return a response.
70
+
71
+ This should be an awaitable method if the underlying agent processes
72
+ asynchronously.
73
+
74
+ Args:
75
+ message: The input message for the agent.
76
+
77
+ Returns:
78
+ The agent's response.
79
+ """
80
+ pass
81
+
82
+ @abstractmethod
83
+ async def stream(self, message: Any) -> AsyncGenerator[Any]:
84
+ """Process a single input message and return an asynchronous stream.
85
+
86
+ Args:
87
+ message: The input message for the agent.
88
+
89
+ Yields:
90
+ Chunks of the agent's response.
91
+ """
92
+ # This is an async generator, so it needs `async def` and `yield`
93
+ # For the ABC, we can't have a `yield` directly in the abstract method body.
94
+ # The signature itself defines it as an async generator.
95
+ # Example: async for chunk in agent.stream(message): ...
96
+ if False: # pragma: no cover (This is just to make it a generator type for static analysis)
97
+ yield
@@ -0,0 +1,9 @@
1
+ """LangGraph agent package."""
2
+
3
+ from .haystack import HaystackAgent
4
+ from .haystack_model import HaystackAgentConfig
5
+
6
+ __all__ = [
7
+ "HaystackAgent",
8
+ "HaystackAgentConfig",
9
+ ]
@@ -0,0 +1,261 @@
1
+ import importlib.util
2
+ import logging
3
+ import os
4
+ import uuid
5
+ from typing import Any
6
+
7
+ os.environ["HAYSTACK_CONTENT_TRACING_ENABLED"] = "true"
8
+
9
+ from haystack import Pipeline
10
+ from haystack.components.agents import Agent
11
+ from haystack.dataclasses import ChatMessage
12
+ from haystack_integrations.components.connectors.langfuse import LangfuseConnector
13
+
14
+ from idun_agent_engine.agent.base import BaseAgent
15
+ from idun_agent_schema.engine.haystack import HaystackAgentConfig
16
+ from idun_agent_engine.agent.haystack.utils import _parse_component_definition
17
+
18
+ logging.basicConfig(
19
+ format="%(asctime)s %(levelname)-8s %(message)s",
20
+ level=logging.INFO,
21
+ datefmt="%Y-%m-%d %H:%M:%S",
22
+ )
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class HaystackAgent(BaseAgent):
28
+ """Haystack agent adapter implementing the BaseAgent protocol."""
29
+
30
+ def __init__(self):
31
+ """Initialize an unconfigured haystack agent with default state."""
32
+ self._id: str = str(uuid.uuid4())
33
+ self._agent_type: str = "haystack"
34
+ self._agent_instance: Any = None
35
+ self._configuration: HaystackAgentConfig | None = None
36
+ self._name: str = "Haystack Agent"
37
+ self._langfuse_tracing: bool = False
38
+ self._enable_tracing: bool = False
39
+ self._infos: dict[str, Any] = {
40
+ "status": "Uninitialized",
41
+ "name": self._name,
42
+ "id": self._id,
43
+ }
44
+ # TODO: input/output schema
45
+ # TODO: checkpointing/debugging
46
+
47
+ @property
48
+ def id(self) -> str:
49
+ """Returns the agent id."""
50
+ return self._id
51
+
52
+ @property
53
+ def agent_type(self) -> str:
54
+ """Return agent type label."""
55
+ return self._agent_type
56
+
57
+ @property
58
+ def name(self) -> str:
59
+ """Return configured human-readable agent name."""
60
+ return self._name
61
+
62
+ @property
63
+ def agent_instance(self) -> Any:
64
+ """Return compiled graph instance.
65
+
66
+ Raises:
67
+ RuntimeError: If the agent is not yet initialized.
68
+ """
69
+ if self._agent_instance is None:
70
+ raise RuntimeError("Agent not initialized. Call initialize() first.")
71
+ return self._agent_instance
72
+
73
+ @property
74
+ def configuration(self) -> HaystackAgentConfig:
75
+ """Return validated configuration.
76
+
77
+ Raises:
78
+ RuntimeError: If the agent has not been configured yet.
79
+ """
80
+ if not self._configuration:
81
+ raise RuntimeError("Agent not configured. Call initialize() first.")
82
+ return self._configuration
83
+
84
+ @property
85
+ def infos(self) -> dict[str, Any]:
86
+ """Return diagnostic information about the agent instance."""
87
+ return self._infos
88
+
89
+ def _check_langfuse_tracing(self, pipeline: Pipeline) -> None:
90
+ """Check if the pipeline has a LangfuseConnector."""
91
+ logger.debug("Searching LangfuseConnector in the pipeline..")
92
+ for name, component in pipeline.walk():
93
+ if isinstance(component, LangfuseConnector):
94
+ logger.info(f"Found LangfuseConnector component with name: {name}")
95
+ self._langfuse_tracing = True
96
+
97
+ def _add_langfuse_tracing(self, component: Agent | Pipeline):
98
+ logger.debug("Checking for Langfuse tracing...")
99
+ if isinstance(component, Pipeline):
100
+ if self._langfuse_tracing:
101
+ logger.info("langfuse tracing already on")
102
+ elif not self._langfuse_tracing and self._enable_tracing:
103
+ logger.info("Pipeline has no tracer included. Adding Langfuse tracer")
104
+ if (
105
+ not os.environ.get("LANGFUSE_API_KEY")
106
+ or not os.environ.get("LANGFUSE_SECRET_KEY")
107
+ or not os.environ.get("LANGFUSE_PUBLIC_KEY")
108
+ ):
109
+ raise ValueError(
110
+ "Langfuse keys not set! make sure you set Langfuse secret and public keys"
111
+ )
112
+ component.add_component(
113
+ f"{self._configuration.name} tracer",
114
+ instance=LangfuseConnector(self._configuration.name),
115
+ )
116
+ logger.info("Added component tracer")
117
+ self._langfuse_tracing = True
118
+ logger.info("Agent tracing not supported yet")
119
+
120
+ async def initialize(self, config: HaystackAgentConfig | dict[str, Any]) -> None:
121
+ try:
122
+ logger.debug(f"Initializing haystack agent config: {config}...")
123
+
124
+ if isinstance(config, HaystackAgentConfig):
125
+ self._configuration = config
126
+ logger.debug("Validated HaystackAgentConfig")
127
+ else:
128
+ logger.warning(f"Validating a dict config: {config}")
129
+ self._configuration = HaystackAgentConfig.model_validate(config)
130
+ logger.debug("Validated dict config")
131
+ self._name = self._configuration.name or "Haystack Agent"
132
+ self._infos["name"] = self._name
133
+ # TODO: await persistence haystack
134
+ # TODO OBS block
135
+
136
+ # check if config has observability `enabled` or `disabled`, so that we adjust our component to
137
+ # either add a tracer or not
138
+ if self._configuration.observability.enabled:
139
+ self._enable_tracing = True
140
+ logger.info("Enabling tracing...")
141
+ component: Agent | Pipeline = self._load_component(
142
+ self._configuration.component_definition
143
+ )
144
+ self._infos["component_type"] = self._configuration.component_type
145
+ self._infos["component_definition"] = (
146
+ self._configuration.component_definition
147
+ )
148
+ self._agent_instance = component
149
+ # TODO: input output schema definition
150
+ self._infos["status"] = "initialized"
151
+ logger.info("Status initialized!")
152
+ self._infos["config_used"] = self._configuration.model_dump()
153
+ except Exception as e:
154
+ logger.error(f"Failed to initialize HaystackAgent: {e}")
155
+ raise
156
+
157
+ def _fetch_component_from_module(self) -> Agent | Pipeline:
158
+ """Fetches the variable that holds the component of an Agent/Pipeline.
159
+
160
+ Returns: Agent | Pipeline.
161
+ """
162
+ module_path, component_variable_name = _parse_component_definition(
163
+ self._configuration.component_definition
164
+ )
165
+ logger.debug(
166
+ f"Importing spec from file location: {self._configuration.component_definition}"
167
+ )
168
+ try:
169
+ spec = importlib.util.spec_from_file_location(
170
+ component_variable_name, module_path
171
+ )
172
+ if spec is None or spec.loader is None:
173
+ logger.error(f"Could not load spec for module at {module_path}")
174
+ raise ImportError(f"Could not load spec for module at {module_path}")
175
+
176
+ module = importlib.util.module_from_spec(spec)
177
+ logger.debug("Execing module..")
178
+ spec.loader.exec_module(module)
179
+ logger.debug("Module executed")
180
+
181
+ component_variable = getattr(module, component_variable_name)
182
+ logger.info(f"Found component variable: {component_variable}")
183
+
184
+ component = getattr(module, component_variable_name)
185
+
186
+ if not isinstance(component, (Pipeline, Agent)):
187
+ raise TypeError(
188
+ f"The variable '{component_variable_name}' from {module_path} is not a Pipeline or Agent instance. Got {type(component)}"
189
+ )
190
+
191
+ return component
192
+
193
+ except Exception as e:
194
+ raise ValueError(
195
+ f"Invalid component definition string: {self._configuration.component_definition}. Error: {e}"
196
+ ) from e
197
+
198
+ def _load_component(self, component_definition: str) -> Agent | Pipeline:
199
+ """Loads a Haystack component (Agent or Pipeline) from the path (component definition) and returns the agent_instance with langfuse tracing."""
200
+ logger.debug(f"Loading component from: {component_definition}...")
201
+
202
+ component = self._fetch_component_from_module()
203
+ if self._enable_tracing:
204
+ try:
205
+ self._add_langfuse_tracing(component)
206
+ except (FileNotFoundError, ImportError, AttributeError) as e:
207
+ raise ValueError(
208
+ f"Failed to load agent from {component_definition}: {e}"
209
+ ) from e
210
+
211
+ return component
212
+ else:
213
+ logger.debug("User wants tracing disabled. Skipping..")
214
+ return component
215
+
216
+ async def invoke(self, message: Any) -> Any:
217
+ """Process a single input to chat with the agent.The message should be a dictionary containing 'query' and 'session_id'."""
218
+ # TODO: validate actual message
219
+ # TODO: validate input schema
220
+ logger.debug(f"Invoking pipeline for message: {message}")
221
+ if self._agent_instance is None:
222
+ raise RuntimeError(
223
+ "Agent not initialized. Call initialize() before processing messages."
224
+ )
225
+
226
+ if (
227
+ not isinstance(message, dict)
228
+ or "query" not in message
229
+ or "session_id" not in message
230
+ ):
231
+ raise ValueError(
232
+ "Message must be a dictionary with 'query' and 'session_id' keys."
233
+ )
234
+
235
+ try:
236
+ # TODO: support async
237
+ # if pipeline
238
+ if isinstance(self._agent_instance, Pipeline):
239
+ logger.debug("Running Pipeline instance...")
240
+ raw_result = self._agent_instance.run(data={"query": message["query"]})
241
+ result = raw_result["generator"]["replies"][0]
242
+ logger.info(f"Pipeline answer: {result}")
243
+ return result
244
+
245
+ # if agent
246
+ elif isinstance(self._agent_instance, Agent):
247
+ logger.debug("Running Agent instance...")
248
+ raw_result = self._agent_instance.run(
249
+ # TODO: make run method arguments based on component type
250
+ messages=[ChatMessage.from_user(message["query"])]
251
+ ) # TODO: from input schema
252
+ logger.info(f"Pipeline answer: {raw_result['messages'][-1].text}")
253
+ result = raw_result["messages"][-1].text
254
+ return result
255
+
256
+ # TODO: validates with output schema, and not hardcodded
257
+ except Exception as e:
258
+ raise RuntimeError(f"Pipeline execution failed: {e}") from e
259
+
260
+ async def stream(self, message: Any) -> Any:
261
+ pass
@@ -0,0 +1,13 @@
1
+ """Configuration models for Haystack agents."""
2
+
3
+ from typing import Literal
4
+
5
+ from idun_agent_engine.core.engine_config import BaseAgentConfig
6
+
7
+
8
+ class HaystackAgentConfig(BaseAgentConfig):
9
+ """Configuration model for Haystack Agents."""
10
+
11
+ type: Literal["haystack"] = "haystack"
12
+ component_type: Literal["pipeline", "agent"]
13
+ component_definition: str
@@ -0,0 +1,13 @@
1
+ def _parse_component_definition(component_definition: str) -> tuple[str, str]:
2
+ try:
3
+ if ":" not in component_definition:
4
+ raise ValueError(
5
+ f" component_definition must be in format: 'path/to/my/module.py:component_variable_name"
6
+ f"got: {component_definition}"
7
+ )
8
+ module_path, component_variable_name = component_definition.rsplit(":", 1)
9
+ return module_path, component_variable_name
10
+ except Exception as e:
11
+ raise ValueError(
12
+ f"Invalid Component Definition format: {component_definition}"
13
+ ) from e
@@ -0,0 +1,7 @@
1
+ """LangGraph agent package."""
2
+
3
+ from .langgraph import LanggraphAgent
4
+
5
+ __all__ = [
6
+ "LanggraphAgent",
7
+ ]