rusticai-forge 0.0.5__tar.gz
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.
- rusticai_forge-0.0.5/PKG-INFO +16 -0
- rusticai_forge-0.0.5/README.md +3 -0
- rusticai_forge-0.0.5/pyproject.toml +35 -0
- rusticai_forge-0.0.5/setup.cfg +4 -0
- rusticai_forge-0.0.5/src/rustic_ai/forge/agent_runner.py +253 -0
- rusticai_forge-0.0.5/src/rustic_ai/forge/agent_wrapper.py +81 -0
- rusticai_forge-0.0.5/src/rustic_ai/forge/agents/system/guild_manager_agent.py +690 -0
- rusticai_forge-0.0.5/src/rustic_ai/forge/agents/system/guild_manager_agent_props.py +12 -0
- rusticai_forge-0.0.5/src/rustic_ai/forge/execution_engine.py +159 -0
- rusticai_forge-0.0.5/src/rustic_ai/forge/metastore/manager_client.py +136 -0
- rusticai_forge-0.0.5/src/rustic_ai/forge/testutils/file_manager_agent.py +85 -0
- rusticai_forge-0.0.5/src/rustic_ai/forge/testutils/mock_llm_agent.py +47 -0
- rusticai_forge-0.0.5/src/rusticai_forge.egg-info/PKG-INFO +16 -0
- rusticai_forge-0.0.5/src/rusticai_forge.egg-info/SOURCES.txt +18 -0
- rusticai_forge-0.0.5/src/rusticai_forge.egg-info/dependency_links.txt +1 -0
- rusticai_forge-0.0.5/src/rusticai_forge.egg-info/requires.txt +6 -0
- rusticai_forge-0.0.5/src/rusticai_forge.egg-info/top_level.txt +1 -0
- rusticai_forge-0.0.5/tests/test_execution_engine.py +184 -0
- rusticai_forge-0.0.5/tests/test_forge_api_contract.py +233 -0
- rusticai_forge-0.0.5/tests/test_forge_integration.py +207 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rusticai-forge
|
|
3
|
+
Version: 0.0.5
|
|
4
|
+
Summary: Python agent wrapper and execution engine for Forge
|
|
5
|
+
Requires-Python: <3.14,>=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pydantic<3.0.0,>=2.12.5
|
|
8
|
+
Requires-Dist: redis<7.0.0,>=6.4.0
|
|
9
|
+
Requires-Dist: rusticai-core<2.0.0,>=1.1.0
|
|
10
|
+
Requires-Dist: rusticai-redis<2.0.0,>=1.1.0
|
|
11
|
+
Requires-Dist: psycopg[binary]<4.0.0,>=3.3.3
|
|
12
|
+
Requires-Dist: httpx<1.0.0,>=0.28.1
|
|
13
|
+
|
|
14
|
+
# rusticai-forge
|
|
15
|
+
|
|
16
|
+
Python execution bridge and system agents for Forge.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rusticai-forge"
|
|
3
|
+
version = "0.0.5"
|
|
4
|
+
description = "Python agent wrapper and execution engine for Forge"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13,<3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"pydantic>=2.12.5,<3.0.0",
|
|
9
|
+
"redis>=6.4.0,<7.0.0",
|
|
10
|
+
# DO NOT CHANGE THESE TO LOCAL FILE PATHS. Always use package references.
|
|
11
|
+
"rusticai-core>=1.1.0,<2.0.0",
|
|
12
|
+
"rusticai-redis>=1.1.0,<2.0.0",
|
|
13
|
+
"psycopg[binary]>=3.3.3,<4.0.0",
|
|
14
|
+
"httpx>=0.28.1,<1.0.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=9.0.2,<10.0.0",
|
|
20
|
+
"requests>=2.32.5,<3.0.0",
|
|
21
|
+
"ruff>=0.13.1,<0.14.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["setuptools>=80"]
|
|
26
|
+
build-backend = "setuptools.build_meta"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
package-dir = {"" = "src"}
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from pydantic import ConfigDict, ValidationError
|
|
7
|
+
from rustic_ai.core.guild.agent import AgentSpec
|
|
8
|
+
from rustic_ai.core.guild.dsl import BaseAgentProps, GuildSpec
|
|
9
|
+
from rustic_ai.core.guild.metaprog.constants import MetaclassConstants
|
|
10
|
+
import rustic_ai.core.guild.dsl as guild_dsl
|
|
11
|
+
from rustic_ai.core.guild import Agent
|
|
12
|
+
from rustic_ai.core.guild.metastore import models as metastore_models
|
|
13
|
+
from rustic_ai.core.messaging.core.message import RoutingSlip
|
|
14
|
+
from rustic_ai.core.messaging import MessagingConfig
|
|
15
|
+
|
|
16
|
+
from rustic_ai.forge.agent_wrapper import ForgeAgentWrapper
|
|
17
|
+
|
|
18
|
+
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
level=getattr(logging, log_level),
|
|
22
|
+
format="[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s",
|
|
23
|
+
handlers=[logging.StreamHandler(sys.stdout)],
|
|
24
|
+
force=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("forge.runner")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _UnresolvedAgent(Agent):
|
|
31
|
+
"""
|
|
32
|
+
Placeholder used only for validation fallback when an agent class cannot be imported yet.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
__annotations__ = {MetaclassConstants.AGENT_PROPS_TYPE: BaseAgentProps}
|
|
36
|
+
|
|
37
|
+
async def run(self):
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _UnresolvedAgentProps(BaseAgentProps):
|
|
42
|
+
model_config = ConfigDict(extra="allow")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_UnresolvedAgent.__annotations__ = {
|
|
46
|
+
MetaclassConstants.AGENT_PROPS_TYPE: _UnresolvedAgentProps
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_ORIGINAL_GET_CLASS_FROM_NAME = guild_dsl.get_class_from_name
|
|
51
|
+
_LENIENT_CLASS_RESOLUTION_ENABLED = False
|
|
52
|
+
_LENIENT_METASTORE_ENABLED = False
|
|
53
|
+
|
|
54
|
+
_ORIGINAL_TO_AGENT_SPEC = metastore_models.AgentModel.to_agent_spec
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _enable_lenient_class_resolution() -> None:
|
|
58
|
+
global _LENIENT_CLASS_RESOLUTION_ENABLED
|
|
59
|
+
if _LENIENT_CLASS_RESOLUTION_ENABLED:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
def _lenient_get_class_from_name(class_name: str):
|
|
63
|
+
try:
|
|
64
|
+
return _ORIGINAL_GET_CLASS_FROM_NAME(class_name)
|
|
65
|
+
except Exception:
|
|
66
|
+
logger.warning("Using unresolved placeholder for class during validation: %s", class_name)
|
|
67
|
+
return _UnresolvedAgent
|
|
68
|
+
|
|
69
|
+
guild_dsl.get_class_from_name = _lenient_get_class_from_name
|
|
70
|
+
_LENIENT_CLASS_RESOLUTION_ENABLED = True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _enable_lenient_metastore_conversion() -> None:
|
|
74
|
+
global _LENIENT_METASTORE_ENABLED
|
|
75
|
+
if _LENIENT_METASTORE_ENABLED:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
def _to_agent_spec_lenient(self):
|
|
79
|
+
try:
|
|
80
|
+
return _ORIGINAL_TO_AGENT_SPEC(self)
|
|
81
|
+
except ValidationError as e:
|
|
82
|
+
logger.warning(
|
|
83
|
+
"AgentModel.to_agent_spec validation failed for %s; using lenient construct fallback: %s",
|
|
84
|
+
self.class_name,
|
|
85
|
+
e,
|
|
86
|
+
)
|
|
87
|
+
return AgentSpec.model_construct(
|
|
88
|
+
id=self.id,
|
|
89
|
+
name=self.name,
|
|
90
|
+
description=self.description,
|
|
91
|
+
class_name=self.class_name,
|
|
92
|
+
properties=self.properties or {},
|
|
93
|
+
additional_topics=self.additional_topics or [],
|
|
94
|
+
listen_to_default_topic=self.listen_to_default_topic,
|
|
95
|
+
act_only_when_tagged=self.act_only_when_tagged,
|
|
96
|
+
dependency_map=self.dependency_map or {},
|
|
97
|
+
additional_dependencies=self.additional_dependencies or [],
|
|
98
|
+
predicates=self.predicates or {},
|
|
99
|
+
resources={},
|
|
100
|
+
qos={},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
metastore_models.AgentModel.to_agent_spec = _to_agent_spec_lenient
|
|
104
|
+
_LENIENT_METASTORE_ENABLED = True
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _load_guild_spec(guild_spec_json: str) -> GuildSpec:
|
|
108
|
+
"""
|
|
109
|
+
Load GuildSpec with a tolerant fallback.
|
|
110
|
+
|
|
111
|
+
Strict validation can fail when guild JSON contains agent classes that are not yet
|
|
112
|
+
importable in this process (for example dynamically downloaded uvx deps). In that
|
|
113
|
+
case we preserve enough structure to start the current agent process.
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
return GuildSpec.model_validate_json(guild_spec_json)
|
|
117
|
+
except ValidationError as e:
|
|
118
|
+
logger.warning(
|
|
119
|
+
"GuildSpec strict validation failed; falling back to lenient parsing for startup: %s",
|
|
120
|
+
e,
|
|
121
|
+
)
|
|
122
|
+
_enable_lenient_class_resolution()
|
|
123
|
+
_enable_lenient_metastore_conversion()
|
|
124
|
+
try:
|
|
125
|
+
return GuildSpec.model_validate_json(guild_spec_json)
|
|
126
|
+
except Exception:
|
|
127
|
+
logger.warning("Lenient GuildSpec re-validation failed; using structural fallback.")
|
|
128
|
+
|
|
129
|
+
raw = json.loads(guild_spec_json)
|
|
130
|
+
if not isinstance(raw, dict):
|
|
131
|
+
raise ValueError("FORGE_GUILD_JSON must decode to a JSON object.")
|
|
132
|
+
|
|
133
|
+
raw_agents = raw.get("agents") or []
|
|
134
|
+
fallback_agents: list[AgentSpec] = []
|
|
135
|
+
for i, raw_agent in enumerate(raw_agents):
|
|
136
|
+
if not isinstance(raw_agent, dict):
|
|
137
|
+
logger.warning("Skipping malformed guild agent entry at index %s", i)
|
|
138
|
+
continue
|
|
139
|
+
fallback_agents.append(
|
|
140
|
+
AgentSpec.model_construct(
|
|
141
|
+
id=str(raw_agent.get("id") or f"agent-{i}"),
|
|
142
|
+
name=str(raw_agent.get("name") or f"agent-{i}"),
|
|
143
|
+
description=str(raw_agent.get("description") or ""),
|
|
144
|
+
class_name=str(raw_agent.get("class_name") or ""),
|
|
145
|
+
additional_topics=list(raw_agent.get("additional_topics") or []),
|
|
146
|
+
properties=raw_agent.get("properties", {}),
|
|
147
|
+
listen_to_default_topic=bool(raw_agent.get("listen_to_default_topic", True)),
|
|
148
|
+
act_only_when_tagged=bool(raw_agent.get("act_only_when_tagged", False)),
|
|
149
|
+
predicates=raw_agent.get("predicates") or {},
|
|
150
|
+
dependency_map=raw_agent.get("dependency_map") or {},
|
|
151
|
+
additional_dependencies=list(raw_agent.get("additional_dependencies") or []),
|
|
152
|
+
resources=raw_agent.get("resources") or {},
|
|
153
|
+
qos=raw_agent.get("qos") or {},
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
routes_raw = raw.get("routes") or {}
|
|
158
|
+
try:
|
|
159
|
+
routes = RoutingSlip.model_validate(routes_raw)
|
|
160
|
+
except Exception:
|
|
161
|
+
routes = RoutingSlip()
|
|
162
|
+
|
|
163
|
+
return GuildSpec.model_construct(
|
|
164
|
+
id=str(raw.get("id") or ""),
|
|
165
|
+
name=str(raw.get("name") or ""),
|
|
166
|
+
description=str(raw.get("description") or ""),
|
|
167
|
+
properties=raw.get("properties") or {},
|
|
168
|
+
agents=fallback_agents,
|
|
169
|
+
dependency_map=raw.get("dependency_map") or {},
|
|
170
|
+
routes=routes,
|
|
171
|
+
gateway=raw.get("gateway"),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _load_agent_spec(agent_spec_json: str) -> AgentSpec:
|
|
176
|
+
try:
|
|
177
|
+
return AgentSpec.model_validate_json(agent_spec_json)
|
|
178
|
+
except ValidationError as e:
|
|
179
|
+
logger.warning(
|
|
180
|
+
"AgentSpec strict validation failed; retrying with lenient class resolution: %s",
|
|
181
|
+
e,
|
|
182
|
+
)
|
|
183
|
+
_enable_lenient_class_resolution()
|
|
184
|
+
_enable_lenient_metastore_conversion()
|
|
185
|
+
return AgentSpec.model_validate_json(agent_spec_json)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def main():
|
|
189
|
+
try:
|
|
190
|
+
logger.info("Starting Forge Agent Runner...")
|
|
191
|
+
|
|
192
|
+
guild_spec_json = os.getenv("FORGE_GUILD_JSON")
|
|
193
|
+
if not guild_spec_json:
|
|
194
|
+
raise ValueError("FORGE_GUILD_JSON environment variable is missing.")
|
|
195
|
+
|
|
196
|
+
guild_spec = _load_guild_spec(guild_spec_json)
|
|
197
|
+
logger.debug(f"Loaded GuildSpec: {guild_spec.id} ({guild_spec.name})")
|
|
198
|
+
|
|
199
|
+
agent_spec_json = os.getenv("FORGE_AGENT_CONFIG_JSON")
|
|
200
|
+
if not agent_spec_json:
|
|
201
|
+
raise ValueError("FORGE_AGENT_CONFIG_JSON environment variable is missing.")
|
|
202
|
+
|
|
203
|
+
agent_spec = _load_agent_spec(agent_spec_json)
|
|
204
|
+
logger.info(f"Loaded AgentSpec: {agent_spec.id} ({agent_spec.name})")
|
|
205
|
+
|
|
206
|
+
client_type_str = os.getenv("FORGE_CLIENT_TYPE", "InMemoryMessagingBackend")
|
|
207
|
+
client_module_str = os.getenv("FORGE_CLIENT_MODULE", "")
|
|
208
|
+
client_props = json.loads(os.getenv("FORGE_CLIENT_PROPERTIES_JSON", "{}"))
|
|
209
|
+
|
|
210
|
+
backend_config = client_props.get("backend_config", client_props)
|
|
211
|
+
|
|
212
|
+
if (
|
|
213
|
+
client_type_str == "RedisMessagingBackend"
|
|
214
|
+
and "redis_client" not in backend_config
|
|
215
|
+
):
|
|
216
|
+
backend_config["redis_client"] = {
|
|
217
|
+
"host": os.getenv("REDIS_HOST", "localhost"),
|
|
218
|
+
"port": int(os.getenv("REDIS_PORT", "6379")),
|
|
219
|
+
"db": int(os.getenv("REDIS_DB", "0")),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
organization_id = client_props.pop("organization_id", None)
|
|
223
|
+
|
|
224
|
+
messaging_config = MessagingConfig(
|
|
225
|
+
backend_module=client_module_str,
|
|
226
|
+
backend_class=client_type_str,
|
|
227
|
+
backend_config=backend_config,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
logger.info(f"Using Messaging Backend: {client_type_str}")
|
|
231
|
+
|
|
232
|
+
machine_id = hash(f"{guild_spec.id}-{agent_spec.id}") % 256
|
|
233
|
+
|
|
234
|
+
wrapper = ForgeAgentWrapper(
|
|
235
|
+
guild_spec=guild_spec,
|
|
236
|
+
agent_spec=agent_spec,
|
|
237
|
+
messaging_config=messaging_config,
|
|
238
|
+
machine_id=machine_id,
|
|
239
|
+
organization_id=organization_id,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
wrapper.run()
|
|
243
|
+
|
|
244
|
+
logger.info("Forge Agent Runner exited gracefully.")
|
|
245
|
+
sys.exit(0)
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.critical(f"Forge Agent Runner crashed: {e}", exc_info=True)
|
|
249
|
+
sys.exit(1)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
main()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import signal
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, Optional, Type
|
|
6
|
+
|
|
7
|
+
from rustic_ai.core.guild.agent import AgentSpec
|
|
8
|
+
from rustic_ai.core.guild.dsl import GuildSpec
|
|
9
|
+
from rustic_ai.core.messaging import Client, MessageTrackingClient, MessagingConfig
|
|
10
|
+
from rustic_ai.core.guild.execution.agent_wrapper import AgentWrapper
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ForgeAgentWrapper(AgentWrapper):
|
|
16
|
+
"""
|
|
17
|
+
An implementation of AgentWrapper that runs as a standalone Python process
|
|
18
|
+
managed by the Go-based Forge AgentSupervisor.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
guild_spec: GuildSpec,
|
|
24
|
+
agent_spec: AgentSpec,
|
|
25
|
+
messaging_config: MessagingConfig,
|
|
26
|
+
machine_id: int,
|
|
27
|
+
client_type: Type[Client] = MessageTrackingClient,
|
|
28
|
+
client_properties: Optional[Dict[str, Any]] = None,
|
|
29
|
+
organization_id: Optional[str] = None,
|
|
30
|
+
):
|
|
31
|
+
if client_properties is None:
|
|
32
|
+
client_properties = {}
|
|
33
|
+
|
|
34
|
+
super().__init__(
|
|
35
|
+
guild_spec=guild_spec,
|
|
36
|
+
agent_spec=agent_spec,
|
|
37
|
+
messaging_config=messaging_config,
|
|
38
|
+
machine_id=machine_id,
|
|
39
|
+
client_type=client_type,
|
|
40
|
+
client_properties=client_properties,
|
|
41
|
+
organization_id=organization_id,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
self.shutdown_event = threading.Event()
|
|
45
|
+
self.is_running = False
|
|
46
|
+
|
|
47
|
+
def run(self) -> None:
|
|
48
|
+
"""Initializes the agent and blocks until shutdown is signaled."""
|
|
49
|
+
try:
|
|
50
|
+
self.is_running = True
|
|
51
|
+
logger.info(f"Initializing agent {self.agent_spec.name} in Forge wrapper")
|
|
52
|
+
self.initialize_agent()
|
|
53
|
+
logger.info(f"Agent {self.agent_spec.name} initialized successfully.")
|
|
54
|
+
|
|
55
|
+
def signal_handler(signum: int, frame: Any) -> None:
|
|
56
|
+
logger.info(
|
|
57
|
+
f"Received signal {signum}, initiating shutdown for {self.agent_spec.id}..."
|
|
58
|
+
)
|
|
59
|
+
self.shutdown_event.set()
|
|
60
|
+
|
|
61
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
62
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
63
|
+
|
|
64
|
+
while not self.shutdown_event.is_set():
|
|
65
|
+
time.sleep(0.5)
|
|
66
|
+
|
|
67
|
+
logger.info(f"Agent {self.agent_spec.id} shutting down cleanly.")
|
|
68
|
+
self.shutdown()
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.error(
|
|
72
|
+
f"Error running agent {self.agent_spec.name} in Forge wrapper: {e}",
|
|
73
|
+
exc_info=True,
|
|
74
|
+
)
|
|
75
|
+
self.is_running = False
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
def shutdown(self) -> None:
|
|
79
|
+
self.shutdown_event.set()
|
|
80
|
+
super().shutdown()
|
|
81
|
+
self.is_running = False
|