worker-agent 1.0.3__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.
- config.yaml +54 -0
- core/__init__.py +3 -0
- core/agent.py +344 -0
- core/config_loader.py +159 -0
- core/job_logger.py +198 -0
- main.py +134 -0
- py.typed +0 -0
- sample_main_agent.py +311 -0
- worker_agent-1.0.3.dist-info/METADATA +307 -0
- worker_agent-1.0.3.dist-info/RECORD +13 -0
- worker_agent-1.0.3.dist-info/WHEEL +4 -0
- worker_agent-1.0.3.dist-info/entry_points.txt +2 -0
- worker_agent-1.0.3.dist-info/licenses/LICENSE +21 -0
config.yaml
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Worker Agent Configuration
|
|
3
|
+
# Clone this file to create a new specialized worker — no code changes needed.
|
|
4
|
+
# =============================================================================
|
|
5
|
+
|
|
6
|
+
agent:
|
|
7
|
+
name: "WorkerAgent"
|
|
8
|
+
version: "1.0.0"
|
|
9
|
+
description: |
|
|
10
|
+
Run the Worker Agent's internal ReAct loop to complete a sub-task.
|
|
11
|
+
Args:
|
|
12
|
+
instruction: A clear, self-contained description of the task to perform.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
The final result produced by the agent after it has finished reasoning
|
|
16
|
+
and using its tools. A log file path is appended for traceability.
|
|
17
|
+
|
|
18
|
+
system_prompt: |
|
|
19
|
+
You are a diligent, autonomous worker agent.
|
|
20
|
+
Your job is to complete the sub-task given to you as precisely and efficiently as possible.
|
|
21
|
+
You have access to a set of tools. Think step-by-step, use the tools you need,
|
|
22
|
+
self-correct on errors, and only respond once the task is fully complete.
|
|
23
|
+
Do NOT ask for clarification — make your best effort with the information provided.
|
|
24
|
+
|
|
25
|
+
model:
|
|
26
|
+
provider: "ollama" # Supported: "ollama", "openai", "gemini"
|
|
27
|
+
model_name: "qwen3-coder:480b-cloud"
|
|
28
|
+
temperature: 0.0
|
|
29
|
+
base_url: "http://localhost:11434" # Uncomment if using openai or gemini (or set API_KEY env var) https://api.openai.com/v1 ollma local (http://localhost:11434)
|
|
30
|
+
# api_key: "your-api-key-here" # Uncomment if using openai or gemini (or set API_KEY env var)
|
|
31
|
+
|
|
32
|
+
# MCP Servers this worker connects TO as a CLIENT to get its tools.
|
|
33
|
+
# Each entry starts a subprocess and connects via stdio.
|
|
34
|
+
mcp_clients:
|
|
35
|
+
# - name: "filesystem-server"
|
|
36
|
+
# command: "npx"
|
|
37
|
+
# args: ["-y", "@modelcontextprotocol/server-filesystem", "C:/Users/Dev/Project"]
|
|
38
|
+
#
|
|
39
|
+
# - name: "brave-search"
|
|
40
|
+
# command: "npx"
|
|
41
|
+
# args: ["-y", "@modelcontextprotocol/server-brave-search"]
|
|
42
|
+
#
|
|
43
|
+
- name: "shell_execution"
|
|
44
|
+
command: "D:\\DEV\\ML\\Seeker\\.venv\\Scripts\\python.exe"
|
|
45
|
+
args: ["-m", "mcp_server_shell", "--shell", "powershell"]
|
|
46
|
+
env:
|
|
47
|
+
CWD: "D:\\DEV\\ML\\Seeker"
|
|
48
|
+
|
|
49
|
+
# How this worker exposes ITSELF as an MCP server.
|
|
50
|
+
server:
|
|
51
|
+
name: "worker-agent-server"
|
|
52
|
+
port: 8001 # HTTP/SSE transport port
|
|
53
|
+
transport: "stdio" # "stdio" or "sse"
|
|
54
|
+
host: "0.0.0.0"
|
core/__init__.py
ADDED
core/agent.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core/agent.py
|
|
3
|
+
-------------
|
|
4
|
+
The heart of the Worker Agent.
|
|
5
|
+
|
|
6
|
+
Responsibilities:
|
|
7
|
+
1. Read config to know which MCP servers to connect to.
|
|
8
|
+
2. Dynamically load all tools from those MCP servers (connections stay alive).
|
|
9
|
+
3. Build a LangGraph ReAct loop with those tools + the local Ollama LLM.
|
|
10
|
+
4. Log every single step to a per-job file via JobLogger.
|
|
11
|
+
5. Expose run_agent(task, config) -> str used by main.py.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import logging
|
|
18
|
+
import traceback
|
|
19
|
+
import warnings
|
|
20
|
+
from contextlib import AsyncExitStack
|
|
21
|
+
|
|
22
|
+
# Suppress LangGraph deprecation noise
|
|
23
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
24
|
+
|
|
25
|
+
from langchain_core.messages import (
|
|
26
|
+
AIMessage,
|
|
27
|
+
HumanMessage,
|
|
28
|
+
SystemMessage,
|
|
29
|
+
ToolMessage,
|
|
30
|
+
)
|
|
31
|
+
from langchain_core.tools import BaseTool
|
|
32
|
+
from langchain_ollama import ChatOllama
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from langchain_openai import ChatOpenAI
|
|
36
|
+
except ImportError:
|
|
37
|
+
pass
|
|
38
|
+
try:
|
|
39
|
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
40
|
+
except ImportError:
|
|
41
|
+
pass
|
|
42
|
+
from langchain_mcp_adapters.tools import load_mcp_tools
|
|
43
|
+
from langgraph.prebuilt import create_react_agent
|
|
44
|
+
from mcp import ClientSession, StdioServerParameters
|
|
45
|
+
from mcp.client.stdio import stdio_client
|
|
46
|
+
|
|
47
|
+
from core.config_loader import AppConfig
|
|
48
|
+
from core.job_logger import JobLogger
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Internal helpers
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _tool_input(tool_call: dict) -> dict:
|
|
59
|
+
"""Extract input args from a LangChain tool call dict."""
|
|
60
|
+
return tool_call.get("args", {}) or {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _truncate(text: str, max_chars: int = 800) -> str:
|
|
64
|
+
if len(text) <= max_chars:
|
|
65
|
+
return text
|
|
66
|
+
return text[:max_chars] + f"\n... [{len(text) - max_chars} chars truncated]"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _unwrap_exception(exc: BaseException) -> BaseException:
|
|
70
|
+
"""Recursively unwrap ExceptionGroups to find the underlying root cause."""
|
|
71
|
+
# Check if this is an ExceptionGroup (Python 3.11+ built-in)
|
|
72
|
+
if hasattr(exc, "exceptions") and exc.exceptions:
|
|
73
|
+
# We recursively unwrap the first exception in the group.
|
|
74
|
+
# This assumes the most interesting error is the first one (often true for simple TaskGroups).
|
|
75
|
+
return _unwrap_exception(exc.exceptions[0])
|
|
76
|
+
return exc
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Main entry-point — called by main.py for every execute_task() invocation
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def run_agent(task: str, config: AppConfig) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Execute a task using the full ReAct loop with per-job structured logging.
|
|
87
|
+
|
|
88
|
+
- One JobLogger (= one log file) is created per call.
|
|
89
|
+
- All MCP connections remain open for the entire loop duration.
|
|
90
|
+
- Every LLM output, tool call, tool result and error is logged as a step.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
task: The natural-language instruction for this worker.
|
|
94
|
+
config: The loaded AppConfig from config.yaml.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
str: The final answer produced by the agent.
|
|
98
|
+
"""
|
|
99
|
+
jl = JobLogger(task=task, agent_name=config.agent.name)
|
|
100
|
+
logger.info("[%s] Job %s started | task: %s", config.agent.name, jl.job_id, task[:80])
|
|
101
|
+
|
|
102
|
+
all_tools: list[BaseTool] = []
|
|
103
|
+
final_answer = ""
|
|
104
|
+
success = False
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
# ----------------------------------------------------------------
|
|
108
|
+
# PHASE 1 — Connect to all MCP tool servers
|
|
109
|
+
# ----------------------------------------------------------------
|
|
110
|
+
async with AsyncExitStack() as stack:
|
|
111
|
+
for client_cfg in config.mcp_clients:
|
|
112
|
+
server_params = StdioServerParameters(
|
|
113
|
+
command=client_cfg.command,
|
|
114
|
+
args=client_cfg.args,
|
|
115
|
+
env=client_cfg.env or None,
|
|
116
|
+
)
|
|
117
|
+
try:
|
|
118
|
+
read, write = await stack.enter_async_context(stdio_client(server_params))
|
|
119
|
+
session: ClientSession = await stack.enter_async_context(
|
|
120
|
+
ClientSession(read, write)
|
|
121
|
+
)
|
|
122
|
+
await session.initialize()
|
|
123
|
+
tools = await load_mcp_tools(session)
|
|
124
|
+
tool_names = [t.name for t in tools]
|
|
125
|
+
all_tools.extend(tools)
|
|
126
|
+
|
|
127
|
+
jl.log_step(
|
|
128
|
+
step_type="MCP_CONNECT",
|
|
129
|
+
title=client_cfg.name,
|
|
130
|
+
details={
|
|
131
|
+
"command": client_cfg.command,
|
|
132
|
+
"args": client_cfg.args,
|
|
133
|
+
"tools_loaded": tool_names,
|
|
134
|
+
},
|
|
135
|
+
success=True,
|
|
136
|
+
)
|
|
137
|
+
logger.info("[%s] Connected | tools: %s", client_cfg.name, tool_names)
|
|
138
|
+
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
tb = traceback.format_exc()
|
|
141
|
+
jl.log_step(
|
|
142
|
+
step_type="MCP_CONNECT",
|
|
143
|
+
title=client_cfg.name,
|
|
144
|
+
details={"command": client_cfg.command, "args": client_cfg.args},
|
|
145
|
+
error=f"{exc}\n{tb}",
|
|
146
|
+
success=False,
|
|
147
|
+
)
|
|
148
|
+
logger.error("[%s] Connection failed: %s", client_cfg.name, exc)
|
|
149
|
+
|
|
150
|
+
if not all_tools:
|
|
151
|
+
jl.log_step(
|
|
152
|
+
step_type="INFO",
|
|
153
|
+
title="No tools available",
|
|
154
|
+
details={
|
|
155
|
+
"note": "Running with LLM only (no MCP tools configured or all failed to connect)."
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# ----------------------------------------------------------------
|
|
160
|
+
# PHASE 2 — Build the ReAct agent
|
|
161
|
+
# ----------------------------------------------------------------
|
|
162
|
+
tool_descriptions = "\n".join(
|
|
163
|
+
f" - {t.name}: {getattr(t, 'description', 'no description')}" for t in all_tools
|
|
164
|
+
)
|
|
165
|
+
enriched_prompt = config.agent.system_prompt + (
|
|
166
|
+
f"\n\nAvailable tools:\n{tool_descriptions}" if tool_descriptions else ""
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
provider = config.model.provider.lower()
|
|
170
|
+
if provider == "openai":
|
|
171
|
+
llm = ChatOpenAI(
|
|
172
|
+
model=config.model.model_name,
|
|
173
|
+
temperature=config.model.temperature,
|
|
174
|
+
api_key=config.model.api_key,
|
|
175
|
+
base_url=config.model.base_url
|
|
176
|
+
if config.model.base_url != "http://localhost:11434"
|
|
177
|
+
else None,
|
|
178
|
+
)
|
|
179
|
+
elif provider == "gemini":
|
|
180
|
+
llm = ChatGoogleGenerativeAI(
|
|
181
|
+
model=config.model.model_name,
|
|
182
|
+
temperature=config.model.temperature,
|
|
183
|
+
api_key=config.model.api_key,
|
|
184
|
+
)
|
|
185
|
+
else:
|
|
186
|
+
# default to ollama
|
|
187
|
+
llm = ChatOllama(
|
|
188
|
+
model=config.model.model_name,
|
|
189
|
+
temperature=config.model.temperature,
|
|
190
|
+
base_url=config.model.base_url,
|
|
191
|
+
)
|
|
192
|
+
graph = create_react_agent(model=llm, tools=all_tools)
|
|
193
|
+
|
|
194
|
+
jl.log_step(
|
|
195
|
+
step_type="AGENT_INIT",
|
|
196
|
+
title="LangGraph ReAct agent ready",
|
|
197
|
+
details={
|
|
198
|
+
"model": config.model.model_name,
|
|
199
|
+
"temperature": config.model.temperature,
|
|
200
|
+
"tools": [t.name for t in all_tools],
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# ----------------------------------------------------------------
|
|
205
|
+
# PHASE 3 — Run the ReAct loop + log every event
|
|
206
|
+
# ----------------------------------------------------------------
|
|
207
|
+
messages = [
|
|
208
|
+
SystemMessage(content=enriched_prompt),
|
|
209
|
+
HumanMessage(content=task),
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
# Track which tool calls we've already logged (by tool call id)
|
|
213
|
+
_logged_tool_calls: set = set()
|
|
214
|
+
_llm_step = 0
|
|
215
|
+
|
|
216
|
+
async for event in graph.astream(
|
|
217
|
+
{"messages": messages},
|
|
218
|
+
stream_mode="values",
|
|
219
|
+
):
|
|
220
|
+
last_msg = event["messages"][-1]
|
|
221
|
+
|
|
222
|
+
# ── AIMessage: LLM produced text or tool-call plan ────────
|
|
223
|
+
if isinstance(last_msg, AIMessage):
|
|
224
|
+
tool_calls = getattr(last_msg, "tool_calls", []) or []
|
|
225
|
+
|
|
226
|
+
# Log the text part of the LLM response (if any)
|
|
227
|
+
if last_msg.content:
|
|
228
|
+
_llm_step += 1
|
|
229
|
+
|
|
230
|
+
# Handle list-based content (e.g., from Gemini's multimodal format)
|
|
231
|
+
content_val = last_msg.content
|
|
232
|
+
if isinstance(content_val, list):
|
|
233
|
+
content_val = "\n".join(
|
|
234
|
+
b.get("text", str(b)) if isinstance(b, dict) else str(b)
|
|
235
|
+
for b in content_val
|
|
236
|
+
)
|
|
237
|
+
elif not isinstance(content_val, str):
|
|
238
|
+
content_val = str(content_val)
|
|
239
|
+
|
|
240
|
+
jl.log_step(
|
|
241
|
+
step_type="LLM_RESPONSE",
|
|
242
|
+
title=f"LLM turn {_llm_step}",
|
|
243
|
+
output=_truncate(content_val),
|
|
244
|
+
)
|
|
245
|
+
final_answer = content_val
|
|
246
|
+
|
|
247
|
+
# Log each tool call the LLM decided to make
|
|
248
|
+
for tc in tool_calls:
|
|
249
|
+
tc_id = tc.get("id", "")
|
|
250
|
+
if tc_id in _logged_tool_calls:
|
|
251
|
+
continue
|
|
252
|
+
_logged_tool_calls.add(tc_id)
|
|
253
|
+
jl.log_step(
|
|
254
|
+
step_type="TOOL_CALL",
|
|
255
|
+
title=tc.get("name", "unknown"),
|
|
256
|
+
details={
|
|
257
|
+
"tool": tc.get("name"),
|
|
258
|
+
"call_id": tc_id,
|
|
259
|
+
"input": _tool_input(tc),
|
|
260
|
+
},
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# ── ToolMessage: result came back from a tool ─────────────
|
|
264
|
+
elif isinstance(last_msg, ToolMessage):
|
|
265
|
+
raw_content = last_msg.content or ""
|
|
266
|
+
# Detect error by checking for non-zero return codes or exception text
|
|
267
|
+
is_error = (
|
|
268
|
+
"error" in str(raw_content).lower()
|
|
269
|
+
or "exception" in str(raw_content).lower()
|
|
270
|
+
or "traceback" in str(raw_content).lower()
|
|
271
|
+
)
|
|
272
|
+
jl.log_step(
|
|
273
|
+
step_type="TOOL_RESULT",
|
|
274
|
+
title=getattr(last_msg, "name", "tool") or "tool",
|
|
275
|
+
details={"call_id": getattr(last_msg, "tool_call_id", "")},
|
|
276
|
+
output=_truncate(str(raw_content)),
|
|
277
|
+
success=not is_error,
|
|
278
|
+
error=str(raw_content) if is_error else None,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# ── Done ──────────────────────────────────────────────────────
|
|
282
|
+
success = True
|
|
283
|
+
|
|
284
|
+
except BaseException as root_exc:
|
|
285
|
+
# We catch BaseException so we can catch BaseExceptionGroup
|
|
286
|
+
exc = _unwrap_exception(root_exc)
|
|
287
|
+
tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
|
|
288
|
+
|
|
289
|
+
# Don't log normal cancellations as fatal errors
|
|
290
|
+
if isinstance(exc, (asyncio.CancelledError, KeyboardInterrupt)):
|
|
291
|
+
logger.warning(
|
|
292
|
+
"[%s] Job %s was cancelled or interrupted.", config.agent.name, jl.job_id
|
|
293
|
+
)
|
|
294
|
+
final_answer = "ERROR: Agent task was cancelled or interrupted."
|
|
295
|
+
success = False
|
|
296
|
+
|
|
297
|
+
# Gracefully log familiar LLM provider errors without tracebacks
|
|
298
|
+
elif type(exc).__name__ in (
|
|
299
|
+
"RateLimitError",
|
|
300
|
+
"AuthenticationError",
|
|
301
|
+
"APIConnectionError",
|
|
302
|
+
"APIError",
|
|
303
|
+
"InvalidRequestError",
|
|
304
|
+
):
|
|
305
|
+
jl.log_step(
|
|
306
|
+
step_type="LLM_API_ERROR",
|
|
307
|
+
title=type(exc).__name__,
|
|
308
|
+
error=f"{exc}",
|
|
309
|
+
success=False,
|
|
310
|
+
)
|
|
311
|
+
logger.error(
|
|
312
|
+
"[%s] Job %s failed with LLM provider API error: %s",
|
|
313
|
+
config.agent.name,
|
|
314
|
+
jl.job_id,
|
|
315
|
+
exc,
|
|
316
|
+
)
|
|
317
|
+
final_answer = f"ERROR: LLM Provider Issue ({type(exc).__name__}): {exc}"
|
|
318
|
+
success = False
|
|
319
|
+
|
|
320
|
+
# All other unhandled exceptions get the dreaded Traceback log
|
|
321
|
+
else:
|
|
322
|
+
jl.log_step(
|
|
323
|
+
step_type="FATAL_ERROR",
|
|
324
|
+
title=type(exc).__name__,
|
|
325
|
+
error=f"{exc}\n\n{tb}",
|
|
326
|
+
success=False,
|
|
327
|
+
)
|
|
328
|
+
logger.exception(
|
|
329
|
+
"[%s] Job %s failed with unhandled exception: %s", config.agent.name, jl.job_id, exc
|
|
330
|
+
)
|
|
331
|
+
final_answer = f"ERROR: {type(exc).__name__}: {exc}"
|
|
332
|
+
success = False
|
|
333
|
+
|
|
334
|
+
finally:
|
|
335
|
+
jl.finish(final_answer=final_answer, success=success)
|
|
336
|
+
logger.info(
|
|
337
|
+
"[%s] Job %s %s | log: %s",
|
|
338
|
+
config.agent.name,
|
|
339
|
+
jl.job_id,
|
|
340
|
+
"COMPLETE" if success else "FAILED",
|
|
341
|
+
jl.path,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
return final_answer or "Agent completed the task but produced no text output."
|
core/config_loader.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
core/config_loader.py
|
|
3
|
+
---------------------
|
|
4
|
+
Loads and validates config.yaml into typed dataclasses.
|
|
5
|
+
Import and use `load_config()` anywhere in the project.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Dataclass models
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ModelConfig:
|
|
23
|
+
provider: str = "ollama" # e.g., "ollama", "openai", "gemini"
|
|
24
|
+
model_name: str = "llama3.2"
|
|
25
|
+
temperature: float = 0.0
|
|
26
|
+
base_url: str = "http://localhost:11434"
|
|
27
|
+
api_key: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class MCPClientConfig:
|
|
32
|
+
"""Represents one external MCP server this worker connects to as a client."""
|
|
33
|
+
|
|
34
|
+
name: str
|
|
35
|
+
command: str
|
|
36
|
+
args: list[str] = field(default_factory=list)
|
|
37
|
+
env: dict = field(default_factory=dict) # optional extra env vars
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ServerConfig:
|
|
42
|
+
name: str = "worker-agent-server"
|
|
43
|
+
port: int = 8001
|
|
44
|
+
transport: str = "stdio" # "stdio" | "sse"
|
|
45
|
+
host: str = "0.0.0.0"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
DEFAULT_DESCRIPTION = (
|
|
49
|
+
"Run the Worker Agent's internal ReAct loop to complete a sub-task.\n"
|
|
50
|
+
"Args:\n"
|
|
51
|
+
" instruction: A clear, self-contained description of the task to perform.\n\n"
|
|
52
|
+
"Returns:\n"
|
|
53
|
+
" The final result produced by the agent after it has finished reasoning\n"
|
|
54
|
+
" and using its tools. A log file path is appended for traceability."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class AgentConfig:
|
|
60
|
+
name: str = "WorkerAgent"
|
|
61
|
+
version: str = "1.0.0"
|
|
62
|
+
description: str = DEFAULT_DESCRIPTION
|
|
63
|
+
system_prompt: str = "You are a helpful worker agent."
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class AppConfig:
|
|
68
|
+
agent: AgentConfig = field(default_factory=AgentConfig)
|
|
69
|
+
model: ModelConfig = field(default_factory=ModelConfig)
|
|
70
|
+
mcp_clients: list[MCPClientConfig] = field(default_factory=list)
|
|
71
|
+
server: ServerConfig = field(default_factory=ServerConfig)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Loader
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_config(config_path: str | None = None) -> AppConfig:
|
|
80
|
+
"""
|
|
81
|
+
Priority:
|
|
82
|
+
1. config_path (passed to function)
|
|
83
|
+
2. WORKER_AGENT_CONFIG (environment variable)
|
|
84
|
+
3. ./config.yaml (Current Working Directory)
|
|
85
|
+
4. ../config.yaml (Package Root fallback)
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
# Define potential locations
|
|
89
|
+
env_path = os.getenv("WORKER_AGENT_CONFIG")
|
|
90
|
+
cwd_path = Path.cwd() / "config.yaml"
|
|
91
|
+
package_root_path = Path(__file__).parent.parent / "config.yaml"
|
|
92
|
+
|
|
93
|
+
# Select the first one that exists
|
|
94
|
+
if config_path:
|
|
95
|
+
final_path = Path(config_path)
|
|
96
|
+
elif env_path:
|
|
97
|
+
final_path = Path(env_path)
|
|
98
|
+
elif cwd_path.exists():
|
|
99
|
+
final_path = cwd_path
|
|
100
|
+
else:
|
|
101
|
+
final_path = package_root_path
|
|
102
|
+
|
|
103
|
+
# Final check
|
|
104
|
+
if not final_path.exists():
|
|
105
|
+
raise FileNotFoundError(
|
|
106
|
+
f"Config file not found. Checked:\n"
|
|
107
|
+
f"- Explicit path: {config_path}\n"
|
|
108
|
+
f"- Env Var (WORKER_AGENT_CONFIG): {env_path}\n"
|
|
109
|
+
f"- Current Directory: {cwd_path}\n"
|
|
110
|
+
f"- Package Fallback: {package_root_path}\n"
|
|
111
|
+
f"Please ensure a 'config.yaml' exists in your current folder."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
print(f"[*] Using config: {final_path.absolute()}")
|
|
115
|
+
|
|
116
|
+
with open(final_path, encoding="utf-8") as f:
|
|
117
|
+
raw = yaml.safe_load(f) or {}
|
|
118
|
+
|
|
119
|
+
# --- Agent ---
|
|
120
|
+
agent_raw = raw.get("agent", {})
|
|
121
|
+
agent = AgentConfig(
|
|
122
|
+
name=agent_raw.get("name", "WorkerAgent"),
|
|
123
|
+
version=agent_raw.get("version", "1.0.0"),
|
|
124
|
+
description=agent_raw.get("description", DEFAULT_DESCRIPTION),
|
|
125
|
+
system_prompt=agent_raw.get("system_prompt", "You are a helpful worker agent."),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# --- Model ---
|
|
129
|
+
model_raw = raw.get("model", {})
|
|
130
|
+
model = ModelConfig(
|
|
131
|
+
provider=model_raw.get("provider", "ollama"),
|
|
132
|
+
model_name=model_raw.get("model_name", "llama3.2"),
|
|
133
|
+
temperature=float(model_raw.get("temperature", 0.0)),
|
|
134
|
+
base_url=model_raw.get("base_url", "http://localhost:11434"),
|
|
135
|
+
api_key=model_raw.get("api_key", os.getenv("API_KEY")),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# --- MCP Clients ---
|
|
139
|
+
mcp_clients = []
|
|
140
|
+
for entry in raw.get("mcp_clients", []) or []:
|
|
141
|
+
mcp_clients.append(
|
|
142
|
+
MCPClientConfig(
|
|
143
|
+
name=entry["name"],
|
|
144
|
+
command=entry["command"],
|
|
145
|
+
args=entry.get("args", []),
|
|
146
|
+
env=entry.get("env", {}),
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# --- Server ---
|
|
151
|
+
server_raw = raw.get("server", {})
|
|
152
|
+
server = ServerConfig(
|
|
153
|
+
name=server_raw.get("name", "worker-agent-server"),
|
|
154
|
+
port=int(server_raw.get("port", 8001)),
|
|
155
|
+
transport=server_raw.get("transport", "stdio"),
|
|
156
|
+
host=server_raw.get("host", "0.0.0.0"),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return AppConfig(agent=agent, model=model, mcp_clients=mcp_clients, server=server)
|