tactus 0.36.0__py3-none-any.whl → 0.38.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tactus/__init__.py +1 -1
- tactus/adapters/channels/base.py +22 -2
- tactus/adapters/channels/broker.py +1 -0
- tactus/adapters/channels/host.py +3 -1
- tactus/adapters/channels/ipc.py +18 -3
- tactus/adapters/channels/sse.py +2 -0
- tactus/adapters/mcp_manager.py +24 -7
- tactus/backends/http_backend.py +2 -2
- tactus/backends/pytorch_backend.py +2 -2
- tactus/broker/client.py +3 -3
- tactus/broker/server.py +17 -5
- tactus/cli/app.py +212 -57
- tactus/core/compaction.py +17 -0
- tactus/core/context_assembler.py +73 -0
- tactus/core/context_models.py +41 -0
- tactus/core/dsl_stubs.py +560 -20
- tactus/core/exceptions.py +8 -0
- tactus/core/execution_context.py +24 -24
- tactus/core/message_history_manager.py +2 -2
- tactus/core/mocking.py +12 -0
- tactus/core/output_validator.py +6 -6
- tactus/core/registry.py +171 -29
- tactus/core/retrieval.py +317 -0
- tactus/core/retriever_tasks.py +30 -0
- tactus/core/runtime.py +431 -117
- tactus/dspy/agent.py +143 -82
- tactus/dspy/broker_lm.py +13 -7
- tactus/dspy/config.py +23 -4
- tactus/dspy/module.py +12 -1
- tactus/ide/coding_assistant.py +2 -2
- tactus/primitives/handles.py +79 -7
- tactus/primitives/model.py +1 -1
- tactus/primitives/procedure.py +1 -1
- tactus/primitives/state.py +2 -2
- tactus/sandbox/config.py +1 -1
- tactus/sandbox/container_runner.py +13 -6
- tactus/sandbox/entrypoint.py +51 -8
- tactus/sandbox/protocol.py +5 -0
- tactus/stdlib/README.md +10 -1
- tactus/stdlib/biblicus/__init__.py +3 -0
- tactus/stdlib/biblicus/text.py +189 -0
- tactus/stdlib/tac/biblicus/text.tac +32 -0
- tactus/stdlib/tac/tactus/biblicus.spec.tac +179 -0
- tactus/stdlib/tac/tactus/corpora/base.tac +42 -0
- tactus/stdlib/tac/tactus/corpora/filesystem.tac +5 -0
- tactus/stdlib/tac/tactus/retrievers/base.tac +37 -0
- tactus/stdlib/tac/tactus/retrievers/embedding_index_file.tac +6 -0
- tactus/stdlib/tac/tactus/retrievers/embedding_index_inmemory.tac +6 -0
- tactus/stdlib/tac/tactus/retrievers/index.md +137 -0
- tactus/stdlib/tac/tactus/retrievers/init.tac +11 -0
- tactus/stdlib/tac/tactus/retrievers/sqlite_full_text_search.tac +6 -0
- tactus/stdlib/tac/tactus/retrievers/tf_vector.tac +6 -0
- tactus/testing/behave_integration.py +2 -0
- tactus/testing/context.py +10 -6
- tactus/testing/evaluation_runner.py +5 -5
- tactus/testing/steps/builtin.py +2 -2
- tactus/testing/test_runner.py +6 -4
- tactus/utils/asyncio_helpers.py +2 -1
- tactus/validation/semantic_visitor.py +357 -6
- tactus/validation/validator.py +142 -2
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/METADATA +9 -6
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/RECORD +65 -47
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/WHEEL +0 -0
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.36.0.dist-info → tactus-0.38.0.dist-info}/licenses/LICENSE +0 -0
tactus/core/exceptions.py
CHANGED
|
@@ -11,6 +11,14 @@ class TactusRuntimeError(Exception):
|
|
|
11
11
|
pass
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class TaskSelectionRequired(TactusRuntimeError):
|
|
15
|
+
"""Raised when multiple tasks are available and no default can be chosen."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, tasks: list[str]):
|
|
18
|
+
self.tasks = tasks
|
|
19
|
+
super().__init__("Multiple tasks available; select one explicitly.")
|
|
20
|
+
|
|
21
|
+
|
|
14
22
|
class ProcedureWaitingForHuman(Exception):
|
|
15
23
|
"""
|
|
16
24
|
Raised to exit workflow when waiting for human response.
|
tactus/core/execution_context.py
CHANGED
|
@@ -6,7 +6,7 @@ Uses pluggable storage and HITL handlers via protocols.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
|
-
from typing import Any, Callable
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
10
10
|
from datetime import datetime, timezone
|
|
11
11
|
import logging
|
|
12
12
|
import time
|
|
@@ -39,7 +39,7 @@ class ExecutionContext(ABC):
|
|
|
39
39
|
self,
|
|
40
40
|
fn: Callable[[], Any],
|
|
41
41
|
checkpoint_type: str,
|
|
42
|
-
source_info:
|
|
42
|
+
source_info: Optional[Dict[str, Any]] = None,
|
|
43
43
|
) -> Any:
|
|
44
44
|
"""
|
|
45
45
|
Execute fn with position-based checkpointing. On replay, return stored result.
|
|
@@ -59,9 +59,9 @@ class ExecutionContext(ABC):
|
|
|
59
59
|
self,
|
|
60
60
|
request_type: str,
|
|
61
61
|
message: str,
|
|
62
|
-
timeout_seconds: int
|
|
62
|
+
timeout_seconds: Optional[int],
|
|
63
63
|
default_value: Any,
|
|
64
|
-
options:
|
|
64
|
+
options: Optional[List[dict]],
|
|
65
65
|
metadata: dict,
|
|
66
66
|
) -> HITLResponse:
|
|
67
67
|
"""
|
|
@@ -121,7 +121,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
121
121
|
self,
|
|
122
122
|
procedure_id: str,
|
|
123
123
|
storage_backend: StorageBackend,
|
|
124
|
-
hitl_handler: HITLHandler
|
|
124
|
+
hitl_handler: Optional[HITLHandler] = None,
|
|
125
125
|
strict_determinism: bool = False,
|
|
126
126
|
log_handler=None,
|
|
127
127
|
):
|
|
@@ -145,14 +145,14 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
145
145
|
self._inside_checkpoint = False
|
|
146
146
|
|
|
147
147
|
# Run ID tracking for distinguishing between different executions
|
|
148
|
-
self.current_run_id: str
|
|
148
|
+
self.current_run_id: Optional[str] = None
|
|
149
149
|
|
|
150
150
|
# .tac file tracking for accurate source locations
|
|
151
|
-
self.current_tac_file: str
|
|
152
|
-
self.current_tac_content: str
|
|
151
|
+
self.current_tac_file: Optional[str] = None
|
|
152
|
+
self.current_tac_content: Optional[str] = None
|
|
153
153
|
|
|
154
154
|
# Lua sandbox reference for debug.getinfo access
|
|
155
|
-
self.lua_sandbox: Any
|
|
155
|
+
self.lua_sandbox: Optional[Any] = None
|
|
156
156
|
|
|
157
157
|
# Rich metadata for HITL notifications
|
|
158
158
|
self._initialize_run_metadata(procedure_id)
|
|
@@ -177,7 +177,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
177
177
|
"""Set the run_id for subsequent checkpoints in this execution."""
|
|
178
178
|
self.current_run_id = run_id
|
|
179
179
|
|
|
180
|
-
def set_tac_file(self, file_path: str, content: str
|
|
180
|
+
def set_tac_file(self, file_path: str, content: Optional[str] = None) -> None:
|
|
181
181
|
"""
|
|
182
182
|
Store the currently executing .tac file for accurate source location capture.
|
|
183
183
|
|
|
@@ -193,7 +193,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
193
193
|
self.lua_sandbox = lua_sandbox
|
|
194
194
|
|
|
195
195
|
def set_procedure_metadata(
|
|
196
|
-
self, procedure_name: str
|
|
196
|
+
self, procedure_name: Optional[str] = None, input_data: Any = None
|
|
197
197
|
) -> None:
|
|
198
198
|
"""
|
|
199
199
|
Set rich metadata for HITL notifications.
|
|
@@ -211,7 +211,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
211
211
|
self,
|
|
212
212
|
fn: Callable[[], Any],
|
|
213
213
|
checkpoint_type: str,
|
|
214
|
-
source_info:
|
|
214
|
+
source_info: Optional[Dict[str, Any]] = None,
|
|
215
215
|
) -> Any:
|
|
216
216
|
"""
|
|
217
217
|
Execute fn with position-based checkpointing and source tracking.
|
|
@@ -397,7 +397,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
397
397
|
except Exception as exception:
|
|
398
398
|
logger.warning("Failed to emit checkpoint event: %s", exception)
|
|
399
399
|
else:
|
|
400
|
-
logger.
|
|
400
|
+
logger.debug("[CHECKPOINT] No log_handler available to emit checkpoint event")
|
|
401
401
|
|
|
402
402
|
# Persist metadata
|
|
403
403
|
self.storage.save_procedure_metadata(self.procedure_id, self.metadata)
|
|
@@ -406,7 +406,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
406
406
|
|
|
407
407
|
def _get_code_context(
|
|
408
408
|
self, file_path: str, line_number: int, context_lines: int = 3
|
|
409
|
-
) -> str
|
|
409
|
+
) -> Optional[str]:
|
|
410
410
|
"""Read source file and extract surrounding lines for debugging."""
|
|
411
411
|
try:
|
|
412
412
|
with open(file_path, "r") as source_file:
|
|
@@ -421,9 +421,9 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
421
421
|
self,
|
|
422
422
|
request_type: str,
|
|
423
423
|
message: str,
|
|
424
|
-
timeout_seconds: int
|
|
424
|
+
timeout_seconds: Optional[int],
|
|
425
425
|
default_value: Any,
|
|
426
|
-
options:
|
|
426
|
+
options: Optional[List[dict]],
|
|
427
427
|
metadata: dict,
|
|
428
428
|
) -> HITLResponse:
|
|
429
429
|
"""
|
|
@@ -505,7 +505,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
505
505
|
async_procedure_handles[handle.procedure_id] = handle.to_dict()
|
|
506
506
|
self.storage.save_procedure_metadata(self.procedure_id, self.metadata)
|
|
507
507
|
|
|
508
|
-
def get_procedure_handle(self, procedure_id: str) ->
|
|
508
|
+
def get_procedure_handle(self, procedure_id: str) -> Optional[Dict[str, Any]]:
|
|
509
509
|
"""
|
|
510
510
|
Retrieve procedure handle.
|
|
511
511
|
|
|
@@ -609,7 +609,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
609
609
|
|
|
610
610
|
return run_id
|
|
611
611
|
|
|
612
|
-
def get_subject(self) -> str
|
|
612
|
+
def get_subject(self) -> Optional[str]:
|
|
613
613
|
"""
|
|
614
614
|
Return a human-readable subject line for this execution.
|
|
615
615
|
|
|
@@ -621,7 +621,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
621
621
|
return f"{self.procedure_name} (checkpoint {checkpoint_position})"
|
|
622
622
|
return f"Procedure {self.procedure_id} (checkpoint {checkpoint_position})"
|
|
623
623
|
|
|
624
|
-
def get_started_at(self) -> datetime
|
|
624
|
+
def get_started_at(self) -> Optional[datetime]:
|
|
625
625
|
"""
|
|
626
626
|
Return when this execution started.
|
|
627
627
|
|
|
@@ -630,7 +630,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
630
630
|
"""
|
|
631
631
|
return self._started_at
|
|
632
632
|
|
|
633
|
-
def get_input_summary(self) ->
|
|
633
|
+
def get_input_summary(self) -> Optional[Dict[str, Any]]:
|
|
634
634
|
"""
|
|
635
635
|
Return a summary of the initial input to this procedure.
|
|
636
636
|
|
|
@@ -647,7 +647,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
647
647
|
# Otherwise wrap it in a dict
|
|
648
648
|
return {"value": self._input_data}
|
|
649
649
|
|
|
650
|
-
def get_conversation_history(self) ->
|
|
650
|
+
def get_conversation_history(self) -> Optional[List[dict]]:
|
|
651
651
|
"""
|
|
652
652
|
Return conversation history if available.
|
|
653
653
|
|
|
@@ -658,7 +658,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
658
658
|
# in future implementations
|
|
659
659
|
return None
|
|
660
660
|
|
|
661
|
-
def get_prior_control_interactions(self) ->
|
|
661
|
+
def get_prior_control_interactions(self) -> Optional[List[dict]]:
|
|
662
662
|
"""
|
|
663
663
|
Return list of prior HITL interactions in this execution.
|
|
664
664
|
|
|
@@ -682,7 +682,7 @@ class BaseExecutionContext(ExecutionContext):
|
|
|
682
682
|
|
|
683
683
|
return hitl_checkpoints if hitl_checkpoints else None
|
|
684
684
|
|
|
685
|
-
def get_lua_source_line(self) -> int
|
|
685
|
+
def get_lua_source_line(self) -> Optional[int]:
|
|
686
686
|
"""
|
|
687
687
|
Get the current source line from Lua debug.getinfo.
|
|
688
688
|
|
|
@@ -768,7 +768,7 @@ class InMemoryExecutionContext(BaseExecutionContext):
|
|
|
768
768
|
and simple CLI workflows that don't need to survive restarts.
|
|
769
769
|
"""
|
|
770
770
|
|
|
771
|
-
def __init__(self, procedure_id: str, hitl_handler: HITLHandler
|
|
771
|
+
def __init__(self, procedure_id: str, hitl_handler: Optional[HITLHandler] = None):
|
|
772
772
|
"""
|
|
773
773
|
Initialize with in-memory storage.
|
|
774
774
|
|
|
@@ -8,7 +8,7 @@ Aligned with pydantic-ai's message_history concept.
|
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from datetime import datetime, timezone
|
|
11
|
-
from typing import Any, Optional
|
|
11
|
+
from typing import Any, Optional, Tuple
|
|
12
12
|
|
|
13
13
|
try:
|
|
14
14
|
from pydantic_ai.messages import ModelMessage
|
|
@@ -146,7 +146,7 @@ class MessageHistoryManager:
|
|
|
146
146
|
return self._apply_named_filter(messages, filter_name, filter_value)
|
|
147
147
|
|
|
148
148
|
@staticmethod
|
|
149
|
-
def _parse_filter_spec(filter_specification: Any) ->
|
|
149
|
+
def _parse_filter_spec(filter_specification: Any) -> Tuple[Optional[str], Any]:
|
|
150
150
|
if not isinstance(filter_specification, tuple) or len(filter_specification) < 2:
|
|
151
151
|
return None, None
|
|
152
152
|
|
tactus/core/mocking.py
CHANGED
|
@@ -13,6 +13,18 @@ import logging
|
|
|
13
13
|
from typing import Any, Optional, Union
|
|
14
14
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
|
+
_CURRENT_MOCK_MANAGER: Optional["MockManager"] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def set_current_mock_manager(manager: Optional["MockManager"]) -> None:
|
|
20
|
+
"""Set the globally accessible mock manager for stdlib helpers."""
|
|
21
|
+
global _CURRENT_MOCK_MANAGER
|
|
22
|
+
_CURRENT_MOCK_MANAGER = manager
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_current_mock_manager() -> Optional["MockManager"]:
|
|
26
|
+
"""Get the globally accessible mock manager for stdlib helpers."""
|
|
27
|
+
return _CURRENT_MOCK_MANAGER
|
|
16
28
|
|
|
17
29
|
|
|
18
30
|
@dataclass
|
tactus/core/output_validator.py
CHANGED
|
@@ -6,7 +6,7 @@ Enables type safety and composability for sub-agent workflows.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
from typing import Any, Optional
|
|
9
|
+
from typing import Any, Optional, Tuple
|
|
10
10
|
|
|
11
11
|
logger = logging.getLogger(__name__)
|
|
12
12
|
|
|
@@ -78,7 +78,7 @@ class OutputValidator:
|
|
|
78
78
|
logger.debug("OutputValidator initialized with %s output fields", field_count)
|
|
79
79
|
|
|
80
80
|
@staticmethod
|
|
81
|
-
def _unwrap_result(output: Any) ->
|
|
81
|
+
def _unwrap_result(output: Any) -> Tuple[Any, Optional[Any]]:
|
|
82
82
|
from tactus.protocols.result import TactusResult
|
|
83
83
|
|
|
84
84
|
wrapped_result = output if isinstance(output, TactusResult) else None
|
|
@@ -94,7 +94,7 @@ class OutputValidator:
|
|
|
94
94
|
|
|
95
95
|
@staticmethod
|
|
96
96
|
def _wrap_validated_output(
|
|
97
|
-
wrapped_result: Any
|
|
97
|
+
wrapped_result: Optional[Any],
|
|
98
98
|
validated_payload: Any,
|
|
99
99
|
) -> Any:
|
|
100
100
|
if wrapped_result is not None:
|
|
@@ -129,7 +129,7 @@ class OutputValidator:
|
|
|
129
129
|
def _validate_without_schema(
|
|
130
130
|
self,
|
|
131
131
|
output: Any,
|
|
132
|
-
wrapped_result: Any
|
|
132
|
+
wrapped_result: Optional[Any],
|
|
133
133
|
) -> Any:
|
|
134
134
|
"""Accept any output when no schema is defined."""
|
|
135
135
|
logger.debug("No output schema defined, skipping validation")
|
|
@@ -139,7 +139,7 @@ class OutputValidator:
|
|
|
139
139
|
def _validate_scalar_schema(
|
|
140
140
|
self,
|
|
141
141
|
output: Any,
|
|
142
|
-
wrapped_result: Any
|
|
142
|
+
wrapped_result: Optional[Any],
|
|
143
143
|
) -> Any:
|
|
144
144
|
"""Validate scalar outputs (`field.string{}` etc.)."""
|
|
145
145
|
# Lua tables are not valid scalar outputs.
|
|
@@ -168,7 +168,7 @@ class OutputValidator:
|
|
|
168
168
|
def _validate_structured_schema(
|
|
169
169
|
self,
|
|
170
170
|
output: Any,
|
|
171
|
-
wrapped_result: Any
|
|
171
|
+
wrapped_result: Optional[Any],
|
|
172
172
|
) -> Any:
|
|
173
173
|
"""Validate dict/table outputs against a schema."""
|
|
174
174
|
if hasattr(output, "items") or isinstance(output, dict):
|
tactus/core/registry.py
CHANGED
|
@@ -6,10 +6,17 @@ procedure declarations from .tac files.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
from typing import Any
|
|
9
|
+
from typing import Any, Dict, Optional, Union
|
|
10
10
|
|
|
11
11
|
from pydantic import BaseModel, Field, ValidationError, ConfigDict
|
|
12
12
|
|
|
13
|
+
from tactus.core.context_models import (
|
|
14
|
+
CompactorDeclaration,
|
|
15
|
+
ContextDeclaration,
|
|
16
|
+
CorpusDeclaration,
|
|
17
|
+
RetrieverDeclaration,
|
|
18
|
+
)
|
|
19
|
+
|
|
13
20
|
logger = logging.getLogger(__name__)
|
|
14
21
|
|
|
15
22
|
|
|
@@ -19,7 +26,7 @@ class OutputFieldDeclaration(BaseModel):
|
|
|
19
26
|
name: str
|
|
20
27
|
field_type: str = Field(alias="type") # string, number, boolean, array, object
|
|
21
28
|
required: bool = False
|
|
22
|
-
description: str
|
|
29
|
+
description: Optional[str] = None
|
|
23
30
|
|
|
24
31
|
model_config = ConfigDict(populate_by_name=True)
|
|
25
32
|
|
|
@@ -31,7 +38,7 @@ class MessageHistoryConfiguration(BaseModel):
|
|
|
31
38
|
"""
|
|
32
39
|
|
|
33
40
|
source: str = "own" # "own", "shared", or another agent's name
|
|
34
|
-
filter: Any
|
|
41
|
+
filter: Optional[Any] = None # Lua function reference or filter name
|
|
35
42
|
|
|
36
43
|
|
|
37
44
|
class AgentOutputSchema(BaseModel):
|
|
@@ -44,21 +51,21 @@ class AgentDeclaration(BaseModel):
|
|
|
44
51
|
"""Agent declaration from DSL."""
|
|
45
52
|
|
|
46
53
|
name: str
|
|
47
|
-
provider: str
|
|
48
|
-
model: str
|
|
49
|
-
system_prompt: str
|
|
50
|
-
initial_message: str
|
|
54
|
+
provider: Optional[str] = None
|
|
55
|
+
model: Union[str, Dict[str, Any]] = "gpt-4o"
|
|
56
|
+
system_prompt: Union[str, Any] # String with {markers} or Lua function
|
|
57
|
+
initial_message: Optional[str] = None
|
|
51
58
|
tools: list[Any] = Field(default_factory=list) # Tool/toolset references and expressions
|
|
52
59
|
inline_tools: list[dict[str, Any]] = Field(default_factory=list) # Inline tool definitions
|
|
53
|
-
output: AgentOutputSchema
|
|
54
|
-
message_history: MessageHistoryConfiguration
|
|
60
|
+
output: Optional[AgentOutputSchema] = None # Aligned with pydantic-ai
|
|
61
|
+
message_history: Optional[MessageHistoryConfiguration] = None
|
|
55
62
|
max_turns: int = 50
|
|
56
63
|
disable_streaming: bool = (
|
|
57
64
|
False # Disable streaming for models that don't support tools in streaming mode
|
|
58
65
|
)
|
|
59
|
-
temperature: float
|
|
60
|
-
max_tokens: int
|
|
61
|
-
model_type: str
|
|
66
|
+
temperature: Optional[float] = None
|
|
67
|
+
max_tokens: Optional[int] = None
|
|
68
|
+
model_type: Optional[str] = None # e.g., "chat", "responses" for reasoning models
|
|
62
69
|
|
|
63
70
|
model_config = ConfigDict(extra="allow")
|
|
64
71
|
|
|
@@ -69,9 +76,9 @@ class HITLDeclaration(BaseModel):
|
|
|
69
76
|
name: str
|
|
70
77
|
hitl_type: str = Field(alias="type") # approval, input, review
|
|
71
78
|
message: str
|
|
72
|
-
timeout: int
|
|
79
|
+
timeout: Optional[int] = None
|
|
73
80
|
default: Any = None
|
|
74
|
-
options: list[dict[str, Any]]
|
|
81
|
+
options: Optional[list[dict[str, Any]]] = None
|
|
75
82
|
|
|
76
83
|
model_config = ConfigDict(populate_by_name=True)
|
|
77
84
|
|
|
@@ -81,9 +88,9 @@ class ScenarioDeclaration(BaseModel):
|
|
|
81
88
|
|
|
82
89
|
name: str
|
|
83
90
|
given: dict[str, Any] = Field(default_factory=dict)
|
|
84
|
-
when: str
|
|
85
|
-
then_output:
|
|
86
|
-
then_state:
|
|
91
|
+
when: Optional[str] = None # defaults to "procedure_completes"
|
|
92
|
+
then_output: Optional[Dict[str, Any]] = None
|
|
93
|
+
then_state: Optional[Dict[str, Any]] = None
|
|
87
94
|
mocks: dict[str, Any] = Field(default_factory=dict) # tool_name -> response
|
|
88
95
|
|
|
89
96
|
|
|
@@ -128,13 +135,25 @@ class AgentMockConfig(BaseModel):
|
|
|
128
135
|
)
|
|
129
136
|
|
|
130
137
|
|
|
138
|
+
class TaskDeclaration(BaseModel):
|
|
139
|
+
"""Task declaration from DSL."""
|
|
140
|
+
|
|
141
|
+
name: str
|
|
142
|
+
children: dict[str, "TaskDeclaration"] = Field(default_factory=dict)
|
|
143
|
+
|
|
144
|
+
model_config = ConfigDict(extra="allow")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
TaskDeclaration.model_rebuild()
|
|
148
|
+
|
|
149
|
+
|
|
131
150
|
class ProcedureRegistry(BaseModel):
|
|
132
151
|
"""Collects all declarations from a .tac file."""
|
|
133
152
|
|
|
134
153
|
model_config = {"arbitrary_types_allowed": True}
|
|
135
154
|
|
|
136
155
|
# Metadata
|
|
137
|
-
description: str
|
|
156
|
+
description: Optional[str] = None
|
|
138
157
|
|
|
139
158
|
# Declarations
|
|
140
159
|
input_schema: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -149,31 +168,37 @@ class ProcedureRegistry(BaseModel):
|
|
|
149
168
|
dependencies: dict[str, DependencyDeclaration] = Field(default_factory=dict)
|
|
150
169
|
mocks: dict[str, dict[str, Any]] = Field(default_factory=dict) # Mock configurations
|
|
151
170
|
agent_mocks: dict[str, AgentMockConfig] = Field(default_factory=dict) # Agent mock configs
|
|
171
|
+
contexts: dict[str, ContextDeclaration] = Field(default_factory=dict)
|
|
172
|
+
corpora: dict[str, CorpusDeclaration] = Field(default_factory=dict)
|
|
173
|
+
retrievers: dict[str, RetrieverDeclaration] = Field(default_factory=dict)
|
|
174
|
+
compactors: dict[str, CompactorDeclaration] = Field(default_factory=dict)
|
|
175
|
+
tasks: dict[str, TaskDeclaration] = Field(default_factory=dict)
|
|
176
|
+
include_tasks: list[dict[str, Any]] = Field(default_factory=list)
|
|
152
177
|
|
|
153
178
|
# Message history configuration (aligned with pydantic-ai)
|
|
154
179
|
message_history_config: dict[str, Any] = Field(default_factory=dict)
|
|
155
180
|
|
|
156
181
|
# Gherkin BDD Testing
|
|
157
|
-
gherkin_specifications: str
|
|
182
|
+
gherkin_specifications: Optional[str] = None # Raw Gherkin text
|
|
158
183
|
specs_from_references: list[str] = Field(default_factory=list) # External spec file paths
|
|
159
184
|
custom_steps: dict[str, Any] = Field(default_factory=dict) # step_text -> lua_function
|
|
160
185
|
evaluation_config: dict[str, Any] = Field(default_factory=dict) # runs, parallel, etc.
|
|
161
186
|
|
|
162
187
|
# Pydantic Evals Integration
|
|
163
|
-
pydantic_evaluations:
|
|
188
|
+
pydantic_evaluations: Optional[Dict[str, Any]] = None # Pydantic Evals configuration
|
|
164
189
|
|
|
165
190
|
# Prompts
|
|
166
191
|
prompts: dict[str, str] = Field(default_factory=dict)
|
|
167
|
-
return_prompt: str
|
|
168
|
-
error_prompt: str
|
|
169
|
-
status_prompt: str
|
|
192
|
+
return_prompt: Optional[str] = None
|
|
193
|
+
error_prompt: Optional[str] = None
|
|
194
|
+
status_prompt: Optional[str] = None
|
|
170
195
|
|
|
171
196
|
# Execution settings
|
|
172
197
|
async_enabled: bool = False
|
|
173
198
|
max_depth: int = 5
|
|
174
199
|
max_turns: int = 50
|
|
175
|
-
default_provider: str
|
|
176
|
-
default_model: str
|
|
200
|
+
default_provider: Optional[str] = None
|
|
201
|
+
default_model: Optional[str] = None
|
|
177
202
|
|
|
178
203
|
# Named procedures (for in-file sub-procedures)
|
|
179
204
|
named_procedures: dict[str, dict[str, Any]] = Field(default_factory=dict)
|
|
@@ -193,8 +218,8 @@ class ValidationMessage(BaseModel):
|
|
|
193
218
|
|
|
194
219
|
level: str # "error" or "warning"
|
|
195
220
|
message: str
|
|
196
|
-
location: tuple[int, int]
|
|
197
|
-
declaration: str
|
|
221
|
+
location: Optional[tuple[int, int]] = None
|
|
222
|
+
declaration: Optional[str] = None
|
|
198
223
|
|
|
199
224
|
|
|
200
225
|
class ValidationResult(BaseModel):
|
|
@@ -203,7 +228,7 @@ class ValidationResult(BaseModel):
|
|
|
203
228
|
valid: bool
|
|
204
229
|
errors: list[ValidationMessage] = Field(default_factory=list)
|
|
205
230
|
warnings: list[ValidationMessage] = Field(default_factory=list)
|
|
206
|
-
registry: ProcedureRegistry
|
|
231
|
+
registry: Optional["ProcedureRegistry"] = None
|
|
207
232
|
|
|
208
233
|
|
|
209
234
|
class RegistryBuilder:
|
|
@@ -229,7 +254,7 @@ class RegistryBuilder:
|
|
|
229
254
|
self,
|
|
230
255
|
name: str,
|
|
231
256
|
config: dict,
|
|
232
|
-
output_schema: dict
|
|
257
|
+
output_schema: Optional[dict] = None,
|
|
233
258
|
) -> None:
|
|
234
259
|
"""Register an agent declaration."""
|
|
235
260
|
agent_config = dict(config)
|
|
@@ -334,6 +359,123 @@ class RegistryBuilder:
|
|
|
334
359
|
except Exception as exception:
|
|
335
360
|
self._add_error(f"Invalid agent mock config for '{agent_name}': {exception}")
|
|
336
361
|
|
|
362
|
+
def register_context(self, name: str, config: dict) -> None:
|
|
363
|
+
"""Register a context declaration."""
|
|
364
|
+
context_config = dict(config)
|
|
365
|
+
context_config["name"] = name
|
|
366
|
+
try:
|
|
367
|
+
self.registry.contexts[name] = ContextDeclaration(**context_config)
|
|
368
|
+
except ValidationError as exception:
|
|
369
|
+
self._add_error(f"Invalid context '{name}': {exception}")
|
|
370
|
+
|
|
371
|
+
def register_corpus(self, name: str, config: dict) -> None:
|
|
372
|
+
"""Register a corpus declaration."""
|
|
373
|
+
corpus_config = dict(config)
|
|
374
|
+
if "root" in corpus_config and "corpus_root" not in corpus_config:
|
|
375
|
+
corpus_config["corpus_root"] = corpus_config.pop("root")
|
|
376
|
+
try:
|
|
377
|
+
self.registry.corpora[name] = CorpusDeclaration(name=name, config=corpus_config)
|
|
378
|
+
except ValidationError as exception:
|
|
379
|
+
self._add_error(f"Invalid corpus '{name}': {exception}")
|
|
380
|
+
|
|
381
|
+
def register_retriever(self, name: str, config: dict) -> None:
|
|
382
|
+
"""Register a retriever declaration."""
|
|
383
|
+
retriever_config = dict(config)
|
|
384
|
+
if "retriever_id" not in retriever_config:
|
|
385
|
+
candidate = retriever_config.get("retriever_type")
|
|
386
|
+
if candidate is not None:
|
|
387
|
+
retriever_config["retriever_id"] = candidate
|
|
388
|
+
if isinstance(retriever_config.get("configuration"), dict):
|
|
389
|
+
pipeline = retriever_config["configuration"].get("pipeline", {}) or {}
|
|
390
|
+
if isinstance(pipeline, dict) and isinstance(pipeline.get("query"), dict):
|
|
391
|
+
query_config = pipeline.get("query") or {}
|
|
392
|
+
for key in (
|
|
393
|
+
"limit",
|
|
394
|
+
"offset",
|
|
395
|
+
"maximum_total_characters",
|
|
396
|
+
"maximum_items_per_source",
|
|
397
|
+
"max_items_per_source",
|
|
398
|
+
"include_metadata",
|
|
399
|
+
"metadata_fields",
|
|
400
|
+
"join_with",
|
|
401
|
+
):
|
|
402
|
+
if key in query_config and key not in retriever_config:
|
|
403
|
+
retriever_config[key] = query_config.get(key)
|
|
404
|
+
corpus_name = retriever_config.pop("corpus", None)
|
|
405
|
+
try:
|
|
406
|
+
self.registry.retrievers[name] = RetrieverDeclaration(
|
|
407
|
+
name=name,
|
|
408
|
+
corpus=corpus_name,
|
|
409
|
+
config=retriever_config,
|
|
410
|
+
)
|
|
411
|
+
except ValidationError as exception:
|
|
412
|
+
self._add_error(f"Invalid retriever '{name}': {exception}")
|
|
413
|
+
|
|
414
|
+
def register_task(
|
|
415
|
+
self,
|
|
416
|
+
name: str,
|
|
417
|
+
task_config: Optional[dict] = None,
|
|
418
|
+
parent: Optional[str] = None,
|
|
419
|
+
) -> None:
|
|
420
|
+
"""Register a task declaration (optionally nested under a parent task)."""
|
|
421
|
+
if not name:
|
|
422
|
+
self._add_error("Task name is required.")
|
|
423
|
+
return
|
|
424
|
+
|
|
425
|
+
if ":" in name:
|
|
426
|
+
self._add_error(f"Task name '{name}' may not contain ':'")
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
task_payload = dict(task_config or {})
|
|
430
|
+
task_payload["name"] = name
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
task = TaskDeclaration(**task_payload)
|
|
434
|
+
except ValidationError as exception:
|
|
435
|
+
self._add_error(f"Invalid task '{name}': {exception}")
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
if parent is None:
|
|
439
|
+
if name in self.registry.tasks:
|
|
440
|
+
self._add_error(f"Duplicate task '{name}'")
|
|
441
|
+
return
|
|
442
|
+
self.registry.tasks[name] = task
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
parent_task = self._find_task(parent)
|
|
446
|
+
if parent_task is None:
|
|
447
|
+
self._add_error(f"Parent task '{parent}' not found for '{name}'")
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
if name in parent_task.children:
|
|
451
|
+
self._add_error(f"Duplicate task '{parent}:{name}'")
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
parent_task.children[name] = task
|
|
455
|
+
|
|
456
|
+
def register_include_tasks(self, path: str, namespace: Optional[str] = None) -> None:
|
|
457
|
+
"""Register an IncludeTasks directive for static task discovery."""
|
|
458
|
+
payload = {"path": path}
|
|
459
|
+
if namespace:
|
|
460
|
+
payload["namespace"] = namespace
|
|
461
|
+
self.registry.include_tasks.append(payload)
|
|
462
|
+
|
|
463
|
+
def _find_task(self, name: str) -> Optional[TaskDeclaration]:
|
|
464
|
+
if name in self.registry.tasks:
|
|
465
|
+
return self.registry.tasks[name]
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
def register_compactor(self, name: str, config: dict) -> None:
|
|
469
|
+
"""Register a compactor declaration."""
|
|
470
|
+
compactor_config = dict(config)
|
|
471
|
+
try:
|
|
472
|
+
self.registry.compactors[name] = CompactorDeclaration(
|
|
473
|
+
name=name,
|
|
474
|
+
config=compactor_config,
|
|
475
|
+
)
|
|
476
|
+
except ValidationError as exception:
|
|
477
|
+
self._add_error(f"Invalid compactor '{name}': {exception}")
|
|
478
|
+
|
|
337
479
|
def register_specification(self, name: str, scenarios: list) -> None:
|
|
338
480
|
"""Register a BDD specification."""
|
|
339
481
|
try:
|