tactus 0.33.0__py3-none-any.whl → 0.34.1__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.
Files changed (100) hide show
  1. tactus/__init__.py +1 -1
  2. tactus/adapters/__init__.py +18 -1
  3. tactus/adapters/broker_log.py +127 -34
  4. tactus/adapters/channels/__init__.py +153 -0
  5. tactus/adapters/channels/base.py +174 -0
  6. tactus/adapters/channels/broker.py +179 -0
  7. tactus/adapters/channels/cli.py +448 -0
  8. tactus/adapters/channels/host.py +225 -0
  9. tactus/adapters/channels/ipc.py +297 -0
  10. tactus/adapters/channels/sse.py +305 -0
  11. tactus/adapters/cli_hitl.py +223 -1
  12. tactus/adapters/control_loop.py +879 -0
  13. tactus/adapters/file_storage.py +35 -2
  14. tactus/adapters/ide_log.py +7 -1
  15. tactus/backends/http_backend.py +0 -1
  16. tactus/broker/client.py +31 -1
  17. tactus/broker/server.py +416 -92
  18. tactus/cli/app.py +270 -7
  19. tactus/cli/control.py +393 -0
  20. tactus/core/config_manager.py +33 -6
  21. tactus/core/dsl_stubs.py +102 -18
  22. tactus/core/execution_context.py +265 -8
  23. tactus/core/lua_sandbox.py +8 -9
  24. tactus/core/registry.py +19 -2
  25. tactus/core/runtime.py +235 -27
  26. tactus/docker/Dockerfile.pypi +49 -0
  27. tactus/docs/__init__.py +33 -0
  28. tactus/docs/extractor.py +326 -0
  29. tactus/docs/html_renderer.py +72 -0
  30. tactus/docs/models.py +121 -0
  31. tactus/docs/templates/base.html +204 -0
  32. tactus/docs/templates/index.html +58 -0
  33. tactus/docs/templates/module.html +96 -0
  34. tactus/dspy/agent.py +382 -22
  35. tactus/dspy/broker_lm.py +57 -6
  36. tactus/dspy/config.py +14 -3
  37. tactus/dspy/history.py +2 -1
  38. tactus/dspy/module.py +136 -11
  39. tactus/dspy/signature.py +0 -1
  40. tactus/ide/server.py +300 -9
  41. tactus/primitives/human.py +619 -47
  42. tactus/primitives/system.py +0 -1
  43. tactus/protocols/__init__.py +25 -0
  44. tactus/protocols/control.py +427 -0
  45. tactus/protocols/notification.py +207 -0
  46. tactus/sandbox/container_runner.py +79 -11
  47. tactus/sandbox/docker_manager.py +23 -0
  48. tactus/sandbox/entrypoint.py +26 -0
  49. tactus/sandbox/protocol.py +3 -0
  50. tactus/stdlib/README.md +77 -0
  51. tactus/stdlib/__init__.py +27 -1
  52. tactus/stdlib/classify/__init__.py +165 -0
  53. tactus/stdlib/classify/classify.spec.tac +195 -0
  54. tactus/stdlib/classify/classify.tac +257 -0
  55. tactus/stdlib/classify/fuzzy.py +282 -0
  56. tactus/stdlib/classify/llm.py +319 -0
  57. tactus/stdlib/classify/primitive.py +287 -0
  58. tactus/stdlib/core/__init__.py +57 -0
  59. tactus/stdlib/core/base.py +320 -0
  60. tactus/stdlib/core/confidence.py +211 -0
  61. tactus/stdlib/core/models.py +161 -0
  62. tactus/stdlib/core/retry.py +171 -0
  63. tactus/stdlib/core/validation.py +274 -0
  64. tactus/stdlib/extract/__init__.py +125 -0
  65. tactus/stdlib/extract/llm.py +330 -0
  66. tactus/stdlib/extract/primitive.py +256 -0
  67. tactus/stdlib/tac/tactus/classify/base.tac +51 -0
  68. tactus/stdlib/tac/tactus/classify/fuzzy.tac +87 -0
  69. tactus/stdlib/tac/tactus/classify/index.md +77 -0
  70. tactus/stdlib/tac/tactus/classify/init.tac +29 -0
  71. tactus/stdlib/tac/tactus/classify/llm.tac +150 -0
  72. tactus/stdlib/tac/tactus/classify.spec.tac +191 -0
  73. tactus/stdlib/tac/tactus/extract/base.tac +138 -0
  74. tactus/stdlib/tac/tactus/extract/index.md +96 -0
  75. tactus/stdlib/tac/tactus/extract/init.tac +27 -0
  76. tactus/stdlib/tac/tactus/extract/llm.tac +201 -0
  77. tactus/stdlib/tac/tactus/extract.spec.tac +153 -0
  78. tactus/stdlib/tac/tactus/generate/base.tac +142 -0
  79. tactus/stdlib/tac/tactus/generate/index.md +195 -0
  80. tactus/stdlib/tac/tactus/generate/init.tac +28 -0
  81. tactus/stdlib/tac/tactus/generate/llm.tac +169 -0
  82. tactus/stdlib/tac/tactus/generate.spec.tac +210 -0
  83. tactus/testing/behave_integration.py +171 -7
  84. tactus/testing/context.py +0 -1
  85. tactus/testing/evaluation_runner.py +0 -1
  86. tactus/testing/gherkin_parser.py +0 -1
  87. tactus/testing/mock_hitl.py +0 -1
  88. tactus/testing/mock_tools.py +0 -1
  89. tactus/testing/models.py +0 -1
  90. tactus/testing/steps/builtin.py +0 -1
  91. tactus/testing/steps/custom.py +81 -22
  92. tactus/testing/steps/registry.py +0 -1
  93. tactus/testing/test_runner.py +7 -1
  94. tactus/validation/semantic_visitor.py +11 -5
  95. tactus/validation/validator.py +0 -1
  96. {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/METADATA +14 -2
  97. {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/RECORD +100 -49
  98. {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/WHEEL +0 -0
  99. {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/entry_points.txt +0 -0
  100. {tactus-0.33.0.dist-info → tactus-0.34.1.dist-info}/licenses/LICENSE +0 -0
@@ -82,6 +82,26 @@ class FileStorage:
82
82
  except (IOError, OSError) as e:
83
83
  raise RuntimeError(f"Failed to write procedure file {file_path}: {e}")
84
84
 
85
+ def _deserialize_result(self, result: Any) -> Any:
86
+ """Deserialize checkpoint result, reconstructing Pydantic models."""
87
+ if result is None:
88
+ return None
89
+ # Check if result is a serialized Pydantic model
90
+ if isinstance(result, dict) and result.get("__pydantic__"):
91
+ from tactus.protocols.models import HITLResponse
92
+
93
+ model_name = result.get("__model__")
94
+ # Remove metadata fields
95
+ data = {k: v for k, v in result.items() if not k.startswith("__")}
96
+ # Reconstruct based on model name
97
+ if model_name == "HITLResponse":
98
+ # Need to parse datetime string back to datetime
99
+ if "responded_at" in data and isinstance(data["responded_at"], str):
100
+ data["responded_at"] = datetime.fromisoformat(data["responded_at"])
101
+ return HITLResponse(**data)
102
+ # Add other model types as needed
103
+ return result
104
+
85
105
  def load_procedure_metadata(self, procedure_id: str) -> ProcedureMetadata:
86
106
  """Load procedure metadata from file."""
87
107
  data = self._read_file(procedure_id)
@@ -102,7 +122,7 @@ class FileStorage:
102
122
  CheckpointEntry(
103
123
  position=entry_data["position"],
104
124
  type=entry_data["type"],
105
- result=entry_data["result"],
125
+ result=self._deserialize_result(entry_data["result"]),
106
126
  timestamp=datetime.fromisoformat(entry_data["timestamp"]),
107
127
  duration_ms=entry_data.get("duration_ms"),
108
128
  input_hash=entry_data.get("input_hash"),
@@ -122,6 +142,19 @@ class FileStorage:
122
142
  waiting_on_message_id=data.get("waiting_on_message_id"),
123
143
  )
124
144
 
145
+ def _serialize_result(self, result: Any) -> Any:
146
+ """Serialize checkpoint result, handling Pydantic models."""
147
+ if result is None:
148
+ return None
149
+ # Check if result is a Pydantic model (has model_dump method)
150
+ if hasattr(result, "model_dump"):
151
+ return {
152
+ "__pydantic__": True,
153
+ "__model__": result.__class__.__name__,
154
+ **result.model_dump(),
155
+ }
156
+ return result
157
+
125
158
  def save_procedure_metadata(self, procedure_id: str, metadata: ProcedureMetadata) -> None:
126
159
  """Save procedure metadata to file."""
127
160
  # Convert to serializable dict
@@ -131,7 +164,7 @@ class FileStorage:
131
164
  {
132
165
  "position": entry.position,
133
166
  "type": entry.type,
134
- "result": entry.result,
167
+ "result": self._serialize_result(entry.result),
135
168
  "timestamp": entry.timestamp.isoformat(),
136
169
  "duration_ms": entry.duration_ms,
137
170
  "input_hash": entry.input_hash,
@@ -21,6 +21,8 @@ class IDELogHandler:
21
21
  for retrieval and streaming to the IDE frontend.
22
22
  """
23
23
 
24
+ supports_streaming = True
25
+
24
26
  def __init__(self):
25
27
  """Initialize IDE log handler."""
26
28
  self.events = queue.Queue()
@@ -34,6 +36,9 @@ class IDELogHandler:
34
36
  Args:
35
37
  event: Structured log event
36
38
  """
39
+ # CRITICAL DEBUG: Log every call to this method
40
+ logger.info(f"[IDE_LOG] log() called with event type: {type(event).__name__}")
41
+
37
42
  # Track cost events for aggregation
38
43
  from tactus.protocols.models import CostEvent, AgentStreamChunkEvent
39
44
 
@@ -47,7 +52,8 @@ class IDELogHandler:
47
52
  )
48
53
 
49
54
  self.events.put(event)
50
- logger.debug(
55
+ # Use INFO level to ensure we see this in logs
56
+ logger.info(
51
57
  f"[IDE_LOG] Event queued: type={type(event).__name__}, queue_size={self.events.qsize()}"
52
58
  )
53
59
 
@@ -7,7 +7,6 @@ from typing import Any
7
7
 
8
8
  import httpx
9
9
 
10
-
11
10
  logger = logging.getLogger(__name__)
12
11
 
13
12
 
tactus/broker/client.py CHANGED
@@ -9,6 +9,7 @@ Uses a broker transport selected at runtime:
9
9
 
10
10
  import asyncio
11
11
  import json
12
+ import logging
12
13
  import os
13
14
  import ssl
14
15
  import sys
@@ -20,6 +21,8 @@ from typing import Any, AsyncIterator, Optional
20
21
  from tactus.broker.protocol import read_message, write_message
21
22
  from tactus.broker.stdio import STDIO_REQUEST_PREFIX, STDIO_TRANSPORT_VALUE
22
23
 
24
+ logger = logging.getLogger(__name__)
25
+
23
26
 
24
27
  def _json_dumps(obj: Any) -> str:
25
28
  return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
@@ -162,7 +165,15 @@ class BrokerClient:
162
165
  ssl_ctx.verify_mode = ssl.CERT_NONE
163
166
 
164
167
  reader, writer = await asyncio.open_connection(host, port, ssl=ssl_ctx)
165
- await write_message(writer, {"id": req_id, "method": method, "params": params})
168
+ logger.info(
169
+ f"[BROKER_CLIENT] Writing message to broker, params keys: {list(params.keys())}"
170
+ )
171
+ try:
172
+ await write_message(writer, {"id": req_id, "method": method, "params": params})
173
+ except TypeError as e:
174
+ logger.error(f"[BROKER_CLIENT] JSON serialization error: {e}")
175
+ logger.error(f"[BROKER_CLIENT] Params: {params}")
176
+ raise
166
177
 
167
178
  try:
168
179
  while True:
@@ -207,6 +218,8 @@ class BrokerClient:
207
218
  temperature: Optional[float] = None,
208
219
  max_tokens: Optional[int] = None,
209
220
  stream: bool,
221
+ tools: Optional[list[dict[str, Any]]] = None,
222
+ tool_choice: Optional[str] = None,
210
223
  ) -> AsyncIterator[dict[str, Any]]:
211
224
  params: dict[str, Any] = {
212
225
  "provider": provider,
@@ -218,6 +231,23 @@ class BrokerClient:
218
231
  params["temperature"] = temperature
219
232
  if max_tokens is not None:
220
233
  params["max_tokens"] = max_tokens
234
+ if tools is not None:
235
+ params["tools"] = tools
236
+ import logging
237
+
238
+ logger = logging.getLogger(__name__)
239
+ logger.info(f"[BROKER_CLIENT] Adding {len(tools)} tools to params")
240
+ else:
241
+ import logging
242
+
243
+ logger = logging.getLogger(__name__)
244
+ logger.warning("[BROKER_CLIENT] No tools to add to params")
245
+ if tool_choice is not None:
246
+ params["tool_choice"] = tool_choice
247
+ import logging
248
+
249
+ logger = logging.getLogger(__name__)
250
+ logger.info(f"[BROKER_CLIENT] Adding tool_choice={tool_choice} to params")
221
251
  return self._request("llm.chat", params)
222
252
 
223
253
  async def call_tool(self, *, name: str, args: dict[str, Any]) -> Any: