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.
- {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/PKG-INFO +2 -1
- {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/pyproject.toml +2 -1
- {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
- {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
- {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
- {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
- {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
- autobots_devtools_shared_lib-0.5.2/src/autobots_devtools_shared_lib/dynagent/utils/schema_directive_resolver.py +328 -0
- {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/README.md +0 -0
- {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/__init__.py +0 -0
- {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/common/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {autobots_devtools_shared_lib-0.4.0 → autobots_devtools_shared_lib-0.5.2}/src/autobots_devtools_shared_lib/dynagent/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
24
|
-
"
|
|
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
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
|
163
|
-
"""Return {agent_name:
|
|
164
|
-
return {name: cfg.
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
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.
|
|
25
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|