tactus 0.31.2__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 (160) hide show
  1. tactus/__init__.py +49 -0
  2. tactus/adapters/__init__.py +9 -0
  3. tactus/adapters/broker_log.py +76 -0
  4. tactus/adapters/cli_hitl.py +189 -0
  5. tactus/adapters/cli_log.py +223 -0
  6. tactus/adapters/cost_collector_log.py +56 -0
  7. tactus/adapters/file_storage.py +367 -0
  8. tactus/adapters/http_callback_log.py +109 -0
  9. tactus/adapters/ide_log.py +71 -0
  10. tactus/adapters/lua_tools.py +336 -0
  11. tactus/adapters/mcp.py +289 -0
  12. tactus/adapters/mcp_manager.py +196 -0
  13. tactus/adapters/memory.py +53 -0
  14. tactus/adapters/plugins.py +419 -0
  15. tactus/backends/http_backend.py +58 -0
  16. tactus/backends/model_backend.py +35 -0
  17. tactus/backends/pytorch_backend.py +110 -0
  18. tactus/broker/__init__.py +12 -0
  19. tactus/broker/client.py +247 -0
  20. tactus/broker/protocol.py +183 -0
  21. tactus/broker/server.py +1123 -0
  22. tactus/broker/stdio.py +12 -0
  23. tactus/cli/__init__.py +7 -0
  24. tactus/cli/app.py +2245 -0
  25. tactus/cli/commands/__init__.py +0 -0
  26. tactus/core/__init__.py +32 -0
  27. tactus/core/config_manager.py +790 -0
  28. tactus/core/dependencies/__init__.py +14 -0
  29. tactus/core/dependencies/registry.py +180 -0
  30. tactus/core/dsl_stubs.py +2117 -0
  31. tactus/core/exceptions.py +66 -0
  32. tactus/core/execution_context.py +480 -0
  33. tactus/core/lua_sandbox.py +508 -0
  34. tactus/core/message_history_manager.py +236 -0
  35. tactus/core/mocking.py +286 -0
  36. tactus/core/output_validator.py +291 -0
  37. tactus/core/registry.py +499 -0
  38. tactus/core/runtime.py +2907 -0
  39. tactus/core/template_resolver.py +142 -0
  40. tactus/core/yaml_parser.py +301 -0
  41. tactus/docker/Dockerfile +61 -0
  42. tactus/docker/entrypoint.sh +69 -0
  43. tactus/dspy/__init__.py +39 -0
  44. tactus/dspy/agent.py +1144 -0
  45. tactus/dspy/broker_lm.py +181 -0
  46. tactus/dspy/config.py +212 -0
  47. tactus/dspy/history.py +196 -0
  48. tactus/dspy/module.py +405 -0
  49. tactus/dspy/prediction.py +318 -0
  50. tactus/dspy/signature.py +185 -0
  51. tactus/formatting/__init__.py +7 -0
  52. tactus/formatting/formatter.py +437 -0
  53. tactus/ide/__init__.py +9 -0
  54. tactus/ide/coding_assistant.py +343 -0
  55. tactus/ide/server.py +2223 -0
  56. tactus/primitives/__init__.py +49 -0
  57. tactus/primitives/control.py +168 -0
  58. tactus/primitives/file.py +229 -0
  59. tactus/primitives/handles.py +378 -0
  60. tactus/primitives/host.py +94 -0
  61. tactus/primitives/human.py +342 -0
  62. tactus/primitives/json.py +189 -0
  63. tactus/primitives/log.py +187 -0
  64. tactus/primitives/message_history.py +157 -0
  65. tactus/primitives/model.py +163 -0
  66. tactus/primitives/procedure.py +564 -0
  67. tactus/primitives/procedure_callable.py +318 -0
  68. tactus/primitives/retry.py +155 -0
  69. tactus/primitives/session.py +152 -0
  70. tactus/primitives/state.py +182 -0
  71. tactus/primitives/step.py +209 -0
  72. tactus/primitives/system.py +93 -0
  73. tactus/primitives/tool.py +375 -0
  74. tactus/primitives/tool_handle.py +279 -0
  75. tactus/primitives/toolset.py +229 -0
  76. tactus/protocols/__init__.py +38 -0
  77. tactus/protocols/chat_recorder.py +81 -0
  78. tactus/protocols/config.py +97 -0
  79. tactus/protocols/cost.py +31 -0
  80. tactus/protocols/hitl.py +71 -0
  81. tactus/protocols/log_handler.py +27 -0
  82. tactus/protocols/models.py +355 -0
  83. tactus/protocols/result.py +33 -0
  84. tactus/protocols/storage.py +90 -0
  85. tactus/providers/__init__.py +13 -0
  86. tactus/providers/base.py +92 -0
  87. tactus/providers/bedrock.py +117 -0
  88. tactus/providers/google.py +105 -0
  89. tactus/providers/openai.py +98 -0
  90. tactus/sandbox/__init__.py +63 -0
  91. tactus/sandbox/config.py +171 -0
  92. tactus/sandbox/container_runner.py +1099 -0
  93. tactus/sandbox/docker_manager.py +433 -0
  94. tactus/sandbox/entrypoint.py +227 -0
  95. tactus/sandbox/protocol.py +213 -0
  96. tactus/stdlib/__init__.py +10 -0
  97. tactus/stdlib/io/__init__.py +13 -0
  98. tactus/stdlib/io/csv.py +88 -0
  99. tactus/stdlib/io/excel.py +136 -0
  100. tactus/stdlib/io/file.py +90 -0
  101. tactus/stdlib/io/fs.py +154 -0
  102. tactus/stdlib/io/hdf5.py +121 -0
  103. tactus/stdlib/io/json.py +109 -0
  104. tactus/stdlib/io/parquet.py +83 -0
  105. tactus/stdlib/io/tsv.py +88 -0
  106. tactus/stdlib/loader.py +274 -0
  107. tactus/stdlib/tac/tactus/tools/done.tac +33 -0
  108. tactus/stdlib/tac/tactus/tools/log.tac +50 -0
  109. tactus/testing/README.md +273 -0
  110. tactus/testing/__init__.py +61 -0
  111. tactus/testing/behave_integration.py +380 -0
  112. tactus/testing/context.py +486 -0
  113. tactus/testing/eval_models.py +114 -0
  114. tactus/testing/evaluation_runner.py +222 -0
  115. tactus/testing/evaluators.py +634 -0
  116. tactus/testing/events.py +94 -0
  117. tactus/testing/gherkin_parser.py +134 -0
  118. tactus/testing/mock_agent.py +315 -0
  119. tactus/testing/mock_dependencies.py +234 -0
  120. tactus/testing/mock_hitl.py +171 -0
  121. tactus/testing/mock_registry.py +168 -0
  122. tactus/testing/mock_tools.py +133 -0
  123. tactus/testing/models.py +115 -0
  124. tactus/testing/pydantic_eval_runner.py +508 -0
  125. tactus/testing/steps/__init__.py +13 -0
  126. tactus/testing/steps/builtin.py +902 -0
  127. tactus/testing/steps/custom.py +69 -0
  128. tactus/testing/steps/registry.py +68 -0
  129. tactus/testing/test_runner.py +489 -0
  130. tactus/tracing/__init__.py +5 -0
  131. tactus/tracing/trace_manager.py +417 -0
  132. tactus/utils/__init__.py +1 -0
  133. tactus/utils/cost_calculator.py +72 -0
  134. tactus/utils/model_pricing.py +132 -0
  135. tactus/utils/safe_file_library.py +502 -0
  136. tactus/utils/safe_libraries.py +234 -0
  137. tactus/validation/LuaLexerBase.py +66 -0
  138. tactus/validation/LuaParserBase.py +23 -0
  139. tactus/validation/README.md +224 -0
  140. tactus/validation/__init__.py +7 -0
  141. tactus/validation/error_listener.py +21 -0
  142. tactus/validation/generated/LuaLexer.interp +231 -0
  143. tactus/validation/generated/LuaLexer.py +5548 -0
  144. tactus/validation/generated/LuaLexer.tokens +124 -0
  145. tactus/validation/generated/LuaLexerBase.py +66 -0
  146. tactus/validation/generated/LuaParser.interp +173 -0
  147. tactus/validation/generated/LuaParser.py +6439 -0
  148. tactus/validation/generated/LuaParser.tokens +124 -0
  149. tactus/validation/generated/LuaParserBase.py +23 -0
  150. tactus/validation/generated/LuaParserVisitor.py +118 -0
  151. tactus/validation/generated/__init__.py +7 -0
  152. tactus/validation/grammar/LuaLexer.g4 +123 -0
  153. tactus/validation/grammar/LuaParser.g4 +178 -0
  154. tactus/validation/semantic_visitor.py +817 -0
  155. tactus/validation/validator.py +157 -0
  156. tactus-0.31.2.dist-info/METADATA +1809 -0
  157. tactus-0.31.2.dist-info/RECORD +160 -0
  158. tactus-0.31.2.dist-info/WHEEL +4 -0
  159. tactus-0.31.2.dist-info/entry_points.txt +2 -0
  160. tactus-0.31.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,343 @@
1
+ """
2
+ Coding Assistant Agent for Tactus IDE.
3
+
4
+ Provides an AI-powered coding assistant that uses Tactus primitives
5
+ to interact with the workspace, files, and user.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Dict, Any, Optional, List
12
+ import dspy
13
+ from tactus.primitives.file import FilePrimitive
14
+ from tactus.primitives.human import HumanPrimitive
15
+ from tactus.core.execution_context import ExecutionContext
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class CodingAssistantAgent:
21
+ """
22
+ AI Coding Assistant powered by Tactus primitives.
23
+
24
+ Uses DSPy for agent inference and Tactus primitives for:
25
+ - File operations (FilePrimitive with workspace sandboxing)
26
+ - Human interaction (HumanPrimitive for HITL)
27
+ - Tool tracking (ToolPrimitive)
28
+ """
29
+
30
+ def __init__(self, workspace_root: str, config: Optional[Dict[str, Any]] = None):
31
+ """
32
+ Initialize the coding assistant.
33
+
34
+ Args:
35
+ workspace_root: Root directory for file operations (sandboxed)
36
+ config: Optional configuration dict with provider/model settings
37
+ """
38
+ self.workspace_root = Path(workspace_root).resolve()
39
+ self.config = config or {}
40
+
41
+ # Initialize Tactus primitives
42
+ self.file_primitive = FilePrimitive(base_path=str(self.workspace_root))
43
+
44
+ # Create a minimal execution context for Human primitive
45
+ # (We don't need full runtime features, just HITL support)
46
+ self.execution_context = self._create_execution_context()
47
+ self.human_primitive = HumanPrimitive(self.execution_context)
48
+
49
+ # Chat history
50
+ self.messages: List[Dict[str, Any]] = []
51
+
52
+ # Initialize DSPy with configured model
53
+ self._setup_dspy()
54
+
55
+ logger.info(f"CodingAssistantAgent initialized with workspace: {self.workspace_root}")
56
+
57
+ def _create_execution_context(self) -> ExecutionContext:
58
+ """Create a minimal execution context for primitives."""
59
+ # Import here to avoid circular dependencies
60
+ from tactus.protocols.hitl import HITLResponse
61
+
62
+ class MinimalExecutionContext:
63
+ """Minimal context that supports wait_for_human."""
64
+
65
+ def __init__(self):
66
+ self._inside_checkpoint = False
67
+ self.hitl_handler = None
68
+
69
+ def wait_for_human(
70
+ self,
71
+ request_type: str,
72
+ message: str,
73
+ timeout_seconds: Optional[int] = None,
74
+ default_value: Any = None,
75
+ options: Optional[List[Dict[str, Any]]] = None,
76
+ metadata: Optional[Dict[str, Any]] = None,
77
+ ) -> HITLResponse:
78
+ """
79
+ Handle human interaction requests.
80
+
81
+ For the IDE, we'll emit events that the frontend can respond to.
82
+ This is a simplified version - full HITL will be implemented
83
+ via the streaming API.
84
+ """
85
+ logger.info(f"HITL request: {request_type} - {message}")
86
+
87
+ # For now, return default values
88
+ # The streaming API will handle actual HITL interaction
89
+ return HITLResponse(
90
+ request_type=request_type, value=default_value, metadata=metadata or {}
91
+ )
92
+
93
+ return MinimalExecutionContext()
94
+
95
+ def _setup_dspy(self):
96
+ """Configure DSPy with the appropriate model."""
97
+ # Get configuration
98
+ assistant_config = self.config.get("coding_assistant", {})
99
+ provider = assistant_config.get(
100
+ "provider", os.environ.get("TACTUS_DEFAULT_PROVIDER", "openai")
101
+ )
102
+ model = assistant_config.get("model", os.environ.get("TACTUS_DEFAULT_MODEL", "gpt-4"))
103
+
104
+ logger.info(f"Setting up DSPy with provider={provider}, model={model}")
105
+
106
+ # Configure DSPy LM based on provider
107
+ if provider == "openai":
108
+ api_key = self.config.get("openai_api_key") or os.environ.get("OPENAI_API_KEY")
109
+ if not api_key:
110
+ raise ValueError("OpenAI API key not found in config or environment")
111
+
112
+ lm = dspy.OpenAI(model=model, api_key=api_key, max_tokens=4000)
113
+ elif provider == "anthropic":
114
+ api_key = self.config.get("anthropic_api_key") or os.environ.get("ANTHROPIC_API_KEY")
115
+ if not api_key:
116
+ raise ValueError("Anthropic API key not found in config or environment")
117
+
118
+ lm = dspy.Claude(model=model, api_key=api_key, max_tokens=4000)
119
+ else:
120
+ raise ValueError(f"Unsupported provider: {provider}")
121
+
122
+ dspy.settings.configure(lm=lm)
123
+
124
+ # Create the agent signature
125
+ self.agent = self._create_agent()
126
+
127
+ def _create_agent(self):
128
+ """Create the DSPy agent with appropriate signature."""
129
+
130
+ class CodingAssistantSignature(dspy.Signature):
131
+ """AI Coding Assistant for Tactus IDE.
132
+
133
+ You are a helpful coding assistant integrated into the Tactus IDE.
134
+ You can help users with:
135
+ - Reading and analyzing code files
136
+ - Writing and editing files
137
+ - Explaining code concepts
138
+ - Debugging issues
139
+ - Refactoring code
140
+
141
+ You have access to tools for file operations within the workspace.
142
+ Always be helpful, concise, and accurate.
143
+ """
144
+
145
+ chat_history = dspy.InputField(desc="Previous conversation messages")
146
+ user_message = dspy.InputField(desc="User's current message")
147
+ workspace_root = dspy.InputField(desc="Root directory of the workspace")
148
+
149
+ response = dspy.OutputField(desc="Your response to the user")
150
+ tool_calls = dspy.OutputField(desc="List of tool calls to make (if any)")
151
+
152
+ return dspy.ChainOfThought(CodingAssistantSignature)
153
+
154
+ def process_message(self, user_message: str) -> Dict[str, Any]:
155
+ """
156
+ Process a user message and generate a response.
157
+
158
+ Args:
159
+ user_message: The user's message
160
+
161
+ Returns:
162
+ Dict with 'response' and optional 'tool_calls'
163
+ """
164
+ logger.info(f"Processing message: {user_message[:100]}...")
165
+
166
+ # Add user message to history
167
+ self.messages.append({"role": "user", "content": user_message})
168
+
169
+ # Format chat history for DSPy
170
+ chat_history_str = self._format_chat_history()
171
+
172
+ # Get response from agent
173
+ try:
174
+ result = self.agent(
175
+ chat_history=chat_history_str,
176
+ user_message=user_message,
177
+ workspace_root=str(self.workspace_root),
178
+ )
179
+
180
+ response = result.response
181
+ tool_calls = result.tool_calls if hasattr(result, "tool_calls") else []
182
+
183
+ # Add assistant response to history
184
+ self.messages.append({"role": "assistant", "content": response})
185
+
186
+ logger.info(f"Generated response: {response[:100]}...")
187
+
188
+ return {"response": response, "tool_calls": tool_calls}
189
+
190
+ except Exception as e:
191
+ logger.error(f"Error processing message: {e}", exc_info=True)
192
+ error_response = f"I encountered an error: {str(e)}"
193
+
194
+ self.messages.append({"role": "assistant", "content": error_response})
195
+
196
+ return {"response": error_response, "tool_calls": []}
197
+
198
+ def _format_chat_history(self) -> str:
199
+ """Format chat history as a string for the agent."""
200
+ if not self.messages:
201
+ return "No previous messages."
202
+
203
+ formatted = []
204
+ for msg in self.messages[-10:]: # Last 10 messages
205
+ role = msg["role"].capitalize()
206
+ content = msg["content"]
207
+ formatted.append(f"{role}: {content}")
208
+
209
+ return "\n".join(formatted)
210
+
211
+ # Tool methods that can be called by the agent
212
+
213
+ def read_file(self, path: str) -> str:
214
+ """
215
+ Read a file from the workspace.
216
+
217
+ Args:
218
+ path: Relative path to file within workspace
219
+
220
+ Returns:
221
+ File contents as string
222
+ """
223
+ try:
224
+ logger.info(f"Tool call: read_file({path})")
225
+ content = self.file_primitive.read(path)
226
+ return content
227
+ except Exception as e:
228
+ logger.error(f"Error reading file {path}: {e}")
229
+ return f"Error reading file: {str(e)}"
230
+
231
+ def write_file(self, path: str, content: str) -> str:
232
+ """
233
+ Write content to a file in the workspace.
234
+
235
+ Args:
236
+ path: Relative path to file within workspace
237
+ content: Content to write
238
+
239
+ Returns:
240
+ Success message or error
241
+ """
242
+ try:
243
+ logger.info(f"Tool call: write_file({path}, {len(content)} bytes)")
244
+ self.file_primitive.write(path, content)
245
+ return f"Successfully wrote {len(content)} bytes to {path}"
246
+ except Exception as e:
247
+ logger.error(f"Error writing file {path}: {e}")
248
+ return f"Error writing file: {str(e)}"
249
+
250
+ def list_directory(self, path: str = ".") -> str:
251
+ """
252
+ List files in a directory within the workspace.
253
+
254
+ Args:
255
+ path: Relative path to directory within workspace
256
+
257
+ Returns:
258
+ Formatted list of files and directories
259
+ """
260
+ try:
261
+ logger.info(f"Tool call: list_directory({path})")
262
+
263
+ # Resolve path safely
264
+ target_path = self.file_primitive._resolve_path(path)
265
+
266
+ if not target_path.exists():
267
+ return f"Directory not found: {path}"
268
+
269
+ if not target_path.is_dir():
270
+ return f"Not a directory: {path}"
271
+
272
+ # List contents
273
+ entries = []
274
+ for item in sorted(target_path.iterdir(), key=lambda x: (not x.is_dir(), x.name)):
275
+ if item.is_dir():
276
+ entries.append(f"📁 {item.name}/")
277
+ else:
278
+ size = item.stat().st_size
279
+ entries.append(f"📄 {item.name} ({size} bytes)")
280
+
281
+ if not entries:
282
+ return f"Directory is empty: {path}"
283
+
284
+ return "\n".join(entries)
285
+
286
+ except Exception as e:
287
+ logger.error(f"Error listing directory {path}: {e}")
288
+ return f"Error listing directory: {str(e)}"
289
+
290
+ def file_exists(self, path: str) -> bool:
291
+ """
292
+ Check if a file exists in the workspace.
293
+
294
+ Args:
295
+ path: Relative path to file within workspace
296
+
297
+ Returns:
298
+ True if file exists, False otherwise
299
+ """
300
+ try:
301
+ logger.info(f"Tool call: file_exists({path})")
302
+ return self.file_primitive.exists(path)
303
+ except Exception as e:
304
+ logger.error(f"Error checking file existence {path}: {e}")
305
+ return False
306
+
307
+ def reset_conversation(self):
308
+ """Clear conversation history."""
309
+ logger.info("Resetting conversation history")
310
+ self.messages.clear()
311
+
312
+ def get_available_tools(self) -> List[Dict[str, Any]]:
313
+ """
314
+ Get list of available tools for the agent.
315
+
316
+ Returns:
317
+ List of tool definitions
318
+ """
319
+ return [
320
+ {
321
+ "name": "read_file",
322
+ "description": "Read contents of a file in the workspace",
323
+ "parameters": {"path": "Relative path to the file"},
324
+ },
325
+ {
326
+ "name": "write_file",
327
+ "description": "Write content to a file in the workspace",
328
+ "parameters": {
329
+ "path": "Relative path to the file",
330
+ "content": "Content to write to the file",
331
+ },
332
+ },
333
+ {
334
+ "name": "list_directory",
335
+ "description": "List files and directories in the workspace",
336
+ "parameters": {"path": "Relative path to directory (default: current directory)"},
337
+ },
338
+ {
339
+ "name": "file_exists",
340
+ "description": "Check if a file exists in the workspace",
341
+ "parameters": {"path": "Relative path to the file"},
342
+ },
343
+ ]