tactus 0.34.1__py3-none-any.whl → 0.35.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.
- tactus/__init__.py +1 -1
- tactus/adapters/broker_log.py +17 -14
- tactus/adapters/channels/__init__.py +17 -15
- tactus/adapters/channels/base.py +16 -7
- tactus/adapters/channels/broker.py +43 -13
- tactus/adapters/channels/cli.py +19 -15
- tactus/adapters/channels/host.py +40 -25
- tactus/adapters/channels/ipc.py +82 -31
- tactus/adapters/channels/sse.py +41 -23
- tactus/adapters/cli_hitl.py +19 -19
- tactus/adapters/cli_log.py +4 -4
- tactus/adapters/control_loop.py +138 -99
- tactus/adapters/cost_collector_log.py +9 -9
- tactus/adapters/file_storage.py +56 -52
- tactus/adapters/http_callback_log.py +23 -13
- tactus/adapters/ide_log.py +17 -9
- tactus/adapters/lua_tools.py +4 -5
- tactus/adapters/mcp.py +16 -19
- tactus/adapters/mcp_manager.py +46 -30
- tactus/adapters/memory.py +9 -9
- tactus/adapters/plugins.py +42 -42
- tactus/broker/client.py +75 -78
- tactus/broker/protocol.py +57 -57
- tactus/broker/server.py +252 -197
- tactus/cli/app.py +3 -1
- tactus/cli/control.py +2 -2
- tactus/core/config_manager.py +181 -135
- tactus/core/dependencies/registry.py +66 -48
- tactus/core/dsl_stubs.py +222 -163
- tactus/core/exceptions.py +10 -1
- tactus/core/execution_context.py +152 -112
- tactus/core/lua_sandbox.py +72 -64
- tactus/core/message_history_manager.py +138 -43
- tactus/core/mocking.py +41 -27
- tactus/core/output_validator.py +49 -44
- tactus/core/registry.py +94 -80
- tactus/core/runtime.py +211 -176
- tactus/core/template_resolver.py +16 -16
- tactus/core/yaml_parser.py +55 -45
- tactus/docs/extractor.py +7 -6
- tactus/ide/server.py +119 -78
- tactus/primitives/control.py +10 -6
- tactus/primitives/file.py +48 -46
- tactus/primitives/handles.py +47 -35
- tactus/primitives/host.py +29 -27
- tactus/primitives/human.py +154 -137
- tactus/primitives/json.py +22 -23
- tactus/primitives/log.py +26 -26
- tactus/primitives/message_history.py +285 -31
- tactus/primitives/model.py +15 -9
- tactus/primitives/procedure.py +86 -64
- tactus/primitives/procedure_callable.py +58 -51
- tactus/primitives/retry.py +31 -29
- tactus/primitives/session.py +42 -29
- tactus/primitives/state.py +54 -43
- tactus/primitives/step.py +9 -13
- tactus/primitives/system.py +34 -21
- tactus/primitives/tool.py +44 -31
- tactus/primitives/tool_handle.py +76 -54
- tactus/primitives/toolset.py +25 -22
- tactus/sandbox/config.py +4 -4
- tactus/sandbox/container_runner.py +161 -107
- tactus/sandbox/docker_manager.py +20 -20
- tactus/sandbox/entrypoint.py +16 -14
- tactus/sandbox/protocol.py +15 -15
- tactus/stdlib/classify/llm.py +1 -3
- tactus/stdlib/core/validation.py +0 -3
- tactus/testing/pydantic_eval_runner.py +1 -1
- tactus/utils/asyncio_helpers.py +27 -0
- tactus/utils/cost_calculator.py +7 -7
- tactus/utils/model_pricing.py +11 -12
- tactus/utils/safe_file_library.py +156 -132
- tactus/utils/safe_libraries.py +27 -27
- tactus/validation/error_listener.py +18 -5
- tactus/validation/semantic_visitor.py +392 -333
- tactus/validation/validator.py +89 -49
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/METADATA +15 -3
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/RECORD +81 -80
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/WHEEL +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/entry_points.txt +0 -0
- {tactus-0.34.1.dist-info → tactus-0.35.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,9 +5,9 @@ This module provides the infrastructure for declaring, creating, and managing
|
|
|
5
5
|
external dependencies (HTTP clients, databases, caches) that procedures need.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from enum import Enum
|
|
9
|
-
from typing import Dict, Any
|
|
10
8
|
import logging
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
@@ -29,13 +29,13 @@ class ResourceFactory:
|
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
31
|
@staticmethod
|
|
32
|
-
async def create(resource_type: str,
|
|
32
|
+
async def create(resource_type: str, resource_config: dict[str, Any]) -> Any:
|
|
33
33
|
"""
|
|
34
34
|
Create a real resource from configuration.
|
|
35
35
|
|
|
36
36
|
Args:
|
|
37
37
|
resource_type: Type of resource (http_client, postgres, redis)
|
|
38
|
-
|
|
38
|
+
resource_config: Configuration dictionary from procedure DSL
|
|
39
39
|
|
|
40
40
|
Returns:
|
|
41
41
|
Configured resource instance
|
|
@@ -45,16 +45,15 @@ class ResourceFactory:
|
|
|
45
45
|
ImportError: If required library is not installed
|
|
46
46
|
"""
|
|
47
47
|
if resource_type == ResourceType.HTTP_CLIENT.value:
|
|
48
|
-
return await ResourceFactory._create_http_client(
|
|
49
|
-
|
|
50
|
-
return await ResourceFactory._create_postgres(
|
|
51
|
-
|
|
52
|
-
return await ResourceFactory._create_redis(
|
|
53
|
-
|
|
54
|
-
raise ValueError(f"Unknown resource type: {resource_type}")
|
|
48
|
+
return await ResourceFactory._create_http_client(resource_config)
|
|
49
|
+
if resource_type == ResourceType.POSTGRES.value:
|
|
50
|
+
return await ResourceFactory._create_postgres(resource_config)
|
|
51
|
+
if resource_type == ResourceType.REDIS.value:
|
|
52
|
+
return await ResourceFactory._create_redis(resource_config)
|
|
53
|
+
raise ValueError(f"Unknown resource type: {resource_type}")
|
|
55
54
|
|
|
56
55
|
@staticmethod
|
|
57
|
-
async def _create_http_client(
|
|
56
|
+
async def _create_http_client(resource_config: dict[str, Any]) -> Any:
|
|
58
57
|
"""Create HTTP client (httpx.AsyncClient)."""
|
|
59
58
|
try:
|
|
60
59
|
import httpx
|
|
@@ -63,16 +62,20 @@ class ResourceFactory:
|
|
|
63
62
|
"httpx is required for HTTP client dependencies. Install it with: pip install httpx"
|
|
64
63
|
)
|
|
65
64
|
|
|
66
|
-
base_url =
|
|
67
|
-
headers =
|
|
68
|
-
|
|
65
|
+
base_url = resource_config.get("base_url")
|
|
66
|
+
headers = resource_config.get("headers", {})
|
|
67
|
+
timeout_seconds = resource_config.get("timeout", 30.0)
|
|
69
68
|
|
|
70
|
-
logger.info(
|
|
69
|
+
logger.info("Creating HTTP client for base_url=%s", base_url)
|
|
71
70
|
|
|
72
|
-
return httpx.AsyncClient(
|
|
71
|
+
return httpx.AsyncClient(
|
|
72
|
+
base_url=base_url,
|
|
73
|
+
headers=headers,
|
|
74
|
+
timeout=timeout_seconds,
|
|
75
|
+
)
|
|
73
76
|
|
|
74
77
|
@staticmethod
|
|
75
|
-
async def _create_postgres(
|
|
78
|
+
async def _create_postgres(resource_config: dict[str, Any]) -> Any:
|
|
76
79
|
"""Create PostgreSQL connection pool (asyncpg.Pool)."""
|
|
77
80
|
try:
|
|
78
81
|
import asyncpg
|
|
@@ -82,18 +85,18 @@ class ResourceFactory:
|
|
|
82
85
|
"Install it with: pip install asyncpg"
|
|
83
86
|
)
|
|
84
87
|
|
|
85
|
-
connection_string =
|
|
86
|
-
pool_size =
|
|
87
|
-
max_pool_size =
|
|
88
|
+
connection_string = resource_config["connection_string"]
|
|
89
|
+
pool_size = resource_config.get("pool_size", 10)
|
|
90
|
+
max_pool_size = resource_config.get("max_pool_size", 20)
|
|
88
91
|
|
|
89
|
-
logger.info(
|
|
92
|
+
logger.info("Creating PostgreSQL pool with size=%s", pool_size)
|
|
90
93
|
|
|
91
94
|
return await asyncpg.create_pool(
|
|
92
95
|
connection_string, min_size=pool_size, max_size=max_pool_size
|
|
93
96
|
)
|
|
94
97
|
|
|
95
98
|
@staticmethod
|
|
96
|
-
async def _create_redis(
|
|
99
|
+
async def _create_redis(resource_config: dict[str, Any]) -> Any:
|
|
97
100
|
"""Create Redis client (redis.asyncio.Redis)."""
|
|
98
101
|
try:
|
|
99
102
|
import redis.asyncio as redis
|
|
@@ -102,14 +105,14 @@ class ResourceFactory:
|
|
|
102
105
|
"redis is required for Redis dependencies. Install it with: pip install redis"
|
|
103
106
|
)
|
|
104
107
|
|
|
105
|
-
url =
|
|
108
|
+
url = resource_config["url"]
|
|
106
109
|
|
|
107
|
-
logger.info(
|
|
110
|
+
logger.info("Creating Redis client for url=%s", url)
|
|
108
111
|
|
|
109
112
|
return redis.from_url(url, encoding="utf-8", decode_responses=True)
|
|
110
113
|
|
|
111
114
|
@staticmethod
|
|
112
|
-
async def create_all(dependencies_config:
|
|
115
|
+
async def create_all(dependencies_config: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
|
113
116
|
"""
|
|
114
117
|
Create all dependencies from configuration.
|
|
115
118
|
|
|
@@ -119,15 +122,21 @@ class ResourceFactory:
|
|
|
119
122
|
Returns:
|
|
120
123
|
Dict mapping dependency name to created resource
|
|
121
124
|
"""
|
|
122
|
-
resources = {}
|
|
125
|
+
resources: dict[str, Any] = {}
|
|
123
126
|
|
|
124
|
-
for
|
|
125
|
-
resource_type =
|
|
127
|
+
for dependency_name, dependency_config in dependencies_config.items():
|
|
128
|
+
resource_type = dependency_config.get("type")
|
|
126
129
|
if not resource_type:
|
|
127
|
-
raise ValueError(f"Dependency '{
|
|
130
|
+
raise ValueError(f"Dependency '{dependency_name}' missing 'type' field")
|
|
128
131
|
|
|
129
|
-
logger.info(
|
|
130
|
-
|
|
132
|
+
logger.info(
|
|
133
|
+
"Creating dependency '%s' of type '%s'",
|
|
134
|
+
dependency_name,
|
|
135
|
+
resource_type,
|
|
136
|
+
)
|
|
137
|
+
resources[dependency_name] = await ResourceFactory.create(
|
|
138
|
+
resource_type, dependency_config
|
|
139
|
+
)
|
|
131
140
|
|
|
132
141
|
return resources
|
|
133
142
|
|
|
@@ -141,40 +150,49 @@ class ResourceManager:
|
|
|
141
150
|
"""
|
|
142
151
|
|
|
143
152
|
def __init__(self):
|
|
144
|
-
self.resources:
|
|
153
|
+
self.resources: dict[str, Any] = {}
|
|
145
154
|
|
|
146
155
|
async def add_resource(self, name: str, resource: Any) -> None:
|
|
147
156
|
"""Add a resource to be managed."""
|
|
148
157
|
self.resources[name] = resource
|
|
149
|
-
logger.debug(
|
|
158
|
+
logger.debug("Added resource '%s' to manager", name)
|
|
150
159
|
|
|
151
160
|
async def cleanup(self) -> None:
|
|
152
161
|
"""Clean up all managed resources."""
|
|
153
|
-
logger.info(
|
|
162
|
+
logger.info("Cleaning up %s resources", len(self.resources))
|
|
154
163
|
|
|
155
|
-
for
|
|
164
|
+
for resource_name, resource in self.resources.items():
|
|
156
165
|
try:
|
|
157
|
-
await self._cleanup_resource(
|
|
158
|
-
except Exception as
|
|
159
|
-
logger.error(
|
|
160
|
-
|
|
161
|
-
|
|
166
|
+
await self._cleanup_resource(resource_name, resource)
|
|
167
|
+
except Exception as exception:
|
|
168
|
+
logger.error(
|
|
169
|
+
"Error cleaning up resource '%s': %s",
|
|
170
|
+
resource_name,
|
|
171
|
+
exception,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
async def _cleanup_resource(self, resource_name: str, resource: Any) -> None:
|
|
162
175
|
"""Clean up a single resource based on its type."""
|
|
163
176
|
# HTTP client cleanup
|
|
164
177
|
if hasattr(resource, "aclose"):
|
|
165
|
-
logger.debug(
|
|
178
|
+
logger.debug("Closing HTTP client '%s'", resource_name)
|
|
166
179
|
await resource.aclose()
|
|
180
|
+
return
|
|
167
181
|
|
|
168
182
|
# PostgreSQL pool cleanup
|
|
169
|
-
|
|
170
|
-
logger.debug(
|
|
183
|
+
if hasattr(resource, "close") and hasattr(resource, "wait_closed"):
|
|
184
|
+
logger.debug("Closing PostgreSQL pool '%s'", resource_name)
|
|
171
185
|
await resource.close()
|
|
172
186
|
await resource.wait_closed()
|
|
187
|
+
return
|
|
173
188
|
|
|
174
189
|
# Redis client cleanup
|
|
175
|
-
|
|
176
|
-
logger.debug(
|
|
190
|
+
if hasattr(resource, "close") and not hasattr(resource, "wait_closed"):
|
|
191
|
+
logger.debug("Closing Redis client '%s'", resource_name)
|
|
177
192
|
await resource.close()
|
|
193
|
+
return
|
|
178
194
|
|
|
179
|
-
|
|
180
|
-
|
|
195
|
+
logger.warning(
|
|
196
|
+
"Unknown resource type for '%s', no cleanup performed",
|
|
197
|
+
resource_name,
|
|
198
|
+
)
|