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.
Files changed (108) hide show
  1. {agentomatic-0.4.1 → agentomatic-0.5.0}/PKG-INFO +1 -1
  2. {agentomatic-0.4.1 → agentomatic-0.5.0}/pyproject.toml +2 -1
  3. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/_version.py +1 -1
  4. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/lifespan.py +6 -0
  5. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/manifest.py +6 -0
  6. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/memory_manager.py +37 -6
  7. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/platform.py +57 -9
  8. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/registry.py +4 -0
  9. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/router_factory.py +26 -14
  10. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/state.py +3 -1
  11. agentomatic-0.5.0/src/agentomatic/middleware/__init__.py +36 -0
  12. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/middleware/feedback.py +5 -0
  13. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/config.py +26 -0
  14. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/optimizer.py +35 -0
  15. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/adapters/langgraph.py +28 -19
  16. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/run_tracker.py +6 -1
  17. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/serve.py +14 -9
  18. agentomatic-0.5.0/src/agentomatic/studio/static/asset-manifest.json +13 -0
  19. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/static/index.html +1 -1
  20. agentomatic-0.5.0/src/agentomatic/studio/static/static/css/main.8b3c42dd.css +4 -0
  21. agentomatic-0.5.0/src/agentomatic/studio/static/static/css/main.8b3c42dd.css.map +1 -0
  22. agentomatic-0.5.0/src/agentomatic/studio/static/static/js/main.cbb2a8ee.js +3 -0
  23. agentomatic-0.5.0/src/agentomatic/studio/static/static/js/main.cbb2a8ee.js.map +1 -0
  24. agentomatic-0.5.0/tests/test_bugfixes_041.py +619 -0
  25. agentomatic-0.4.1/src/agentomatic/middleware/__init__.py +0 -18
  26. agentomatic-0.4.1/src/agentomatic/studio/static/asset-manifest.json +0 -13
  27. agentomatic-0.4.1/src/agentomatic/studio/static/static/css/main.8692ac74.css +0 -4
  28. agentomatic-0.4.1/src/agentomatic/studio/static/static/css/main.8692ac74.css.map +0 -1
  29. agentomatic-0.4.1/src/agentomatic/studio/static/static/js/main.aa720e8f.js +0 -3
  30. agentomatic-0.4.1/src/agentomatic/studio/static/static/js/main.aa720e8f.js.map +0 -1
  31. {agentomatic-0.4.1 → agentomatic-0.5.0}/LICENSE +0 -0
  32. {agentomatic-0.4.1 → agentomatic-0.5.0}/README.md +0 -0
  33. {agentomatic-0.4.1 → agentomatic-0.5.0}/examples/full_agent/README.md +0 -0
  34. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/__init__.py +0 -0
  35. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/cli/__init__.py +0 -0
  36. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/cli/commands.py +0 -0
  37. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/cli/templates.py +0 -0
  38. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/config/__init__.py +0 -0
  39. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/config/defaults.py +0 -0
  40. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/config/settings.py +0 -0
  41. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/core/__init__.py +0 -0
  42. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/demo/__init__.py +0 -0
  43. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/demo/agent.py +0 -0
  44. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/demo/server.py +0 -0
  45. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/middleware/auth.py +0 -0
  46. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/middleware/logging.py +0 -0
  47. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/middleware/metrics.py +0 -0
  48. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/middleware/rate_limit.py +0 -0
  49. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/observability/__init__.py +0 -0
  50. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/observability/concurrency.py +0 -0
  51. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/observability/metrics.py +0 -0
  52. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/observability/telemetry.py +0 -0
  53. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/__init__.py +0 -0
  54. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/dataset.py +0 -0
  55. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/deployment.py +0 -0
  56. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/eval_contract.py +0 -0
  57. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/failure_analysis.py +0 -0
  58. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/fitter.py +0 -0
  59. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/fitter_optimizers.py +0 -0
  60. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/judges.py +0 -0
  61. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/llm_caller.py +0 -0
  62. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/loop.py +0 -0
  63. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/metrics.py +0 -0
  64. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/report.py +0 -0
  65. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/runner.py +0 -0
  66. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/search_space.py +0 -0
  67. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/strategies.py +0 -0
  68. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/optimize/synthesizer.py +0 -0
  69. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/prompts/__init__.py +0 -0
  70. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/prompts/manager.py +0 -0
  71. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/protocols/__init__.py +0 -0
  72. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/protocols/decorators.py +0 -0
  73. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/providers/__init__.py +0 -0
  74. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/providers/embeddings.py +0 -0
  75. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/providers/llm.py +0 -0
  76. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/py.typed +0 -0
  77. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/storage/__init__.py +0 -0
  78. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/storage/base.py +0 -0
  79. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/storage/checkpointer.py +0 -0
  80. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/storage/memory.py +0 -0
  81. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/storage/models.py +0 -0
  82. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/storage/sqlalchemy.py +0 -0
  83. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/__init__.py +0 -0
  84. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/adapter.py +0 -0
  85. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/adapters/__init__.py +0 -0
  86. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/adapters/generic.py +0 -0
  87. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/adapters/langchain.py +0 -0
  88. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/decorators.py +0 -0
  89. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/graph_inspector.py +0 -0
  90. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/models.py +0 -0
  91. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/router.py +0 -0
  92. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/static/imgs/graph_view.png +0 -0
  93. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/static/imgs/logo.png +0 -0
  94. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/static/imgs/main_screen.png +0 -0
  95. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/static/imgs/main_screen_ok.png +0 -0
  96. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/studio/static/manifest.json +0 -0
  97. /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
  98. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/ui/__init__.py +0 -0
  99. {agentomatic-0.4.1 → agentomatic-0.5.0}/src/agentomatic/ui/chat.py +0 -0
  100. {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_agentomatic.py +0 -0
  101. {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_cli.py +0 -0
  102. {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_coverage_boost.py +0 -0
  103. {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_deep_agent.py +0 -0
  104. {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_fitter.py +0 -0
  105. {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_integration.py +0 -0
  106. {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_optimize.py +0 -0
  107. {agentomatic-0.4.1 → agentomatic-0.5.0}/tests/test_platform_features.py +0 -0
  108. {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.4.1
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.4.1"
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",
@@ -2,4 +2,4 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.4.1"
5
+ __version__ = "0.5.0"
@@ -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
- # Track which agents are already pre-registered (programmatic)
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
- # Ensure agents directory is importable
281
- parent = str(platform.agents_dir.parent)
282
- if parent not in sys.path:
283
- sys.path.insert(0, parent)
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
- from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
626
-
627
- lc_messages: list[Any] = []
628
- for msg in request.messages:
629
- role = msg.get("role", "user")
630
- content_val = msg.get("content", "")
631
- if role == "assistant":
632
- lc_messages.append(AIMessage(content=content_val))
633
- elif role == "system":
634
- lc_messages.append(SystemMessage(content=content_val))
635
- else:
636
- lc_messages.append(HumanMessage(content=content_val))
637
- lc_messages.append(HumanMessage(content=request.content))
638
- state["messages"] = lc_messages
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
- graph = self._agent.graph_fn()
70
- if getattr(graph, "checkpointer", None) is not None:
71
- caps.extend(["checkpoints", "state", "breakpoints"])
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
- drawable = graph.get_graph()
80
- node_names = [
81
- getattr(n, "name", str(k)).lower()
82
- for k, n in getattr(drawable, "nodes", {}).items()
83
- ]
84
- if any("write_todo" in n or "task" in n for n in node_names):
85
- caps.append("deep_agent")
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
- graph = self._agent.graph_fn()
208
- checkpointer = getattr(graph, "checkpointer", None)
209
- if checkpointer is not None:
210
- cfg = {"configurable": {"thread_id": thread_id, "checkpoint_ns": ""}}
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
- graph = self._agent.graph_fn()
257
- if hasattr(graph, "update_state"):
258
- cfg = {"configurable": {"thread_id": thread_id}}
259
- await graph.aupdate_state(cfg, updates)
260
- logger.debug(f"State updated via LangGraph checkpointer for {thread_id}")
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() -> HTMLResponse:
94
+ async def studio_root_redirect() -> RedirectResponse:
88
95
  """Redirect bare path to trailing-slash version."""
89
- from fastapi.responses import RedirectResponse
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.aa720e8f.js"></script><link href="/studio/ui/static/css/main.8692ac74.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
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>