agentomatic 0.4.1__tar.gz → 0.5.0__tar.gz
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.
- {agentomatic-0.4.1 → agentomatic-0.5.0}/PKG-INFO +1 -1
- {agentomatic-0.4.1 → agentomatic-0.5.0}/pyproject.toml +2 -1
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/_version.py +1 -1
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/lifespan.py +6 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/manifest.py +6 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/memory_manager.py +37 -6
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/platform.py +57 -9
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/registry.py +4 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/router_factory.py +26 -14
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/state.py +3 -1
- agentomatic-0.5.0/src/agentomatic/middleware/__init__.py +36 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/middleware/feedback.py +5 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/config.py +26 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/optimizer.py +35 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/adapters/langgraph.py +28 -19
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/run_tracker.py +6 -1
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/serve.py +14 -9
- agentomatic-0.5.0/src/agentomatic/studio/static/asset-manifest.json +13 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/static/index.html +1 -1
- agentomatic-0.5.0/src/agentomatic/studio/static/static/css/main.8b3c42dd.css +4 -0
- agentomatic-0.5.0/src/agentomatic/studio/static/static/css/main.8b3c42dd.css.map +1 -0
- agentomatic-0.5.0/src/agentomatic/studio/static/static/js/main.cbb2a8ee.js +3 -0
- agentomatic-0.5.0/src/agentomatic/studio/static/static/js/main.cbb2a8ee.js.map +1 -0
- agentomatic-0.5.0/tests/test_bugfixes_041.py +619 -0
- agentomatic-0.4.1/src/agentomatic/middleware/__init__.py +0 -18
- agentomatic-0.4.1/src/agentomatic/studio/static/asset-manifest.json +0 -13
- agentomatic-0.4.1/src/agentomatic/studio/static/static/css/main.8692ac74.css +0 -4
- agentomatic-0.4.1/src/agentomatic/studio/static/static/css/main.8692ac74.css.map +0 -1
- agentomatic-0.4.1/src/agentomatic/studio/static/static/js/main.aa720e8f.js +0 -3
- agentomatic-0.4.1/src/agentomatic/studio/static/static/js/main.aa720e8f.js.map +0 -1
- {agentomatic-0.4.1 → agentomatic-0.5.0}/LICENSE +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/README.md +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/examples/full_agent/README.md +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/cli/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/cli/commands.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/cli/templates.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/config/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/config/defaults.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/config/settings.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/demo/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/demo/agent.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/demo/server.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/middleware/auth.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/middleware/logging.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/middleware/metrics.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/middleware/rate_limit.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/observability/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/observability/concurrency.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/observability/metrics.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/observability/telemetry.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/dataset.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/deployment.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/eval_contract.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/failure_analysis.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/fitter.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/fitter_optimizers.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/judges.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/llm_caller.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/loop.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/metrics.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/report.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/runner.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/search_space.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/strategies.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/synthesizer.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/prompts/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/prompts/manager.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/protocols/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/protocols/decorators.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/providers/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/providers/embeddings.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/providers/llm.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/py.typed +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/storage/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/storage/base.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/storage/checkpointer.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/storage/memory.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/storage/models.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/storage/sqlalchemy.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/adapter.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/adapters/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/adapters/generic.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/adapters/langchain.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/decorators.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/graph_inspector.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/models.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/router.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/static/imgs/graph_view.png +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/static/imgs/logo.png +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/static/imgs/main_screen.png +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/static/imgs/main_screen_ok.png +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/static/manifest.json +0 -0
- /agentomatic-0.4.1/src/agentomatic/studio/static/static/js/main.aa720e8f.js.LICENSE.txt → /agentomatic-0.5.0/src/agentomatic/studio/static/static/js/main.cbb2a8ee.js.LICENSE.txt +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/ui/__init__.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/ui/chat.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_agentomatic.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_cli.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_coverage_boost.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_deep_agent.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_fitter.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_integration.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_optimize.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_platform_features.py +0 -0
- {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_studio.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentomatic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Drop agents, not code — zero-code multi-agent API platform framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/UnicoLab/agentomatic
|
|
6
6
|
Project-URL: Documentation, https://unicolab.github.io/agentomatic
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agentomatic"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Drop agents, not code — zero-code multi-agent API platform framework"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {text = "MIT"}
|
|
@@ -83,6 +83,7 @@ Changelog = "https://github.com/UnicoLab/agentomatic/blob/main/CHANGELOG.md"
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
|
|
86
87
|
[dependency-groups]
|
|
87
88
|
dev = [
|
|
88
89
|
"pytest>=8",
|
|
@@ -47,6 +47,12 @@ async def create_lifespan(
|
|
|
47
47
|
) -> AsyncIterator[Callable[[FastAPI], Any]]:
|
|
48
48
|
"""Create a FastAPI lifespan context manager.
|
|
49
49
|
|
|
50
|
+
.. deprecated::
|
|
51
|
+
This function is retained for backward compatibility but is no
|
|
52
|
+
longer used by :meth:`AgentPlatform.build`, which creates its own
|
|
53
|
+
inline lifespan. Prefer :meth:`AgentPlatform.on_startup` /
|
|
54
|
+
:meth:`AgentPlatform.on_shutdown` hooks instead.
|
|
55
|
+
|
|
50
56
|
Startup sequence:
|
|
51
57
|
1. Configure logging
|
|
52
58
|
2. Discover agents
|
|
@@ -58,6 +58,12 @@ class RegisteredAgent:
|
|
|
58
58
|
config: Any = None
|
|
59
59
|
prompt_manager: PromptManager | None = None
|
|
60
60
|
|
|
61
|
+
# Studio hooks (set programmatically or via decorators)
|
|
62
|
+
_studio_graph_fn: Callable[..., Any] | None = field(default=None, repr=False)
|
|
63
|
+
_studio_state_fn: Callable[..., Any] | None = field(default=None, repr=False)
|
|
64
|
+
_studio_stream_fn: Callable[..., Any] | None = field(default=None, repr=False)
|
|
65
|
+
_studio_adapter: Any = field(default=None, repr=False)
|
|
66
|
+
|
|
61
67
|
@property
|
|
62
68
|
def name(self) -> str:
|
|
63
69
|
"""Return the agent's short machine name."""
|
|
@@ -21,16 +21,47 @@ from __future__ import annotations
|
|
|
21
21
|
import uuid
|
|
22
22
|
from typing import Any
|
|
23
23
|
|
|
24
|
-
from langchain_core.messages import (
|
|
25
|
-
AIMessage,
|
|
26
|
-
BaseMessage,
|
|
27
|
-
HumanMessage,
|
|
28
|
-
SystemMessage,
|
|
29
|
-
)
|
|
30
24
|
from loguru import logger
|
|
31
25
|
|
|
32
26
|
from agentomatic.storage.base import BaseStore
|
|
33
27
|
|
|
28
|
+
# langchain_core is an optional dependency — provide lightweight fallbacks
|
|
29
|
+
# so the module can be imported without it. The real classes are used when
|
|
30
|
+
# langchain_core is installed; the fallbacks store role + content only.
|
|
31
|
+
try:
|
|
32
|
+
from langchain_core.messages import (
|
|
33
|
+
AIMessage,
|
|
34
|
+
BaseMessage,
|
|
35
|
+
HumanMessage,
|
|
36
|
+
SystemMessage,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
HAS_LANGCHAIN = True
|
|
40
|
+
except ImportError:
|
|
41
|
+
HAS_LANGCHAIN = False
|
|
42
|
+
|
|
43
|
+
class BaseMessage: # type: ignore[no-redef]
|
|
44
|
+
"""Minimal stand-in when langchain_core is not installed."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, content: str = "", **kwargs: Any) -> None:
|
|
47
|
+
self.content = content
|
|
48
|
+
self.type = kwargs.get("type", "base")
|
|
49
|
+
|
|
50
|
+
def __repr__(self) -> str:
|
|
51
|
+
return f"{self.__class__.__name__}(content={self.content!r})"
|
|
52
|
+
|
|
53
|
+
class HumanMessage(BaseMessage): # type: ignore[no-redef]
|
|
54
|
+
def __init__(self, content: str = "", **kwargs: Any) -> None:
|
|
55
|
+
super().__init__(content=content, type="human", **kwargs)
|
|
56
|
+
|
|
57
|
+
class AIMessage(BaseMessage): # type: ignore[no-redef]
|
|
58
|
+
def __init__(self, content: str = "", **kwargs: Any) -> None:
|
|
59
|
+
super().__init__(content=content, type="ai", **kwargs)
|
|
60
|
+
|
|
61
|
+
class SystemMessage(BaseMessage): # type: ignore[no-redef]
|
|
62
|
+
def __init__(self, content: str = "", **kwargs: Any) -> None:
|
|
63
|
+
super().__init__(content=content, type="system", **kwargs)
|
|
64
|
+
|
|
34
65
|
# Default summary system prompt
|
|
35
66
|
_SUMMARY_SYSTEM_PROMPT = (
|
|
36
67
|
"You are a conversation summarizer. Condense the following conversation "
|
|
@@ -141,6 +141,7 @@ class AgentPlatform:
|
|
|
141
141
|
self._on_shutdown: list[Callable[..., Any]] = []
|
|
142
142
|
self._extra_routers: list[tuple[str, Any, dict[str, Any]]] = []
|
|
143
143
|
self._app: FastAPI | None = None
|
|
144
|
+
self._discovered: bool = False # guard against double discovery
|
|
144
145
|
|
|
145
146
|
# ------------------------------------------------------------------
|
|
146
147
|
# Factory helpers
|
|
@@ -233,6 +234,31 @@ class AgentPlatform:
|
|
|
233
234
|
self._on_shutdown.append(fn)
|
|
234
235
|
return fn
|
|
235
236
|
|
|
237
|
+
def discover_agents(self) -> None:
|
|
238
|
+
"""Manually trigger agent discovery from the configured folder.
|
|
239
|
+
|
|
240
|
+
This is useful for testing scenarios, embedding, or any context
|
|
241
|
+
where the async lifespan startup may not have run yet. Safe to
|
|
242
|
+
call multiple times — subsequent calls are no-ops.
|
|
243
|
+
|
|
244
|
+
Example::
|
|
245
|
+
|
|
246
|
+
platform = AgentPlatform.from_folder("agents/")
|
|
247
|
+
platform.discover_agents() # synchronous discovery
|
|
248
|
+
app = platform.build()
|
|
249
|
+
"""
|
|
250
|
+
if self._discovered:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
# Ensure agents directory is importable
|
|
254
|
+
parent = str(self.agents_dir.parent)
|
|
255
|
+
if parent not in sys.path:
|
|
256
|
+
sys.path.insert(0, parent)
|
|
257
|
+
|
|
258
|
+
prefix = self.package_prefix or self.agents_dir.name
|
|
259
|
+
self._registry.discover(self.agents_dir, prefix)
|
|
260
|
+
self._discovered = True
|
|
261
|
+
|
|
236
262
|
# ------------------------------------------------------------------
|
|
237
263
|
# Custom routers
|
|
238
264
|
# ------------------------------------------------------------------
|
|
@@ -261,7 +287,22 @@ class AgentPlatform:
|
|
|
261
287
|
"""
|
|
262
288
|
platform = self # capture for closure
|
|
263
289
|
|
|
264
|
-
#
|
|
290
|
+
# Ensure agents directory is importable BEFORE build so
|
|
291
|
+
# programmatic and synchronous access to agents works.
|
|
292
|
+
parent = str(platform.agents_dir.parent)
|
|
293
|
+
if parent not in sys.path:
|
|
294
|
+
sys.path.insert(0, parent)
|
|
295
|
+
|
|
296
|
+
# Eagerly discover agents so they are available immediately
|
|
297
|
+
# after build() returns — no need to wait for lifespan startup.
|
|
298
|
+
# This also fixes TestClient scenarios where lifespan may not
|
|
299
|
+
# fully trigger.
|
|
300
|
+
if not platform._discovered:
|
|
301
|
+
prefix = platform.package_prefix or platform.agents_dir.name
|
|
302
|
+
platform._registry.discover(platform.agents_dir, prefix)
|
|
303
|
+
platform._discovered = True
|
|
304
|
+
|
|
305
|
+
# Track which agents are already registered (programmatic + discovered)
|
|
265
306
|
_pre_registered = set(platform._registry.list_names())
|
|
266
307
|
|
|
267
308
|
@asynccontextmanager
|
|
@@ -277,14 +318,11 @@ class AgentPlatform:
|
|
|
277
318
|
await platform._store.initialize()
|
|
278
319
|
logger.info("🗄️ Storage backend initialized")
|
|
279
320
|
|
|
280
|
-
#
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
# Auto-discover agents from folder (skips already-registered)
|
|
286
|
-
prefix = platform.package_prefix or platform.agents_dir.name
|
|
287
|
-
platform._registry.discover(platform.agents_dir, prefix)
|
|
321
|
+
# Re-discover in lifespan only if not already done (hot-reload)
|
|
322
|
+
if not platform._discovered:
|
|
323
|
+
prefix = platform.package_prefix or platform.agents_dir.name
|
|
324
|
+
platform._registry.discover(platform.agents_dir, prefix)
|
|
325
|
+
platform._discovered = True
|
|
288
326
|
|
|
289
327
|
# Auto-generate + mount routers for NEWLY discovered agents
|
|
290
328
|
for name, agent in platform._registry.all().items():
|
|
@@ -577,6 +615,16 @@ class AgentPlatform:
|
|
|
577
615
|
|
|
578
616
|
if is_studio_available():
|
|
579
617
|
mount_studio_ui(app)
|
|
618
|
+
|
|
619
|
+
# Convenience redirects: /studio → /studio/ui/
|
|
620
|
+
from fastapi.responses import RedirectResponse
|
|
621
|
+
|
|
622
|
+
@app.get("/studio", include_in_schema=False)
|
|
623
|
+
@app.get("/studio/", include_in_schema=False)
|
|
624
|
+
async def _studio_redirect() -> RedirectResponse:
|
|
625
|
+
return RedirectResponse(url="/studio/ui/")
|
|
626
|
+
|
|
627
|
+
logger.info("🎨 Studio UI + redirects mounted")
|
|
580
628
|
else:
|
|
581
629
|
logger.info(
|
|
582
630
|
"🎨 Studio API is running but UI assets not bundled. "
|
|
@@ -89,6 +89,10 @@ class AgentRegistry:
|
|
|
89
89
|
manifest=manifest,
|
|
90
90
|
node_fn=node_fn,
|
|
91
91
|
module_path=module_path,
|
|
92
|
+
_studio_graph_fn=getattr(mod, "studio_graph_topology", None),
|
|
93
|
+
_studio_state_fn=getattr(mod, "studio_state_provider", None),
|
|
94
|
+
# Also look for a stream provider if it exists
|
|
95
|
+
_studio_stream_fn=getattr(mod, "studio_stream_provider", None),
|
|
92
96
|
)
|
|
93
97
|
|
|
94
98
|
# Discover optional enhancements
|
|
@@ -622,20 +622,31 @@ def create_default_router(
|
|
|
622
622
|
history_loaded = 0
|
|
623
623
|
if request.messages is not None:
|
|
624
624
|
# User supplied their own messages — use them directly
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
625
|
+
try:
|
|
626
|
+
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
|
627
|
+
|
|
628
|
+
lc_messages: list[Any] = []
|
|
629
|
+
for msg in request.messages:
|
|
630
|
+
role = msg.get("role", "user")
|
|
631
|
+
content_val = msg.get("content", "")
|
|
632
|
+
if role == "assistant":
|
|
633
|
+
lc_messages.append(AIMessage(content=content_val))
|
|
634
|
+
elif role == "system":
|
|
635
|
+
lc_messages.append(SystemMessage(content=content_val))
|
|
636
|
+
else:
|
|
637
|
+
lc_messages.append(HumanMessage(content=content_val))
|
|
638
|
+
lc_messages.append(HumanMessage(content=request.content))
|
|
639
|
+
state["messages"] = lc_messages
|
|
640
|
+
except ImportError:
|
|
641
|
+
# langchain_core not installed — use plain dicts
|
|
642
|
+
lc_messages_plain: list[dict[str, str]] = []
|
|
643
|
+
for msg in request.messages:
|
|
644
|
+
lc_messages_plain.append({
|
|
645
|
+
"role": msg.get("role", "user"),
|
|
646
|
+
"content": msg.get("content", ""),
|
|
647
|
+
})
|
|
648
|
+
lc_messages_plain.append({"role": "user", "content": request.content})
|
|
649
|
+
state["messages"] = lc_messages_plain
|
|
639
650
|
history_loaded = len(request.messages)
|
|
640
651
|
elif memory_mgr and request.include_history:
|
|
641
652
|
try:
|
|
@@ -656,6 +667,7 @@ def create_default_router(
|
|
|
656
667
|
history_loaded = max(0, len(messages) - 1)
|
|
657
668
|
except Exception as exc:
|
|
658
669
|
logger.warning(f"History loading failed for chat: {exc}")
|
|
670
|
+
state["metadata"]["_history_error"] = str(exc)
|
|
659
671
|
|
|
660
672
|
# Run before_node hooks
|
|
661
673
|
for hook in registry.before_node_hooks:
|
|
@@ -15,7 +15,9 @@ except ImportError:
|
|
|
15
15
|
def add_messages(left, right): # type: ignore[misc]
|
|
16
16
|
"""Fallback: simple list concatenation."""
|
|
17
17
|
if left is None:
|
|
18
|
-
return right
|
|
18
|
+
return right if right is not None else []
|
|
19
|
+
if right is None:
|
|
20
|
+
return left
|
|
19
21
|
return left + right
|
|
20
22
|
|
|
21
23
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Pluggable middleware stack for agentomatic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"LoggingMiddleware",
|
|
9
|
+
"RateLimitMiddleware",
|
|
10
|
+
"AuthMiddleware",
|
|
11
|
+
"MetricsMiddleware",
|
|
12
|
+
"FeedbackCollector",
|
|
13
|
+
"collect_feedback",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
# Lazy imports to avoid cascading failures when optional dependencies
|
|
17
|
+
# (e.g. prometheus_client for MetricsMiddleware) are not installed.
|
|
18
|
+
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
|
19
|
+
"AuthMiddleware": (".auth", "AuthMiddleware"),
|
|
20
|
+
"LoggingMiddleware": (".logging", "LoggingMiddleware"),
|
|
21
|
+
"MetricsMiddleware": (".metrics", "MetricsMiddleware"),
|
|
22
|
+
"RateLimitMiddleware": (".rate_limit", "RateLimitMiddleware"),
|
|
23
|
+
"FeedbackCollector": (".feedback", "FeedbackCollector"),
|
|
24
|
+
"collect_feedback": (".feedback", "collect_feedback"),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def __getattr__(name: str) -> Any:
|
|
29
|
+
if name in _LAZY_IMPORTS:
|
|
30
|
+
module_path, attr = _LAZY_IMPORTS[name]
|
|
31
|
+
import importlib
|
|
32
|
+
|
|
33
|
+
mod = importlib.import_module(module_path, package=__name__)
|
|
34
|
+
return getattr(mod, attr)
|
|
35
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
36
|
+
|
|
@@ -198,6 +198,11 @@ _collector: FeedbackCollector | None = None
|
|
|
198
198
|
def get_collector() -> FeedbackCollector:
|
|
199
199
|
global _collector
|
|
200
200
|
if _collector is None:
|
|
201
|
+
logger.warning(
|
|
202
|
+
"📝 FeedbackCollector initialized without a storage backend — "
|
|
203
|
+
"feedback will be kept in a volatile in-memory buffer only. "
|
|
204
|
+
"Pass a store via set_collector() or enable_feedback + store to persist."
|
|
205
|
+
)
|
|
201
206
|
_collector = FeedbackCollector()
|
|
202
207
|
return _collector
|
|
203
208
|
|
|
@@ -449,3 +449,29 @@ class PromptFitResult:
|
|
|
449
449
|
logger.info("Wrote runtime config to {}", config_path.resolve())
|
|
450
450
|
|
|
451
451
|
return version
|
|
452
|
+
|
|
453
|
+
def save(self, directory: str | Path) -> Path:
|
|
454
|
+
"""Save the full result as JSON to *directory*.
|
|
455
|
+
|
|
456
|
+
This is a convenience method that writes the entire
|
|
457
|
+
:meth:`to_dict` output as ``fit_result.json``. Use
|
|
458
|
+
:meth:`apply` to additionally write ``prompts.json`` and
|
|
459
|
+
``runtime_config.json`` for the agent.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
directory: Output directory (created if needed).
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Path to the written JSON file.
|
|
466
|
+
"""
|
|
467
|
+
out = Path(directory)
|
|
468
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
469
|
+
|
|
470
|
+
json_path = out / "fit_result.json"
|
|
471
|
+
json_path.write_text(
|
|
472
|
+
json.dumps(self.to_dict(), indent=2, ensure_ascii=False, default=str)
|
|
473
|
+
+ "\n",
|
|
474
|
+
encoding="utf-8",
|
|
475
|
+
)
|
|
476
|
+
logger.info("Saved fit result to {}", json_path.resolve())
|
|
477
|
+
return json_path
|
|
@@ -299,6 +299,41 @@ class OptimizationResult:
|
|
|
299
299
|
],
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
def save(self, directory: str | Path) -> tuple[Path, Path | None]:
|
|
303
|
+
"""Save results as JSON + optional HTML report.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
directory: Output directory (created if needed).
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
``(json_path, html_path)`` tuple. ``html_path`` is ``None``
|
|
310
|
+
if the report generator is unavailable.
|
|
311
|
+
"""
|
|
312
|
+
out = Path(directory)
|
|
313
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
314
|
+
|
|
315
|
+
# JSON results
|
|
316
|
+
json_path = out / "optimization_results.json"
|
|
317
|
+
json_path.write_text(
|
|
318
|
+
json.dumps(self.to_dict(), indent=2, ensure_ascii=False, default=str)
|
|
319
|
+
)
|
|
320
|
+
logger.info(f"📄 Results saved to {json_path}")
|
|
321
|
+
|
|
322
|
+
# HTML report
|
|
323
|
+
html_path: Path | None = None
|
|
324
|
+
try:
|
|
325
|
+
from agentomatic.optimize.report import generate_html_report
|
|
326
|
+
|
|
327
|
+
report_out = out / f"optimization_report_{self.experiment_id}.html"
|
|
328
|
+
html_path_str = generate_html_report(self, output_path=str(report_out))
|
|
329
|
+
if html_path_str:
|
|
330
|
+
html_path = Path(html_path_str)
|
|
331
|
+
logger.info(f"📊 Report saved to {html_path}")
|
|
332
|
+
except Exception as exc:
|
|
333
|
+
logger.warning(f"HTML report generation failed: {exc}")
|
|
334
|
+
|
|
335
|
+
return json_path, html_path
|
|
336
|
+
|
|
302
337
|
|
|
303
338
|
# =====================================================================
|
|
304
339
|
# Prompt Optimizer — Main Engine
|
|
@@ -66,9 +66,10 @@ class LangGraphAdapter(StudioAdapter):
|
|
|
66
66
|
def capabilities(self) -> list[str]:
|
|
67
67
|
caps = ["graph", "streaming"]
|
|
68
68
|
try:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
if self._agent.graph_fn is not None:
|
|
70
|
+
graph = self._agent.graph_fn()
|
|
71
|
+
if getattr(graph, "checkpointer", None) is not None:
|
|
72
|
+
caps.extend(["checkpoints", "state", "breakpoints"])
|
|
72
73
|
except Exception:
|
|
73
74
|
pass
|
|
74
75
|
if self._agent.manifest.framework == "langgraph":
|
|
@@ -76,13 +77,15 @@ class LangGraphAdapter(StudioAdapter):
|
|
|
76
77
|
|
|
77
78
|
# Detect deep_agent capabilities from graph node names
|
|
78
79
|
try:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
if self._agent.graph_fn is not None:
|
|
81
|
+
graph = self._agent.graph_fn()
|
|
82
|
+
drawable = graph.get_graph()
|
|
83
|
+
node_names = [
|
|
84
|
+
getattr(n, "name", str(k)).lower()
|
|
85
|
+
for k, n in getattr(drawable, "nodes", {}).items()
|
|
86
|
+
]
|
|
87
|
+
if any("write_todo" in n or "task" in n for n in node_names):
|
|
88
|
+
caps.append("deep_agent")
|
|
86
89
|
caps.append("subagents")
|
|
87
90
|
caps.append("planning")
|
|
88
91
|
except Exception:
|
|
@@ -95,6 +98,8 @@ class LangGraphAdapter(StudioAdapter):
|
|
|
95
98
|
# ------------------------------------------------------------------
|
|
96
99
|
|
|
97
100
|
async def get_graph(self) -> StudioGraphTopology:
|
|
101
|
+
if self._agent.graph_fn is None:
|
|
102
|
+
raise ValueError(f"LangGraph agent '{self.agent_name}' has no graph_fn")
|
|
98
103
|
graph = self._agent.graph_fn()
|
|
99
104
|
drawable = graph.get_graph()
|
|
100
105
|
|
|
@@ -161,6 +166,8 @@ class LangGraphAdapter(StudioAdapter):
|
|
|
161
166
|
breakpoints: list[str] | None = None,
|
|
162
167
|
checkpoint_id: str | None = None,
|
|
163
168
|
) -> AsyncGenerator[StudioRunEvent, None]:
|
|
169
|
+
if self._agent.graph_fn is None:
|
|
170
|
+
raise ValueError(f"LangGraph agent '{self.agent_name}' has no graph_fn")
|
|
164
171
|
graph = self._agent.graph_fn()
|
|
165
172
|
config = dict(config or {})
|
|
166
173
|
|
|
@@ -204,10 +211,11 @@ class LangGraphAdapter(StudioAdapter):
|
|
|
204
211
|
checkpoint_id: str | None = None
|
|
205
212
|
|
|
206
213
|
try:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
214
|
+
if self._agent.graph_fn is not None:
|
|
215
|
+
graph = self._agent.graph_fn()
|
|
216
|
+
checkpointer = getattr(graph, "checkpointer", None)
|
|
217
|
+
if checkpointer is not None:
|
|
218
|
+
cfg = {"configurable": {"thread_id": thread_id, "checkpoint_ns": ""}}
|
|
211
219
|
if hasattr(checkpointer, "aget_tuple"):
|
|
212
220
|
cp_tuple = await checkpointer.aget_tuple(cfg)
|
|
213
221
|
elif hasattr(checkpointer, "get_tuple"):
|
|
@@ -253,11 +261,12 @@ class LangGraphAdapter(StudioAdapter):
|
|
|
253
261
|
merged = {**(current.state if current else {}), **updates}
|
|
254
262
|
|
|
255
263
|
try:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
264
|
+
if self._agent.graph_fn is not None:
|
|
265
|
+
graph = self._agent.graph_fn()
|
|
266
|
+
if hasattr(graph, "update_state"):
|
|
267
|
+
cfg = {"configurable": {"thread_id": thread_id}}
|
|
268
|
+
await graph.aupdate_state(cfg, updates)
|
|
269
|
+
logger.debug(f"State updated via LangGraph checkpointer for {thread_id}")
|
|
261
270
|
except Exception as exc:
|
|
262
271
|
logger.warning(f"LangGraph update_state failed: {exc}")
|
|
263
272
|
|
|
@@ -171,6 +171,7 @@ class RunTracker:
|
|
|
171
171
|
timestamp=_now_iso(),
|
|
172
172
|
data={
|
|
173
173
|
"agent": run.agent_name,
|
|
174
|
+
"thread_id": thread_id,
|
|
174
175
|
"input": run.input,
|
|
175
176
|
"capabilities": adapter.capabilities,
|
|
176
177
|
},
|
|
@@ -179,15 +180,19 @@ class RunTracker:
|
|
|
179
180
|
yield f"data: {start_event.model_dump_json()}\n\n"
|
|
180
181
|
|
|
181
182
|
try:
|
|
183
|
+
last_output: dict[str, Any] = {}
|
|
182
184
|
async for event in adapter.stream_execution(state, config, breakpoints, checkpoint_id):
|
|
183
185
|
# Stamp the run_id onto adapter events
|
|
184
186
|
event.run_id = run_id
|
|
185
187
|
self.add_event(run_id, event)
|
|
186
188
|
yield f"data: {event.model_dump_json()}\n\n"
|
|
189
|
+
# Track the latest node_end output as the run result
|
|
190
|
+
if event.event == "node_end" and event.data:
|
|
191
|
+
last_output = event.data
|
|
187
192
|
|
|
188
193
|
# -- Run complete --
|
|
189
194
|
duration = (time.monotonic() - start_time) * 1000
|
|
190
|
-
self.complete_run(run_id, state, duration)
|
|
195
|
+
self.complete_run(run_id, last_output or state, duration)
|
|
191
196
|
|
|
192
197
|
run = self._runs.get(run_id)
|
|
193
198
|
complete_event = StudioRunEvent(
|
|
@@ -24,6 +24,17 @@ from loguru import logger
|
|
|
24
24
|
if TYPE_CHECKING:
|
|
25
25
|
from fastapi import FastAPI
|
|
26
26
|
|
|
27
|
+
# ── Runtime FastAPI imports ──────────────────────────────────────────
|
|
28
|
+
# These MUST live at module level so that ``from __future__ import
|
|
29
|
+
# annotations`` (which turns every annotation into a lazy string) can
|
|
30
|
+
# still be resolved by FastAPI's dependency-injection machinery.
|
|
31
|
+
# When they were imported *locally* inside ``mount_studio_ui()``,
|
|
32
|
+
# FastAPI could not find ``Request`` in the module globals and fell
|
|
33
|
+
# back to treating it as a required query parameter → 422.
|
|
34
|
+
from fastapi import Request # noqa: E402
|
|
35
|
+
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse # noqa: E402
|
|
36
|
+
from fastapi.staticfiles import StaticFiles # noqa: E402
|
|
37
|
+
|
|
27
38
|
# Directory containing the built React app (index.html, static/js, static/css)
|
|
28
39
|
STATIC_DIR = Path(__file__).parent / "static"
|
|
29
40
|
|
|
@@ -53,10 +64,6 @@ def mount_studio_ui(app: FastAPI, path_prefix: str = "/studio/ui") -> None:
|
|
|
53
64
|
)
|
|
54
65
|
return
|
|
55
66
|
|
|
56
|
-
from fastapi import Request
|
|
57
|
-
from fastapi.responses import FileResponse, HTMLResponse
|
|
58
|
-
from fastapi.staticfiles import StaticFiles
|
|
59
|
-
|
|
60
67
|
# Normalise prefix
|
|
61
68
|
prefix = path_prefix.rstrip("/")
|
|
62
69
|
|
|
@@ -72,7 +79,7 @@ def mount_studio_ui(app: FastAPI, path_prefix: str = "/studio/ui") -> None:
|
|
|
72
79
|
)
|
|
73
80
|
|
|
74
81
|
# Serve root-level assets (favicon, manifest, robots.txt, etc.)
|
|
75
|
-
@app.get(f"{prefix}/{{filename:path}}")
|
|
82
|
+
@app.get(f"{prefix}/{{filename:path}}", response_model=None)
|
|
76
83
|
async def studio_spa(request: Request, filename: str) -> FileResponse | HTMLResponse:
|
|
77
84
|
"""Serve Studio UI files or fallback to index.html for SPA routing."""
|
|
78
85
|
# Try to serve the exact file
|
|
@@ -84,10 +91,8 @@ def mount_studio_ui(app: FastAPI, path_prefix: str = "/studio/ui") -> None:
|
|
|
84
91
|
|
|
85
92
|
# Redirect /studio/ui to /studio/ui/ for consistency
|
|
86
93
|
@app.get(prefix, include_in_schema=False)
|
|
87
|
-
async def studio_root_redirect() ->
|
|
94
|
+
async def studio_root_redirect() -> RedirectResponse:
|
|
88
95
|
"""Redirect bare path to trailing-slash version."""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return RedirectResponse(url=f"{prefix}/") # type: ignore[return-value]
|
|
96
|
+
return RedirectResponse(url=f"{prefix}/")
|
|
92
97
|
|
|
93
98
|
logger.info(f"🎨 Studio UI mounted at {prefix}/")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": {
|
|
3
|
+
"main.css": "/studio/ui/static/css/main.8b3c42dd.css",
|
|
4
|
+
"main.js": "/studio/ui/static/js/main.cbb2a8ee.js",
|
|
5
|
+
"index.html": "/studio/ui/index.html",
|
|
6
|
+
"main.8b3c42dd.css.map": "/studio/ui/static/css/main.8b3c42dd.css.map",
|
|
7
|
+
"main.cbb2a8ee.js.map": "/studio/ui/static/js/main.cbb2a8ee.js.map"
|
|
8
|
+
},
|
|
9
|
+
"entrypoints": [
|
|
10
|
+
"static/css/main.8b3c42dd.css",
|
|
11
|
+
"static/js/main.cbb2a8ee.js"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#7c3aed"/><meta name="description" content="Agentomatic Studio - Visual interface for debugging and testing Agentomatic agents"/><link rel="manifest" href="/studio/ui/manifest.json"/><title>Agentomatic Studio</title><script defer="defer" src="/studio/ui/static/js/main.
|
|
1
|
+
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#7c3aed"/><meta name="description" content="Agentomatic Studio - Visual interface for debugging and testing Agentomatic agents"/><link rel="manifest" href="/studio/ui/manifest.json"/><title>Agentomatic Studio</title><script defer="defer" src="/studio/ui/static/js/main.cbb2a8ee.js"></script><link href="/studio/ui/static/css/main.8b3c42dd.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|