tactus 0.37.0__py3-none-any.whl → 0.39.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 +2 -0
- 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 +568 -17
- tactus/core/exceptions.py +8 -0
- tactus/core/execution_context.py +1 -1
- tactus/core/mocking.py +12 -0
- tactus/core/registry.py +142 -0
- tactus/core/retrieval.py +317 -0
- tactus/core/retriever_tasks.py +30 -0
- tactus/core/runtime.py +441 -75
- tactus/dspy/agent.py +143 -82
- tactus/dspy/config.py +16 -0
- tactus/dspy/module.py +12 -1
- tactus/ide/coding_assistant.py +2 -2
- tactus/plugins/__init__.py +3 -0
- tactus/plugins/noaa.py +76 -0
- tactus/primitives/handles.py +79 -7
- tactus/sandbox/config.py +1 -1
- tactus/sandbox/container_runner.py +2 -0
- 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 +208 -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 +4 -0
- tactus/validation/semantic_visitor.py +430 -88
- tactus/validation/validator.py +142 -2
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/METADATA +3 -2
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/RECORD +48 -28
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/WHEEL +0 -0
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/entry_points.txt +0 -0
- {tactus-0.37.0.dist-info → tactus-0.39.0.dist-info}/licenses/LICENSE +0 -0
tactus/primitives/handles.py
CHANGED
|
@@ -222,7 +222,7 @@ class AgentHandle:
|
|
|
222
222
|
return f"AgentHandle('{self.name}', {connected})"
|
|
223
223
|
|
|
224
224
|
|
|
225
|
-
class ModelHandle:
|
|
225
|
+
class ModelHandle:
|
|
226
226
|
"""
|
|
227
227
|
Lightweight handle returned by model() DSL function.
|
|
228
228
|
|
|
@@ -303,12 +303,84 @@ class ModelHandle:
|
|
|
303
303
|
self._primitive = primitive
|
|
304
304
|
logger.debug("ModelHandle '%s' connected to primitive", self.name)
|
|
305
305
|
|
|
306
|
-
def __repr__(self) -> str:
|
|
307
|
-
connected = "connected" if self._primitive else "disconnected"
|
|
308
|
-
return f"ModelHandle('{self.name}', {connected})"
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
class
|
|
306
|
+
def __repr__(self) -> str:
|
|
307
|
+
connected = "connected" if self._primitive else "disconnected"
|
|
308
|
+
return f"ModelHandle('{self.name}', {connected})"
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class ContextHandle:
|
|
312
|
+
"""
|
|
313
|
+
Lightweight handle returned by Context DSL declarations.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
def __init__(self, name: str):
|
|
317
|
+
"""
|
|
318
|
+
Initialize context handle.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
name: Context name (string identifier)
|
|
322
|
+
"""
|
|
323
|
+
self.name = name
|
|
324
|
+
|
|
325
|
+
def __repr__(self) -> str:
|
|
326
|
+
return f"ContextHandle('{self.name}')"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class CorpusHandle:
|
|
330
|
+
"""
|
|
331
|
+
Lightweight handle returned by Corpus DSL declarations.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
def __init__(self, name: str):
|
|
335
|
+
"""
|
|
336
|
+
Initialize corpus handle.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
name: Corpus name (string identifier)
|
|
340
|
+
"""
|
|
341
|
+
self.name = name
|
|
342
|
+
|
|
343
|
+
def __repr__(self) -> str:
|
|
344
|
+
return f"CorpusHandle('{self.name}')"
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class RetrieverHandle:
|
|
348
|
+
"""
|
|
349
|
+
Lightweight handle returned by Retriever DSL declarations.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
def __init__(self, name: str):
|
|
353
|
+
"""
|
|
354
|
+
Initialize retriever handle.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
name: Retriever name (string identifier)
|
|
358
|
+
"""
|
|
359
|
+
self.name = name
|
|
360
|
+
|
|
361
|
+
def __repr__(self) -> str:
|
|
362
|
+
return f"RetrieverHandle('{self.name}')"
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class CompactorHandle:
|
|
366
|
+
"""
|
|
367
|
+
Lightweight handle returned by Compactor DSL declarations.
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
def __init__(self, name: str):
|
|
371
|
+
"""
|
|
372
|
+
Initialize compactor handle.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
name: Compactor name (string identifier)
|
|
376
|
+
"""
|
|
377
|
+
self.name = name
|
|
378
|
+
|
|
379
|
+
def __repr__(self) -> str:
|
|
380
|
+
return f"CompactorHandle('{self.name}')"
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class AgentLookup:
|
|
312
384
|
"""
|
|
313
385
|
Agent lookup primitive - provides Agent("name") lookup functionality.
|
|
314
386
|
|
tactus/sandbox/config.py
CHANGED
|
@@ -113,7 +113,7 @@ class SandboxConfig(BaseModel):
|
|
|
113
113
|
|
|
114
114
|
# Development mode: mount live Tactus source code
|
|
115
115
|
dev_mode: bool = Field(
|
|
116
|
-
default=
|
|
116
|
+
default=True,
|
|
117
117
|
description="Enable development mode: mount live Tactus source code instead of using baked-in version",
|
|
118
118
|
)
|
|
119
119
|
|
|
@@ -358,6 +358,7 @@ class ContainerRunner:
|
|
|
358
358
|
source_file_path: Optional[str] = None,
|
|
359
359
|
working_dir: Optional[Path] = None,
|
|
360
360
|
format: str = "lua",
|
|
361
|
+
task_name: Optional[str] = None,
|
|
361
362
|
event_handler: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
362
363
|
callback_url: Optional[str] = None,
|
|
363
364
|
run_id: Optional[str] = None,
|
|
@@ -497,6 +498,7 @@ class ContainerRunner:
|
|
|
497
498
|
run_id=run_id,
|
|
498
499
|
source_file_path=source_file_path,
|
|
499
500
|
format=format,
|
|
501
|
+
task_name=task_name,
|
|
500
502
|
)
|
|
501
503
|
|
|
502
504
|
# Run container
|
tactus/sandbox/entrypoint.py
CHANGED
|
@@ -86,6 +86,7 @@ async def execute_procedure(
|
|
|
86
86
|
source_file_path: Optional[str] = None,
|
|
87
87
|
format: str = "lua",
|
|
88
88
|
run_id: Optional[str] = None,
|
|
89
|
+
task_name: Optional[str] = None,
|
|
89
90
|
) -> Any:
|
|
90
91
|
"""
|
|
91
92
|
Execute a procedure using TactusRuntime.
|
|
@@ -163,6 +164,7 @@ async def execute_procedure(
|
|
|
163
164
|
source=source,
|
|
164
165
|
context=params,
|
|
165
166
|
format=format,
|
|
167
|
+
task_name=task_name,
|
|
166
168
|
)
|
|
167
169
|
|
|
168
170
|
# CRITICAL: Flush pending log events before returning
|
|
@@ -181,6 +183,7 @@ async def main_async() -> int:
|
|
|
181
183
|
from tactus.sandbox.protocol import (
|
|
182
184
|
ExecutionRequest,
|
|
183
185
|
ExecutionResult,
|
|
186
|
+
ExecutionStatus,
|
|
184
187
|
)
|
|
185
188
|
|
|
186
189
|
start_time = time.time()
|
|
@@ -200,14 +203,54 @@ async def main_async() -> int:
|
|
|
200
203
|
request = ExecutionRequest(**request_data)
|
|
201
204
|
logger.info("Executing procedure (id=%s)", request.execution_id)
|
|
202
205
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
try:
|
|
207
|
+
# Execute procedure
|
|
208
|
+
proc_result = await execute_procedure(
|
|
209
|
+
source=request.source,
|
|
210
|
+
params=request.params,
|
|
211
|
+
source_file_path=request.source_file_path,
|
|
212
|
+
format=request.format,
|
|
213
|
+
run_id=request.run_id,
|
|
214
|
+
task_name=request.task_name,
|
|
215
|
+
)
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
# Provide structured results for common control-flow exceptions.
|
|
218
|
+
from tactus.core.exceptions import ProcedureWaitingForHuman, TaskSelectionRequired
|
|
219
|
+
|
|
220
|
+
duration = time.time() - start_time
|
|
221
|
+
|
|
222
|
+
if isinstance(exc, TaskSelectionRequired):
|
|
223
|
+
result = ExecutionResult.failure(
|
|
224
|
+
error=str(exc),
|
|
225
|
+
error_type=type(exc).__name__,
|
|
226
|
+
traceback=None,
|
|
227
|
+
duration_seconds=duration,
|
|
228
|
+
exit_code=2,
|
|
229
|
+
metadata={"tasks": list(exc.tasks)},
|
|
230
|
+
)
|
|
231
|
+
write_result_to_stdout(result)
|
|
232
|
+
return 2
|
|
233
|
+
|
|
234
|
+
if isinstance(exc, ProcedureWaitingForHuman):
|
|
235
|
+
result = ExecutionResult(
|
|
236
|
+
status=ExecutionStatus.CANCELLED,
|
|
237
|
+
result=None,
|
|
238
|
+
error=str(exc),
|
|
239
|
+
error_type=type(exc).__name__,
|
|
240
|
+
traceback=None,
|
|
241
|
+
duration_seconds=duration,
|
|
242
|
+
exit_code=0,
|
|
243
|
+
logs=[],
|
|
244
|
+
metadata={
|
|
245
|
+
"waiting_for_human": True,
|
|
246
|
+
"procedure_id": exc.procedure_id,
|
|
247
|
+
"pending_message_id": exc.pending_message_id,
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
write_result_to_stdout(result)
|
|
251
|
+
return 0
|
|
252
|
+
|
|
253
|
+
raise
|
|
211
254
|
|
|
212
255
|
# Create success result
|
|
213
256
|
duration = time.time() - start_time
|
tactus/sandbox/protocol.py
CHANGED
|
@@ -60,6 +60,9 @@ class ExecutionRequest:
|
|
|
60
60
|
# Source format: "lua" for .tac files, "yaml" for legacy YAML format
|
|
61
61
|
format: str = "lua"
|
|
62
62
|
|
|
63
|
+
# Optional task name to execute
|
|
64
|
+
task_name: Optional[str] = None
|
|
65
|
+
|
|
63
66
|
def to_json(self) -> str:
|
|
64
67
|
"""Serialize to JSON string."""
|
|
65
68
|
return json.dumps(asdict(self), indent=None, separators=(",", ":"))
|
|
@@ -148,6 +151,7 @@ class ExecutionResult:
|
|
|
148
151
|
duration_seconds: float = 0.0,
|
|
149
152
|
exit_code: int = 1,
|
|
150
153
|
logs: Optional[list[dict[str, Any]]] = None,
|
|
154
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
151
155
|
) -> "ExecutionResult":
|
|
152
156
|
"""Create a failed result."""
|
|
153
157
|
return cls(
|
|
@@ -158,6 +162,7 @@ class ExecutionResult:
|
|
|
158
162
|
duration_seconds=duration_seconds,
|
|
159
163
|
exit_code=exit_code,
|
|
160
164
|
logs=logs or [],
|
|
165
|
+
metadata=metadata or {},
|
|
161
166
|
)
|
|
162
167
|
|
|
163
168
|
@classmethod
|
tactus/stdlib/README.md
CHANGED
|
@@ -22,6 +22,15 @@ tactus/stdlib/
|
|
|
22
22
|
│ └── fuzzy.py # Fuzzy string matching
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Available Modules
|
|
26
|
+
|
|
27
|
+
- `tactus.classify` - LLM and fuzzy classification
|
|
28
|
+
- `tactus.extract` - Structured extraction utilities
|
|
29
|
+
- `tactus.generate` - LLM-based generation helpers
|
|
30
|
+
- `tactus.io.*` - File I/O helpers (json, csv, tsv, file)
|
|
31
|
+
- `biblicus.text` - Biblicus-backed text utilities
|
|
32
|
+
- `tactus.retrievers.*` - Biblicus-backed retrievers
|
|
33
|
+
|
|
25
34
|
## Testing
|
|
26
35
|
|
|
27
36
|
Run all stdlib specs:
|
|
@@ -54,7 +63,7 @@ The Classify primitive demonstrates the stdlib pattern:
|
|
|
54
63
|
**Current Status**:
|
|
55
64
|
- ✅ Specs pass with Python implementation
|
|
56
65
|
- ✅ Tactus reference implementation exists
|
|
57
|
-
-
|
|
66
|
+
- Next: Module loading system needed to use Tactus impl
|
|
58
67
|
|
|
59
68
|
## Adding New Primitives
|
|
60
69
|
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tactus.biblicus.text - Biblicus text utilities for Tactus.
|
|
3
|
+
|
|
4
|
+
This module exposes Biblicus text helpers to Lua via the stdlib loader.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Dict, List
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _require_biblicus_text() -> Dict[str, Any]:
|
|
14
|
+
try:
|
|
15
|
+
from biblicus.ai.models import LlmClientConfig
|
|
16
|
+
from biblicus.text import (
|
|
17
|
+
apply_text_annotate,
|
|
18
|
+
apply_text_extract,
|
|
19
|
+
apply_text_link,
|
|
20
|
+
apply_text_redact,
|
|
21
|
+
apply_text_slice,
|
|
22
|
+
)
|
|
23
|
+
from biblicus.text.markup import (
|
|
24
|
+
parse_span_markup,
|
|
25
|
+
strip_span_tags,
|
|
26
|
+
summarize_span_context,
|
|
27
|
+
)
|
|
28
|
+
from biblicus.text.models import (
|
|
29
|
+
TextAnnotateRequest,
|
|
30
|
+
TextExtractRequest,
|
|
31
|
+
TextLinkRequest,
|
|
32
|
+
TextRedactRequest,
|
|
33
|
+
TextSliceRequest,
|
|
34
|
+
)
|
|
35
|
+
except ModuleNotFoundError as exc:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
"Biblicus text utilities are unavailable. Install a Biblicus build "
|
|
38
|
+
"that includes biblicus.text to use the text stdlib."
|
|
39
|
+
) from exc
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
"LlmClientConfig": LlmClientConfig,
|
|
43
|
+
"apply_text_annotate": apply_text_annotate,
|
|
44
|
+
"apply_text_extract": apply_text_extract,
|
|
45
|
+
"apply_text_link": apply_text_link,
|
|
46
|
+
"apply_text_redact": apply_text_redact,
|
|
47
|
+
"apply_text_slice": apply_text_slice,
|
|
48
|
+
"parse_span_markup": parse_span_markup,
|
|
49
|
+
"strip_span_tags": strip_span_tags,
|
|
50
|
+
"summarize_span_context": summarize_span_context,
|
|
51
|
+
"TextAnnotateRequest": TextAnnotateRequest,
|
|
52
|
+
"TextExtractRequest": TextExtractRequest,
|
|
53
|
+
"TextLinkRequest": TextLinkRequest,
|
|
54
|
+
"TextRedactRequest": TextRedactRequest,
|
|
55
|
+
"TextSliceRequest": TextSliceRequest,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _normalize_client_config(client: Any) -> Any:
|
|
60
|
+
if not isinstance(client, dict):
|
|
61
|
+
raise ValueError("client must be a table with provider and model")
|
|
62
|
+
|
|
63
|
+
payload = dict(client)
|
|
64
|
+
model = payload.get("model")
|
|
65
|
+
if not model:
|
|
66
|
+
raise ValueError("client.model is required")
|
|
67
|
+
|
|
68
|
+
provider, model = _resolve_provider_and_model(payload.get("provider"), model)
|
|
69
|
+
payload["provider"] = provider
|
|
70
|
+
payload["model"] = model
|
|
71
|
+
|
|
72
|
+
biblicus = _require_biblicus_text()
|
|
73
|
+
return biblicus["LlmClientConfig"](**payload)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _resolve_provider_and_model(provider: str | None, model: Any) -> tuple[str, Any]:
|
|
77
|
+
if provider is None and isinstance(model, str) and "/" in model:
|
|
78
|
+
provider, model = model.split("/", 1)
|
|
79
|
+
elif provider is not None and isinstance(model, str) and model.startswith(f"{provider}/"):
|
|
80
|
+
model = model.split("/", 1)[1]
|
|
81
|
+
|
|
82
|
+
if provider is None:
|
|
83
|
+
raise ValueError("client.provider is required when model lacks a provider prefix")
|
|
84
|
+
|
|
85
|
+
return provider, model
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _prepare_request(request: Dict[str, Any]) -> Dict[str, Any]:
|
|
89
|
+
if not isinstance(request, dict):
|
|
90
|
+
raise ValueError("request must be a table")
|
|
91
|
+
|
|
92
|
+
payload = dict(request)
|
|
93
|
+
mock_mode = os.environ.get("TACTUS_MOCK_MODE")
|
|
94
|
+
if mock_mode == "0":
|
|
95
|
+
payload.pop("mock_marked_up_text", None)
|
|
96
|
+
client = payload.get("client")
|
|
97
|
+
if client is None:
|
|
98
|
+
raise ValueError("client is required")
|
|
99
|
+
payload["client"] = _normalize_client_config(client)
|
|
100
|
+
return payload
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _maybe_mock(tool_name: str, payload: Dict[str, Any]) -> Dict[str, Any] | None:
|
|
104
|
+
mock_manager = _get_mock_manager()
|
|
105
|
+
if mock_manager is None:
|
|
106
|
+
return None
|
|
107
|
+
mock_result = mock_manager.get_mock_response(tool_name, payload)
|
|
108
|
+
if mock_result is None:
|
|
109
|
+
return None
|
|
110
|
+
mock_manager.record_call(tool_name, payload, mock_result)
|
|
111
|
+
return mock_result
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _get_mock_manager() -> Any | None:
|
|
115
|
+
try:
|
|
116
|
+
from tactus.core.mocking import get_current_mock_manager
|
|
117
|
+
except Exception:
|
|
118
|
+
return None
|
|
119
|
+
return get_current_mock_manager()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _run_text_tool(
|
|
123
|
+
request: Dict[str, Any],
|
|
124
|
+
*,
|
|
125
|
+
tool_name: str,
|
|
126
|
+
request_model_key: str,
|
|
127
|
+
apply_key: str,
|
|
128
|
+
) -> Dict[str, Any]:
|
|
129
|
+
payload = _prepare_request(request)
|
|
130
|
+
mock_result = _maybe_mock(tool_name, payload)
|
|
131
|
+
if mock_result is not None:
|
|
132
|
+
return mock_result
|
|
133
|
+
biblicus = _require_biblicus_text()
|
|
134
|
+
result = biblicus[apply_key](biblicus[request_model_key](**payload))
|
|
135
|
+
return result.model_dump()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def extract(request: Dict[str, Any]) -> Dict[str, Any]:
|
|
139
|
+
return _run_text_tool(
|
|
140
|
+
request,
|
|
141
|
+
tool_name="biblicus.text.extract",
|
|
142
|
+
request_model_key="TextExtractRequest",
|
|
143
|
+
apply_key="apply_text_extract",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def slice(request: Dict[str, Any]) -> Dict[str, Any]:
|
|
148
|
+
return _run_text_tool(
|
|
149
|
+
request,
|
|
150
|
+
tool_name="biblicus.text.slice",
|
|
151
|
+
request_model_key="TextSliceRequest",
|
|
152
|
+
apply_key="apply_text_slice",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def annotate(request: Dict[str, Any]) -> Dict[str, Any]:
|
|
157
|
+
return _run_text_tool(
|
|
158
|
+
request,
|
|
159
|
+
tool_name="biblicus.text.annotate",
|
|
160
|
+
request_model_key="TextAnnotateRequest",
|
|
161
|
+
apply_key="apply_text_annotate",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def redact(request: Dict[str, Any]) -> Dict[str, Any]:
|
|
166
|
+
return _run_text_tool(
|
|
167
|
+
request,
|
|
168
|
+
tool_name="biblicus.text.redact",
|
|
169
|
+
request_model_key="TextRedactRequest",
|
|
170
|
+
apply_key="apply_text_redact",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def link(request: Dict[str, Any]) -> Dict[str, Any]:
|
|
175
|
+
return _run_text_tool(
|
|
176
|
+
request,
|
|
177
|
+
tool_name="biblicus.text.link",
|
|
178
|
+
request_model_key="TextLinkRequest",
|
|
179
|
+
apply_key="apply_text_link",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def strip_span_tags(marked_up_text: str) -> str:
|
|
184
|
+
biblicus = _require_biblicus_text()
|
|
185
|
+
return biblicus["strip_span_tags"](marked_up_text)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def parse_span_markup(marked_up_text: str) -> List[Dict[str, Any]]:
|
|
189
|
+
biblicus = _require_biblicus_text()
|
|
190
|
+
spans = biblicus["parse_span_markup"](marked_up_text)
|
|
191
|
+
return [span.model_dump() for span in spans]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def summarize_span_context(marked_up_text: str, span_indices: List[int]) -> List[str]:
|
|
195
|
+
biblicus = _require_biblicus_text()
|
|
196
|
+
return biblicus["summarize_span_context"](marked_up_text, span_indices)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
__tactus_exports__ = [
|
|
200
|
+
"extract",
|
|
201
|
+
"slice",
|
|
202
|
+
"annotate",
|
|
203
|
+
"redact",
|
|
204
|
+
"link",
|
|
205
|
+
"strip_span_tags",
|
|
206
|
+
"parse_span_markup",
|
|
207
|
+
"summarize_span_context",
|
|
208
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
--[[doc
|
|
2
|
+
# biblicus.text
|
|
3
|
+
|
|
4
|
+
Biblicus-powered text utilities for Tactus.
|
|
5
|
+
|
|
6
|
+
This module exposes Biblicus text helpers through the Tactus stdlib. It mirrors
|
|
7
|
+
the Biblicus request/response shapes while keeping configuration in Tactus.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```lua
|
|
12
|
+
local text = require("biblicus.text")
|
|
13
|
+
|
|
14
|
+
local result = text.extract({
|
|
15
|
+
text = "Alice met Bob in Paris.",
|
|
16
|
+
client = {
|
|
17
|
+
provider = "openai",
|
|
18
|
+
model = "gpt-4o-mini"
|
|
19
|
+
},
|
|
20
|
+
prompt_template = "Extract the person names in the text.",
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Testing hook
|
|
25
|
+
|
|
26
|
+
For deterministic tests, use Tactus `Mocks {}` to return stable Biblicus
|
|
27
|
+
results without touching the model.
|
|
28
|
+
]]
|
|
29
|
+
|
|
30
|
+
local text = require("tactus.biblicus.text")
|
|
31
|
+
|
|
32
|
+
return text
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
--[[doc
|
|
2
|
+
# Biblicus Text Utilities
|
|
3
|
+
|
|
4
|
+
This specification covers the Biblicus-backed `biblicus.text` stdlib module.
|
|
5
|
+
It exercises the deterministic mock path plus markup helper functions.
|
|
6
|
+
]]
|
|
7
|
+
|
|
8
|
+
local text = require("biblicus.text")
|
|
9
|
+
|
|
10
|
+
local test_state = {}
|
|
11
|
+
local function build_request(text_value)
|
|
12
|
+
local client = {
|
|
13
|
+
provider = "openai",
|
|
14
|
+
model = "gpt-4o-mini"
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
text = text_value,
|
|
18
|
+
client = client,
|
|
19
|
+
prompt_template = "Return only the updated markup."
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Step("a biblicus request for \"(.+)\"", function(ctx, raw_text)
|
|
24
|
+
test_state.request = build_request(raw_text)
|
|
25
|
+
end)
|
|
26
|
+
|
|
27
|
+
Step("I apply mock extraction markup \"(.+)\"", function(ctx, markup)
|
|
28
|
+
local request = test_state.request
|
|
29
|
+
request.mock_marked_up_text = markup
|
|
30
|
+
local ok, result = pcall(text.extract, request)
|
|
31
|
+
assert(ok, "text.extract failed: " .. tostring(result))
|
|
32
|
+
test_state.result = result
|
|
33
|
+
end)
|
|
34
|
+
|
|
35
|
+
Step("the extracted span should be \"(.+)\"", function(ctx, expected)
|
|
36
|
+
local spans = test_state.result.spans or {}
|
|
37
|
+
assert(#spans == 1, "Expected one span")
|
|
38
|
+
assert(spans[1].text == expected,
|
|
39
|
+
"Expected span text '" .. expected .. "' but got '" .. tostring(spans[1].text) .. "'")
|
|
40
|
+
end)
|
|
41
|
+
|
|
42
|
+
Step("I apply mock annotation markup \"(.+)\"", function(ctx, markup)
|
|
43
|
+
local request = test_state.request
|
|
44
|
+
request.mock_marked_up_text = markup
|
|
45
|
+
local ok, result = pcall(text.annotate, request)
|
|
46
|
+
assert(ok, "text.annotate failed: " .. tostring(result))
|
|
47
|
+
test_state.result = result
|
|
48
|
+
end)
|
|
49
|
+
|
|
50
|
+
Step("the annotation span label should be \"(.+)\"", function(ctx, expected)
|
|
51
|
+
local spans = test_state.result.spans or {}
|
|
52
|
+
assert(#spans == 1, "Expected one span")
|
|
53
|
+
assert(spans[1].attributes.label == expected,
|
|
54
|
+
"Expected label '" .. expected .. "' but got '" .. tostring(spans[1].attributes.label) .. "'")
|
|
55
|
+
end)
|
|
56
|
+
|
|
57
|
+
Step("I apply mock link markup \"(.+)\"", function(ctx, markup)
|
|
58
|
+
local request = test_state.request
|
|
59
|
+
request.mock_marked_up_text = markup
|
|
60
|
+
request.id_prefix = "link_"
|
|
61
|
+
local ok, result = pcall(text.link, request)
|
|
62
|
+
assert(ok, "text.link failed: " .. tostring(result))
|
|
63
|
+
test_state.result = result
|
|
64
|
+
end)
|
|
65
|
+
|
|
66
|
+
Step("the link spans should include ids", function(ctx)
|
|
67
|
+
local spans = test_state.result.spans or {}
|
|
68
|
+
assert(#spans == 2, "Expected two spans")
|
|
69
|
+
assert(spans[1].attributes.id == "link_1", "Expected first span id link_1")
|
|
70
|
+
assert(spans[2].attributes.ref == "link_1", "Expected second span ref link_1")
|
|
71
|
+
end)
|
|
72
|
+
|
|
73
|
+
Step("I apply mock redaction markup \"(.+)\"", function(ctx, markup)
|
|
74
|
+
local request = test_state.request
|
|
75
|
+
request.mock_marked_up_text = markup
|
|
76
|
+
local ok, result = pcall(text.redact, request)
|
|
77
|
+
assert(ok, "text.redact failed: " .. tostring(result))
|
|
78
|
+
test_state.result = result
|
|
79
|
+
end)
|
|
80
|
+
|
|
81
|
+
Step("the redaction should return one span", function(ctx)
|
|
82
|
+
local spans = test_state.result.spans or {}
|
|
83
|
+
assert(#spans == 1, "Expected one redaction span")
|
|
84
|
+
end)
|
|
85
|
+
|
|
86
|
+
Step("I apply mock slice markup \"(.+)\"", function(ctx, markup)
|
|
87
|
+
local request = test_state.request
|
|
88
|
+
request.mock_marked_up_text = markup
|
|
89
|
+
local ok, result = pcall(text.slice, request)
|
|
90
|
+
assert(ok, "text.slice failed: " .. tostring(result))
|
|
91
|
+
test_state.result = result
|
|
92
|
+
end)
|
|
93
|
+
|
|
94
|
+
Step("the slices should be \"(.+)\" and \"(.+)\"", function(ctx, first, second)
|
|
95
|
+
local slices = test_state.result.slices or {}
|
|
96
|
+
assert(#slices == 2, "Expected two slices")
|
|
97
|
+
assert(slices[1].text == first, "Expected first slice '" .. first .. "'")
|
|
98
|
+
assert(slices[2].text == second, "Expected second slice '" .. second .. "'")
|
|
99
|
+
end)
|
|
100
|
+
|
|
101
|
+
Step("I strip span tags from \"(.+)\"", function(ctx, markup)
|
|
102
|
+
test_state.cleaned = text.strip_span_tags(markup)
|
|
103
|
+
end)
|
|
104
|
+
|
|
105
|
+
Step("the stripped text should be \"(.+)\"", function(ctx, expected)
|
|
106
|
+
assert(test_state.cleaned == expected,
|
|
107
|
+
"Expected stripped text '" .. expected .. "' but got '" .. tostring(test_state.cleaned) .. "'")
|
|
108
|
+
end)
|
|
109
|
+
|
|
110
|
+
Step("I parse spans from \"(.+)\"", function(ctx, markup)
|
|
111
|
+
test_state.parsed_spans = text.parse_span_markup(markup)
|
|
112
|
+
end)
|
|
113
|
+
|
|
114
|
+
Step("the parsed span text should be \"(.+)\"", function(ctx, expected)
|
|
115
|
+
local spans = test_state.parsed_spans or {}
|
|
116
|
+
assert(#spans == 1, "Expected one parsed span")
|
|
117
|
+
assert(spans[1].text == expected,
|
|
118
|
+
"Expected span text '" .. expected .. "' but got '" .. tostring(spans[1].text) .. "'")
|
|
119
|
+
end)
|
|
120
|
+
|
|
121
|
+
Step("I summarize span 1 from \"(.+)\"", function(ctx, markup)
|
|
122
|
+
local summaries = text.summarize_span_context(markup, {1})
|
|
123
|
+
test_state.summary = summaries[1]
|
|
124
|
+
end)
|
|
125
|
+
|
|
126
|
+
Step("the summary should be \"(.+)\"", function(ctx, expected)
|
|
127
|
+
assert(test_state.summary == expected,
|
|
128
|
+
"Expected summary '" .. expected .. "' but got '" .. tostring(test_state.summary) .. "'")
|
|
129
|
+
end)
|
|
130
|
+
|
|
131
|
+
Specification([[
|
|
132
|
+
Feature: Biblicus Text Utilities
|
|
133
|
+
As a Tactus developer
|
|
134
|
+
I want to access Biblicus text utilities from the stdlib
|
|
135
|
+
So that I can reuse Biblicus text processing in workflows
|
|
136
|
+
|
|
137
|
+
Scenario: Extract spans from mock markup
|
|
138
|
+
Given a biblicus request for "Alice met Bob."
|
|
139
|
+
When I apply mock extraction markup "Alice met <span>Bob</span>."
|
|
140
|
+
Then the extracted span should be "Bob"
|
|
141
|
+
|
|
142
|
+
Scenario: Annotate spans with attributes
|
|
143
|
+
Given a biblicus request for "Ada wrote code."
|
|
144
|
+
When I apply mock annotation markup "Ada wrote <span label=\"artifact\">code</span>."
|
|
145
|
+
Then the annotation span label should be "artifact"
|
|
146
|
+
|
|
147
|
+
Scenario: Link repeated spans
|
|
148
|
+
Given a biblicus request for "Alice met Bob and Bob waved."
|
|
149
|
+
When I apply mock link markup "Alice met <span id=\"link_1\">Bob</span> and <span ref=\"link_1\">Bob</span> waved."
|
|
150
|
+
Then the link spans should include ids
|
|
151
|
+
|
|
152
|
+
Scenario: Redact spans without types
|
|
153
|
+
Given a biblicus request for "The secret is safe."
|
|
154
|
+
When I apply mock redaction markup "The <span>secret</span> is safe."
|
|
155
|
+
Then the redaction should return one span
|
|
156
|
+
|
|
157
|
+
Scenario: Slice text into segments
|
|
158
|
+
Given a biblicus request for "First sentence. Second sentence."
|
|
159
|
+
When I apply mock slice markup "First sentence.<slice/> Second sentence."
|
|
160
|
+
Then the slices should be "First sentence." and " Second sentence."
|
|
161
|
+
|
|
162
|
+
Scenario: Use markup helpers
|
|
163
|
+
Given a biblicus request for "Ignored."
|
|
164
|
+
When I strip span tags from "Hello <span>world</span>."
|
|
165
|
+
Then the stripped text should be "Hello world."
|
|
166
|
+
When I parse spans from "Hello <span>world</span>."
|
|
167
|
+
Then the parsed span text should be "world"
|
|
168
|
+
When I summarize span 1 from "Hello <span>world</span>."
|
|
169
|
+
Then the summary should be "Span 1: world"
|
|
170
|
+
]])
|
|
171
|
+
|
|
172
|
+
Procedure {
|
|
173
|
+
output = {
|
|
174
|
+
result = field.string{required = true}
|
|
175
|
+
},
|
|
176
|
+
function(input)
|
|
177
|
+
return {result = "Biblicus text stdlib specs executed"}
|
|
178
|
+
end
|
|
179
|
+
}
|