autobots-devtools-shared-lib 0.4.0__tar.gz → 0.5.2__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 (68) hide show
  1. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/PKG-INFO +2 -1
  2. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/pyproject.toml +2 -1
  3. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/utils/format_utils.py +1 -1
  4. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/agents/agent_config_utils.py +127 -46
  5. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/agents/agent_meta.py +7 -5
  6. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/agents/middleware.py +29 -2
  7. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/services/structured_converter.py +9 -2
  8. autobots_devtools_shared_lib-0.5.2/src/autobots_devtools_shared_lib/dynagent/utils/schema_directive_resolver.py +328 -0
  9. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/README.md +0 -0
  10. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/__init__.py +0 -0
  11. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/__init__.py +0 -0
  12. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/config/__init__.py +0 -0
  13. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/config/jenkins_config.py +0 -0
  14. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/config/jenkins_constants.py +0 -0
  15. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/config/jenkins_loader.py +0 -0
  16. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/observability/__init__.py +0 -0
  17. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/observability/logging_utils.py +0 -0
  18. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/observability/otel_fastapi.py +0 -0
  19. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/observability/trace_metadata.py +0 -0
  20. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/observability/trace_propagation.py +0 -0
  21. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/observability/tracing.py +0 -0
  22. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/servers/__init__.py +0 -0
  23. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/servers/fileserver/README.md +0 -0
  24. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/servers/fileserver/__init__.py +0 -0
  25. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/servers/fileserver/app.py +0 -0
  26. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/servers/fileserver/config.py +0 -0
  27. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/servers/fileserver/models.py +0 -0
  28. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/services/__init__.py +0 -0
  29. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/services/context/README.md +0 -0
  30. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/services/context/__init__.py +0 -0
  31. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/services/context/cache_backed.py +0 -0
  32. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/services/context/db_repository.py +0 -0
  33. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/services/context/factory.py +0 -0
  34. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/services/context/in_memory.py +0 -0
  35. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/services/context/redis_store.py +0 -0
  36. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/services/context/store.py +0 -0
  37. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/tools/__init__.py +0 -0
  38. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/tools/context_tools.py +0 -0
  39. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/tools/format_tools.py +0 -0
  40. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/tools/fserver_client_tools.py +0 -0
  41. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/tools/jenkins_builtin_tools.py +0 -0
  42. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/tools/jenkins_pipeline_tools.py +0 -0
  43. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/utils/__init__.py +0 -0
  44. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/utils/context_utils.py +0 -0
  45. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/utils/fserver_client_utils.py +0 -0
  46. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/utils/jenkins_builtin_utils.py +0 -0
  47. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/utils/jenkins_http_utils.py +0 -0
  48. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/utils/jenkins_pipeline_utils.py +0 -0
  49. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/__init__.py +0 -0
  50. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/agents/__init__.py +0 -0
  51. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/agents/base_agent.py +0 -0
  52. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/agents/batch.py +0 -0
  53. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/agents/invocation_utils.py +0 -0
  54. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/config/__init__.py +0 -0
  55. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/config/dynagent_settings.py +0 -0
  56. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/llm/__init__.py +0 -0
  57. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/llm/llm.py +0 -0
  58. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/models/__init__.py +0 -0
  59. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/models/state.py +0 -0
  60. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/services/__init__.py +0 -0
  61. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/tools/__init__.py +0 -0
  62. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/tools/state_tools.py +0 -0
  63. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/tools/tool_registry.py +0 -0
  64. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/ui/__init__.py +0 -0
  65. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/ui/default_ui.py +0 -0
  66. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/ui/ui_utils.py +0 -0
  67. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/utils/__init__.py +0 -0
  68. {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autobots-devtools-shared-lib
3
- Version: 0.4.0
3
+ Version: 0.5.2
4
4
  Summary: Shared library functions to be used for all autobots projects
5
5
  License: MIT
6
6
  Author: Pralhad
@@ -25,6 +25,7 @@ Requires-Dist: opentelemetry-sdk (>=1.30.0,<2.0.0)
25
25
  Requires-Dist: pydantic-settings (>=2.10.1)
26
26
  Requires-Dist: python-dotenv (>=1.1.1)
27
27
  Requires-Dist: pyyaml (>=6.0.3)
28
+ Requires-Dist: referencing (>=0.37.0)
28
29
  Requires-Dist: uvicorn[standard] (>=0.32.0)
29
30
  Description-Content-Type: text/markdown
30
31
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "autobots-devtools-shared-lib"
3
- version = "0.4.0"
3
+ version = "0.5.2"
4
4
  description = "Shared library functions to be used for all autobots projects"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -10,6 +10,7 @@ license = {text = "MIT"}
10
10
  requires-python = ">=3.12,<4.0.0"
11
11
  dependencies = [
12
12
  "chainlit>=2.9.6",
13
+ "referencing>=0.37.0",
13
14
  "jsonschema>=4.26.0",
14
15
  "langchain>=1.0.0",
15
16
  "langchain-anthropic>=1.4.0",
@@ -43,7 +43,7 @@ def output_format_converter(
43
43
  )
44
44
 
45
45
  meta = AgentMeta.instance()
46
- schema = meta.schema_map.get(agent_name) # pyright: ignore[reportAttributeAccessIssue]
46
+ schema = meta.output_schema_map.get(agent_name) # pyright: ignore[reportAttributeAccessIssue]
47
47
 
48
48
  if schema is None:
49
49
  return f"Error: no output schema configured for agent '{agent_name}'"
@@ -2,7 +2,7 @@
2
2
  # ABOUTME: Reads agents.yaml and provides typed accessors for prompts, tools, schemas.
3
3
 
4
4
  import json
5
- from dataclasses import dataclass
5
+ from dataclasses import dataclass, field
6
6
  from pathlib import Path
7
7
  from typing import Any
8
8
 
@@ -10,6 +10,9 @@ import yaml
10
10
 
11
11
  from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
12
12
  from autobots_devtools_shared_lib.dynagent.config.dynagent_settings import get_dynagent_settings
13
+ from autobots_devtools_shared_lib.dynagent.utils.schema_directive_resolver import (
14
+ resolve_parent_with_directives,
15
+ )
13
16
 
14
17
  logger = get_logger(__name__)
15
18
 
@@ -17,11 +20,12 @@ __all__ = [
17
20
  "AgentConfig",
18
21
  "get_agent_list",
19
22
  "get_batch_enabled_agents",
23
+ "get_capabilities_map",
20
24
  "get_config_dir",
21
25
  "get_default_agent",
22
26
  "get_prompt_map",
23
- "get_schema_map",
24
- "get_schema_path_map",
27
+ "get_resolved_input_schema_map",
28
+ "get_resolved_output_schema_map",
25
29
  "get_tool_map",
26
30
  "load_agents_config",
27
31
  "load_prompt",
@@ -36,8 +40,13 @@ class AgentConfig:
36
40
  agent_id: str
37
41
  prompt: str
38
42
  tools: list[str]
43
+ # inputs: schema_name -> directive_filename (or None)
44
+ inputs: dict[str, str | None] = field(default_factory=dict)
45
+ # output: single-entry {schema_name -> directive_filename} map (or None)
46
+ output: dict[str, str | None] | None = None
47
+ # optional list of human-readable capabilities
48
+ capabilities: list[str] = field(default_factory=list)
39
49
  section: str | None = None
40
- output_schema: str | None = None
41
50
  approach: str | None = None
42
51
  dynamic: bool = False
43
52
  batch_enabled: bool = False
@@ -47,12 +56,44 @@ class AgentConfig:
47
56
  @classmethod
48
57
  def from_dict(cls, agent_id: str, data: dict[str, Any]) -> "AgentConfig":
49
58
  """Create AgentConfig from a dictionary."""
59
+
60
+ raw_inputs = data.get("inputs") or []
61
+ inputs: dict[str, str | None] = {}
62
+ for item in raw_inputs:
63
+ if not isinstance(item, dict):
64
+ continue
65
+ schema_name = item.get("schema")
66
+ if not schema_name:
67
+ continue
68
+ directives_name = item.get("directives")
69
+ inputs[schema_name] = directives_name
70
+
71
+ raw_output = data.get("output")
72
+ output_cfg: dict[str, str | None] | None = None
73
+ if isinstance(raw_output, dict):
74
+ schema_name_val = raw_output.get("schema")
75
+ directives_name_val = raw_output.get("directives")
76
+ if isinstance(schema_name_val, str) or isinstance(directives_name_val, str):
77
+ schema_key = schema_name_val if isinstance(schema_name_val, str) else ""
78
+ output_cfg = {
79
+ schema_key: directives_name_val
80
+ if isinstance(directives_name_val, str)
81
+ else None
82
+ }
83
+
84
+ raw_capabilities = data.get("capabilities")
85
+ capabilities: list[str] = []
86
+ if isinstance(raw_capabilities, list):
87
+ capabilities = [item for item in raw_capabilities if isinstance(item, str)]
88
+
50
89
  return cls(
51
90
  agent_id=agent_id,
52
91
  prompt=data.get("prompt", ""),
53
92
  tools=data.get("tools", []),
93
+ inputs=inputs,
94
+ output=output_cfg,
95
+ capabilities=capabilities,
54
96
  section=data.get("section"),
55
- output_schema=data.get("output_schema"),
56
97
  approach=data.get("approach"),
57
98
  dynamic=data.get("dynamic", False),
58
99
  batch_enabled=data.get("batch_enabled", False),
@@ -114,21 +155,15 @@ def load_prompt(name: str) -> str:
114
155
  return f"Error reading prompt: {e}"
115
156
 
116
157
 
117
- def load_schema(name: str) -> dict:
118
- """Read and parse a JSON schema file by name from the schemas/ directory.
119
-
120
- Args:
121
- name: Schema filename (e.g., "joke-output.json", "01-preface.json")
158
+ def load_schema(name: str, base_dir: Path | None = None) -> dict:
159
+ """Read and parse a JSON schema file by name.
122
160
 
123
- Returns:
124
- Parsed JSON schema as dictionary
125
-
126
- Raises:
127
- FileNotFoundError: If schema file doesn't exist
128
- ValueError: If JSON is invalid
161
+ When base_dir is provided, reads from base_dir/schemas; otherwise uses
162
+ the active config_dir/schemas.
129
163
  """
130
- config_dir = get_config_dir()
131
- schema_dir = Path(config_dir) / "schemas"
164
+ if base_dir is None:
165
+ base_dir = get_config_dir()
166
+ schema_dir = Path(base_dir) / "schemas"
132
167
  schema_file = schema_dir / name
133
168
 
134
169
  if not schema_file.exists():
@@ -149,6 +184,77 @@ def load_schema(name: str) -> dict:
149
184
  raise
150
185
 
151
186
 
187
+ def _build_parent_paths(schema_name: str) -> list[Path]:
188
+ """Compute candidate parent schema paths in common and domain-specific folders."""
189
+ config_dir = get_config_dir()
190
+ if not schema_name.endswith(".json"):
191
+ schema_name = f"{schema_name}.json"
192
+
193
+ domain_schema = Path(config_dir) / "schemas" / schema_name
194
+ common_schema = Path(config_dir).parent / "common" / "schemas" / schema_name
195
+ return [common_schema, domain_schema]
196
+
197
+
198
+ def _normalize_directive_filename(directive_name: str) -> str:
199
+ """Ensure directive filenames resolve to json files."""
200
+ return directive_name if directive_name.endswith(".json") else f"{directive_name}.json"
201
+
202
+
203
+ def get_resolved_input_schema_map() -> dict[str, dict[str, dict]]:
204
+ """Return {agent_name: {schema_key: resolved_schema_dict}} for all input schemas."""
205
+ config_dir = get_config_dir()
206
+ agents = load_agents_config()
207
+
208
+ result: dict[str, dict[str, dict]] = {}
209
+
210
+ for agent_name, cfg in agents.items():
211
+ per_agent: dict[str, dict] = {}
212
+ for schema_name, directive_name in cfg.inputs.items():
213
+ if not directive_name:
214
+ continue
215
+ parent_paths = _build_parent_paths(schema_name)
216
+ directive_path = (
217
+ config_dir / "directives" / _normalize_directive_filename(directive_name)
218
+ )
219
+ resolved = resolve_parent_with_directives(parent_paths, directive_path)
220
+ schema_key = Path(schema_name).stem
221
+ per_agent[schema_key] = resolved
222
+ result[agent_name] = per_agent
223
+
224
+ return result
225
+
226
+
227
+ def _resolve_output_schema_for_agent(cfg: AgentConfig, config_dir: Path) -> dict | None:
228
+ """Resolve a single agent's output schema from output schema+directive config."""
229
+ if cfg.output is None:
230
+ return None
231
+
232
+ # Expect at most one entry in the output map.
233
+ for schema_name, directive_name in cfg.output.items():
234
+ if not schema_name:
235
+ return None
236
+ parent_paths = _build_parent_paths(schema_name)
237
+ if directive_name:
238
+ directive_path = (
239
+ config_dir / "directives" / _normalize_directive_filename(directive_name)
240
+ )
241
+ return resolve_parent_with_directives(parent_paths, directive_path)
242
+
243
+ return resolve_parent_with_directives(parent_paths, None)
244
+
245
+ return None
246
+
247
+
248
+ def get_resolved_output_schema_map() -> dict[str, dict | None]:
249
+ """Return {agent_name: resolved output schema or None} for all agents."""
250
+ config_dir = get_config_dir()
251
+ agents = load_agents_config()
252
+ return {
253
+ agent_name: _resolve_output_schema_for_agent(cfg, config_dir)
254
+ for agent_name, cfg in agents.items()
255
+ }
256
+
257
+
152
258
  def get_agent_list() -> list[str]:
153
259
  """Return list of agent names from config."""
154
260
  return list(load_agents_config().keys())
@@ -159,34 +265,9 @@ def get_prompt_map() -> dict[str, str]:
159
265
  return {name: load_prompt(cfg.prompt) for name, cfg in load_agents_config().items()}
160
266
 
161
267
 
162
- def get_schema_path_map() -> dict[str, str | None]:
163
- """Return {agent_name: raw_schema_path_or_None}."""
164
- return {name: cfg.output_schema for name, cfg in load_agents_config().items()}
165
-
166
-
167
- def get_schema_map() -> dict[str, dict | None]:
168
- """Return {agent_name: parsed_schema_dict_or_None} loaded from schema files.
169
-
170
- Reads all schema files referenced in agents.yaml at startup.
171
- Agents without output_schema get None.
172
-
173
- Returns:
174
- Dictionary mapping agent names to parsed schema dicts
175
-
176
- Raises:
177
- FileNotFoundError: If a referenced schema file is missing
178
- ValueError: If a schema file contains invalid JSON
179
- """
180
- result = {}
181
- for agent_name, cfg in load_agents_config().items():
182
- if cfg.output_schema is None:
183
- result[agent_name] = None
184
- else:
185
- logger.info(f"Loading schema '{cfg.output_schema}' for agent '{agent_name}'")
186
- result[agent_name] = load_schema(cfg.output_schema)
187
-
188
- logger.info(f"Loaded {sum(1 for v in result.values() if v is not None)} schemas")
189
- return result
268
+ def get_capabilities_map() -> dict[str, list[str]]:
269
+ """Return {agent_name: capabilities[]} from agents config."""
270
+ return {name: cfg.capabilities for name, cfg in load_agents_config().items()}
190
271
 
191
272
 
192
273
  def get_tool_map() -> dict[str, list[Any]]:
@@ -1,5 +1,5 @@
1
1
  # ABOUTME: Singleton holding all agent configuration loaded at startup.
2
- # ABOUTME: Provides prompt_map, tool_map, and schema_path_map.
2
+ # ABOUTME: Provides prompt_map, tool_map, input/output schema maps, and capabilities.
3
3
 
4
4
  from __future__ import annotations
5
5
 
@@ -14,15 +14,17 @@ class AgentMeta:
14
14
  _instance: AgentMeta | None = None
15
15
  prompt_map: dict[str, str]
16
16
  tool_map: dict[str, list[Any]]
17
- schema_path_map: dict[str, str | None]
18
- schema_map: dict[str, dict | None]
17
+ input_schema_map: dict[str, dict[str, dict]]
18
+ output_schema_map: dict[str, dict | None]
19
+ capabilities_map: dict[str, list[str]]
19
20
  default_agent: str | None
20
21
 
21
22
  def __init__(self) -> None:
22
23
  self.prompt_map = _agent_config.get_prompt_map()
23
24
  self.tool_map = _agent_config.get_tool_map()
24
- self.schema_path_map = _agent_config.get_schema_path_map()
25
- self.schema_map = _agent_config.get_schema_map() # pyright: ignore[reportAttributeAccessIssue]
25
+ self.input_schema_map = _agent_config.get_resolved_input_schema_map()
26
+ self.output_schema_map = _agent_config.get_resolved_output_schema_map()
27
+ self.capabilities_map = _agent_config.get_capabilities_map()
26
28
  self.default_agent = _agent_config.get_default_agent()
27
29
 
28
30
  @classmethod
@@ -1,6 +1,7 @@
1
1
  # ABOUTME: Middleware that injects agent-specific prompts and tools on every LLM call.
2
2
  # ABOUTME: Reads current agent_name from state and overrides via AgentMeta.
3
3
 
4
+ import json
4
5
  from collections import defaultdict
5
6
  from collections.abc import Awaitable, Callable
6
7
 
@@ -26,7 +27,20 @@ async def inject_agent_async(
26
27
 
27
28
  # Format prompt safely — missing placeholders become empty strings
28
29
  raw_prompt = meta.prompt_map.get(agent_name, "")
29
- format_values = defaultdict(str, **request.state)
30
+
31
+ # Inject resolved input schemas (if any) as JSON strings using their schema keys.
32
+ input_schemas = meta.input_schema_map.get(agent_name, {})
33
+ input_directives_map = {
34
+ schema_key: json.dumps(schema, indent=2, sort_keys=True)
35
+ for schema_key, schema in input_schemas.items()
36
+ }
37
+
38
+ input_directives = {"input_schemas": input_directives_map}
39
+ output_directives = {"output_schema": meta.output_schema_map.get(agent_name, {}) or {}}
40
+
41
+ # request.state values take precedence on key collision, consistent with previous behavior.
42
+ combined_values = {**input_directives, **output_directives, **request.state}
43
+ format_values = defaultdict(str, **combined_values)
30
44
  system_prompt = raw_prompt.format_map(format_values)
31
45
 
32
46
  tools = meta.tool_map.get(agent_name, [])
@@ -52,7 +66,20 @@ def inject_agent_sync(
52
66
 
53
67
  # Format prompt safely — missing placeholders become empty strings
54
68
  raw_prompt = meta.prompt_map.get(agent_name, "")
55
- format_values = defaultdict(str, **request.state)
69
+
70
+ # Inject resolved input schemas (if any) as JSON strings using their schema keys.
71
+ input_schemas = meta.input_schema_map.get(agent_name, {})
72
+ input_directives_map = {
73
+ schema_key: json.dumps(schema, indent=2, sort_keys=True)
74
+ for schema_key, schema in input_schemas.items()
75
+ }
76
+
77
+ input_directives = {"input_schemas": input_directives_map}
78
+ output_directives = {"output_schema": meta.output_schema_map.get(agent_name, {}) or {}}
79
+
80
+ # request.state values take precedence on key collision, consistent with previous behavior.
81
+ combined_values = {**input_directives, **output_directives, **request.state}
82
+ format_values = defaultdict(str, **combined_values)
56
83
  system_prompt = raw_prompt.format_map(format_values)
57
84
 
58
85
  tools = meta.tool_map.get(agent_name, [])
@@ -9,6 +9,7 @@ from langchain.messages import ToolMessage
9
9
  from langchain_core.messages import BaseMessage
10
10
 
11
11
  from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
12
+ from autobots_devtools_shared_lib.dynagent.agents.agent_meta import AgentMeta
12
13
 
13
14
  logger = get_logger(__name__)
14
15
 
@@ -110,11 +111,17 @@ class StructuredOutputConverter:
110
111
  # Create conversion prompt
111
112
  conversion_prompt = self._create_conversion_prompt(filtered_messages)
112
113
 
114
+ # Prefer pre-resolved output schema from AgentMeta when available.
115
+ meta = AgentMeta.instance()
116
+ effective_schema = meta.output_schema_map.get(current_agent) or json_schema
117
+
113
118
  # Use structured output to convert
114
119
  try:
115
- schema_title = json_schema.get("title", "structured output")
120
+ schema_title = effective_schema.get("title", "structured output")
116
121
  logger.info(f"Converting conversation to {schema_title} for agent {current_agent}")
117
- structured_llm = self.model.with_structured_output(json_schema, method="json_schema")
122
+ structured_llm = self.model.with_structured_output(
123
+ effective_schema, method="json_schema"
124
+ )
118
125
  result = structured_llm.invoke(conversion_prompt)
119
126
 
120
127
  except Exception as e:
@@ -0,0 +1,328 @@
1
+ """Schema + directive resolver.
2
+
3
+ Responsible for:
4
+ - Loading parent schema JSON docs from one or more paths (common + domain).
5
+ - Deep-merging parents with domain overriding common.
6
+ - Applying directive JSON (directives: [...]) via JSON Pointer and x-fbp-pragmas.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import copy
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from referencing import Registry, Resource
17
+ from referencing.jsonschema import DRAFT202012
18
+
19
+ from autobots_devtools_shared_lib.common.observability.logging_utils import get_logger
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ def _resolve_json_pointer(document: Any, pointer: str) -> Any:
25
+ """Resolve a JSON Pointer (RFC 6901) against a document.
26
+
27
+ Raises ValueError if the pointer cannot be resolved.
28
+ """
29
+ if pointer == "":
30
+ return document
31
+ if pointer == "/":
32
+ # Directive convention: "/" means the root document, not the RFC 6901 empty-string key.
33
+ # Schemas in this codebase never have an empty-string key, so treat "/" as root.
34
+ return document
35
+
36
+ if not pointer.startswith("/"):
37
+ raise ValueError(f"Invalid JSON Pointer (must start with '/'): {pointer}")
38
+
39
+ current = document
40
+ # Split on '/' and ignore the first empty segment
41
+ for raw_token in pointer.split("/")[1:]:
42
+ token = raw_token.replace("~1", "/").replace("~0", "~")
43
+ if isinstance(current, list):
44
+ try:
45
+ index = int(token)
46
+ except ValueError as e: # pragma: no cover - defensive
47
+ raise ValueError(
48
+ f"Non-numeric index '{token}' for list in pointer {pointer}"
49
+ ) from e
50
+ try:
51
+ current = current[index]
52
+ except IndexError as e:
53
+ raise ValueError(f"Index '{index}' out of range for pointer {pointer}") from e
54
+ elif isinstance(current, dict):
55
+ if token not in current:
56
+ raise ValueError(f"Key '{token}' not found while resolving pointer {pointer}")
57
+ current = current[token]
58
+ else: # pragma: no cover - defensive
59
+ raise TypeError(
60
+ f"Cannot traverse into non-container type at token '{token}' for pointer {pointer}"
61
+ )
62
+ return current
63
+
64
+
65
+ def _merge_pragmas(
66
+ target_node: dict[str, Any], pragma_obj: dict[str, list[str]], description: str | None
67
+ ) -> None:
68
+ """Merge x-fbp-pragmas into the target node in-place."""
69
+ existing_pragmas = target_node.get("x-fbp-pragmas")
70
+ if not isinstance(existing_pragmas, dict):
71
+ existing_pragmas = {}
72
+ target_node["x-fbp-pragmas"] = existing_pragmas
73
+
74
+ for scope, items in pragma_obj.items():
75
+ if not isinstance(items, list): # pragma: no cover - defensive
76
+ logger.warning("Expected list for pragma scope '%s', got %r", scope, items)
77
+ continue
78
+ existing_list = existing_pragmas.get(scope)
79
+ if not isinstance(existing_list, list):
80
+ existing_list = []
81
+ existing_pragmas[scope] = existing_list
82
+ existing_list.extend(items)
83
+
84
+ if description:
85
+ # Directive descriptions directly override/define the node description.
86
+ target_node["description"] = description
87
+
88
+
89
+ def _retrieve_from_path(base_dir: Path):
90
+ """Create a retriever that loads JSON schema files relative to a base directory."""
91
+
92
+ def _retrieve(uri: str) -> Resource:
93
+ from urllib.parse import unquote, urlparse
94
+
95
+ parsed = urlparse(uri)
96
+ path = Path(unquote(parsed.path))
97
+ if not path.is_absolute():
98
+ path = base_dir / path
99
+ contents = json.loads(path.read_text())
100
+ return Resource(contents=contents, specification=DRAFT202012)
101
+
102
+ return _retrieve
103
+
104
+
105
+ def _resolve_all_refs(schema: Any, resolver, _seen: frozenset[str] = frozenset()) -> Any:
106
+ """Recursively resolve all $ref in a schema, returning a plain dict.
107
+
108
+ Uses the referencing library's Resolver to look up each $ref URI,
109
+ then recurses into the resolved content with its updated resolver context
110
+ (critical for resolving relative $ref in nested files).
111
+
112
+ The ``_seen`` parameter tracks resolved URIs to prevent infinite recursion
113
+ from circular $ref chains.
114
+ """
115
+ if isinstance(schema, dict):
116
+ if "$ref" in schema:
117
+ ref = schema["$ref"]
118
+ if ref in _seen:
119
+ raise ValueError(f"Circular $ref detected: {ref}")
120
+ resolved = resolver.lookup(ref)
121
+ return _resolve_all_refs(resolved.contents, resolved.resolver, _seen | {ref})
122
+ return {k: _resolve_all_refs(v, resolver, _seen) for k, v in schema.items()}
123
+ if isinstance(schema, list):
124
+ return [_resolve_all_refs(item, resolver, _seen) for item in schema]
125
+ return schema
126
+
127
+
128
+ def _merge_parent_schemas(parent_docs: list[dict]) -> dict:
129
+ """Deep-merge a list of parent schema documents (domain overrides common)."""
130
+
131
+ def _merge(a: dict, b: dict) -> dict:
132
+ result: dict = copy.deepcopy(a)
133
+ for key, value in b.items():
134
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
135
+ result[key] = _merge(result[key], value)
136
+ else:
137
+ result[key] = copy.deepcopy(value)
138
+ return result
139
+
140
+ merged: dict = {}
141
+ for doc in parent_docs:
142
+ merged = _merge(merged, doc)
143
+ return merged
144
+
145
+
146
+ def _flatten_directive_entries(
147
+ entries: list[dict],
148
+ directive_dir: Path,
149
+ link_target: str = "",
150
+ _seen: frozenset[str] = frozenset(),
151
+ _sources: list[dict] | None = None,
152
+ ) -> list[tuple[str, dict, str | None]]:
153
+ """Flatten directive entries, resolving ``$ref`` links recursively.
154
+
155
+ Entries with ``$ref`` load the referenced directive file and rebase its entries
156
+ under the linking entry's ``target``. Recursion is guarded by ``_seen``.
157
+
158
+ Rebasing rule:
159
+ - Referenced ``/`` → ``link_target`` (or ``/`` at top level)
160
+ - Referenced ``/properties/x`` → ``link_target + /properties/x``
161
+
162
+ Args:
163
+ entries: The ``directives`` list from a directive document.
164
+ directive_dir: Directory for resolving relative ``$ref`` paths.
165
+ link_target: JSON Pointer prefix for all targets in ``entries``.
166
+ Empty string means no rebasing (top-level call).
167
+ _seen: Resolved directive file paths — guards against circular references.
168
+ _sources: Optional list; each loaded directive file appends its metadata here.
169
+
170
+ Returns:
171
+ Flat list of ``(pointer, pragma_obj, description)`` tuples.
172
+ """
173
+ result: list[tuple[str, dict, str | None]] = []
174
+
175
+ for entry in entries:
176
+ if not isinstance(entry, dict):
177
+ raise TypeError(f"Directive entries must be objects, got {type(entry)!r}")
178
+
179
+ has_ref = "$ref" in entry
180
+ has_pragmas = "x-fbp-pragmas" in entry
181
+
182
+ if not has_ref and not has_pragmas:
183
+ raise ValueError(
184
+ f"Directive entry must have 'x-fbp-pragmas' or '$ref' "
185
+ f"(target={entry.get('target')!r}): {entry}"
186
+ )
187
+
188
+ entry_target: str = entry.get("target", "/")
189
+
190
+ # Compute the absolute JSON Pointer for this entry.
191
+ if entry_target == "/":
192
+ abs_target = link_target if link_target else "/"
193
+ else:
194
+ abs_target = (link_target + entry_target) if link_target else entry_target
195
+
196
+ # Emit this entry's own x-fbp-pragmas at abs_target.
197
+ if has_pragmas:
198
+ result.append((abs_target, entry["x-fbp-pragmas"], entry.get("description")))
199
+
200
+ # Resolve $ref: load the referenced directive file and rebase its entries.
201
+ if has_ref:
202
+ ref: str = entry["$ref"]
203
+ ref_path = (directive_dir / ref).resolve()
204
+ ref_uri = str(ref_path)
205
+
206
+ if ref_uri in _seen:
207
+ raise ValueError(f"Circular $ref in directives detected: {ref!r}")
208
+
209
+ try:
210
+ ref_doc = json.loads(ref_path.read_text())
211
+ except (OSError, json.JSONDecodeError) as e:
212
+ raise ValueError(f"Failed to load directive $ref {ref!r}: {e}") from e
213
+
214
+ if _sources is not None:
215
+ _sources.append(
216
+ {
217
+ "id": ref_doc.get("id", ref_path.stem),
218
+ "title": ref_doc.get("title", ref_path.stem),
219
+ }
220
+ )
221
+
222
+ sub_entries = ref_doc.get("directives", [])
223
+ if not isinstance(sub_entries, list):
224
+ raise TypeError(f"'directives' in {ref!r} must be a list")
225
+
226
+ result.extend(
227
+ _flatten_directive_entries(
228
+ sub_entries,
229
+ ref_path.parent,
230
+ abs_target,
231
+ _seen | {ref_uri},
232
+ _sources,
233
+ )
234
+ )
235
+
236
+ return result
237
+
238
+
239
+ def resolve_parent_with_directives(parent_paths: list[Path], directive_path: None | Path) -> dict:
240
+ """Load parent schema(s), merge common+domain, then apply directives.
241
+
242
+ Args:
243
+ parent_paths: Ordered [common_parent, domain_parent] schema file paths.
244
+ directive_path: Path to directive JSON with a top-level 'directives' array.
245
+ """
246
+ parent_docs: list[dict] = []
247
+ for path in parent_paths:
248
+ if path.exists():
249
+ try:
250
+ with Path.open(path) as f:
251
+ parent_docs.append(json.load(f))
252
+ except json.JSONDecodeError as e:
253
+ error_msg = f"Invalid JSON in schema {path}: {e}"
254
+ logger.exception(error_msg)
255
+ raise ValueError(error_msg) from e
256
+
257
+ if not parent_docs:
258
+ error_msg = f"No parent schema files found at {[str(p) for p in parent_paths]}"
259
+ logger.error(error_msg)
260
+ raise FileNotFoundError(error_msg)
261
+
262
+ merged_parent = _merge_parent_schemas(parent_docs)
263
+
264
+ if not directive_path or not directive_path.exists():
265
+ error_msg = f"Directive file not found: {directive_path}"
266
+ # logger.error(error_msg)
267
+ # raise FileNotFoundError(error_msg)
268
+ return merged_parent
269
+
270
+ try:
271
+ with Path.open(directive_path) as f:
272
+ directive_doc = json.load(f)
273
+ except json.JSONDecodeError as e:
274
+ error_msg = f"Invalid JSON in directive {directive_path}: {e}"
275
+ logger.exception(error_msg)
276
+ raise ValueError(error_msg) from e
277
+
278
+ entries = directive_doc.get("directives", [])
279
+ if not isinstance(entries, list):
280
+ raise TypeError(f"'directives' must be a list in directive file '{directive_path.name}'")
281
+
282
+ # Resolve $ref file references so JSON Pointer directives can navigate into them.
283
+ # Determine the base directory from the last existing parent path for relative $ref.
284
+ base_dir = parent_paths[0].resolve().parent
285
+ for path in reversed(parent_paths):
286
+ if path.exists():
287
+ base_dir = path.resolve().parent
288
+ break
289
+
290
+ registry: Registry = Registry(retrieve=_retrieve_from_path(base_dir))
291
+ base_uri = base_dir.as_uri() + "/"
292
+ resolver = registry.resolver(base_uri)
293
+ merged = _resolve_all_refs(merged_parent, resolver)
294
+
295
+ sources = merged.get("x-fbp-directive-sources")
296
+ if not isinstance(sources, list):
297
+ sources = []
298
+ merged["x-fbp-directive-sources"] = sources
299
+
300
+ directive_source = {
301
+ "id": directive_doc.get("id", directive_path.stem),
302
+ "title": directive_doc.get("title", directive_path.stem),
303
+ }
304
+ sources.append(directive_source)
305
+
306
+ transitive_sources: list[dict] = []
307
+ flat_entries = _flatten_directive_entries(
308
+ entries, directive_path.parent, _sources=transitive_sources
309
+ )
310
+ sources.extend(transitive_sources)
311
+
312
+ for pointer, pragma_obj, description in flat_entries:
313
+ if not isinstance(pragma_obj, dict):
314
+ raise TypeError(
315
+ f"'x-fbp-pragmas' must be an object in directive '{directive_path.name}' "
316
+ f"for target '{pointer}', got {type(pragma_obj)!r}"
317
+ )
318
+
319
+ try:
320
+ target_node = _resolve_json_pointer(merged, pointer)
321
+ except ValueError as e:
322
+ error_msg = f"Failed to resolve JSON Pointer '{pointer}' in directive '{directive_path.name}': {e}"
323
+ logger.exception(error_msg)
324
+ raise ValueError(error_msg) from e
325
+
326
+ _merge_pragmas(target_node, pragma_obj, description)
327
+
328
+ return merged