-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathcore.py
More file actions
164 lines (136 loc) · 5.17 KB
/
Copy pathcore.py
File metadata and controls
164 lines (136 loc) · 5.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
"""
The agent loop.
run_agent() — single provider, all tools run locally
run_agent_mixed() — local Ollama orchestrates; routes complex tasks to a remote LLM
"""
from __future__ import annotations
from providers import LLMProvider, LLMMessage
from tools import TOOL_SCHEMAS, call_tool
from ui import Spinner
_SYSTEM_DEFAULT = (
"You are a helpful assistant. Use the available tools when needed. "
"When you have a complete final answer, respond in plain text without calling any tools."
)
_SYSTEM_MIXED = (
"You are a local AI orchestrator running on the user's machine. "
"You have access to local tools: get_current_date, calculate, get_weather, web_search. "
"You also have ask_remote() to delegate tasks to a more capable cloud model. "
"Use local tools for simple lookups, calculations, and factual queries. "
"Use ask_remote() for tasks that need deep reasoning, creative writing, detailed "
"explanations, or broad world knowledge that you are not confident about. "
"Always assemble the final answer yourself."
)
_ASK_REMOTE_SCHEMA = {
"type": "function",
"function": {
"name": "ask_remote",
"description": (
"Delegate a task to the more capable remote AI model. "
"Use this for complex reasoning, creative writing, detailed explanations, "
"or questions that require broad world knowledge."
),
"parameters": {
"type": "object",
"properties": {
"task": {
"type": "string",
"description": "The specific question or subtask to send to the remote model.",
}
},
"required": ["task"],
},
},
}
def _fmt_args(arguments: dict) -> str:
s = ", ".join(f"{k}={v!r}" for k, v in arguments.items())
return (s[:80] + "…") if len(s) > 80 else s
def run_agent(
task: str,
provider: LLMProvider,
tools: list[dict] | None = None,
extra_functions: dict | None = None,
system_prompt: str = _SYSTEM_DEFAULT,
verbose: bool = True,
tool_dispatcher=None,
_spinner: Spinner | None = None,
) -> str:
"""
Core agent loop.
Sends the task to the provider, executes tool calls, feeds results back,
and repeats until the model returns a plain-text final answer.
extra_functions: optional dict of {name: callable} for dynamically injected
tools (e.g. ask_remote in mixed mode).
_spinner: caller-supplied Spinner; run_agent_mixed passes its own so that
ask_remote() can update the same spinner line.
"""
if tools is None:
tools = TOOL_SCHEMAS
spinner: Spinner | None = (_spinner or Spinner()) if verbose else None
messages: list[dict] = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": task},
]
while True:
if spinner:
spinner.start("Thinking…")
try:
response: LLMMessage = provider.complete(messages, tools)
finally:
if spinner:
spinner.stop()
messages.append(response.to_dict())
if not response.tool_calls:
return response.content or ""
dispatch = tool_dispatcher or call_tool
for tc in response.tool_calls:
args_str = _fmt_args(tc.arguments)
if spinner:
spinner.start(f"{tc.name}({args_str})")
try:
if extra_functions and tc.name in extra_functions:
result = extra_functions[tc.name](**tc.arguments)
else:
result = dispatch(tc.name, tc.arguments)
finally:
if spinner:
spinner.stop()
if spinner:
result_preview = str(result)
if len(result_preview) > 100:
result_preview = result_preview[:100] + "…"
spinner.println(f" ✓ {tc.name}({args_str}) → {result_preview}")
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": str(result),
})
def run_agent_mixed(
task: str,
local_provider: LLMProvider,
remote_provider: LLMProvider,
verbose: bool = True,
) -> str:
"""
Mixed-mode agent loop.
The local Ollama model acts as orchestrator: it handles simple tool calls
itself and routes complex subtasks to the remote model via ask_remote().
"""
spinner: Spinner | None = Spinner() if verbose else None
def ask_remote(task: str) -> str:
task_preview = (task[:60] + "…") if len(task) > 60 else task
if spinner:
spinner.update(f"→ remote ({remote_provider}): {task_preview}")
result = run_agent(task, remote_provider, verbose=False)
if spinner:
spinner.update("← remote: done")
return result
mixed_tools = TOOL_SCHEMAS + [_ASK_REMOTE_SCHEMA]
return run_agent(
task=task,
provider=local_provider,
tools=mixed_tools,
extra_functions={"ask_remote": ask_remote},
system_prompt=_SYSTEM_MIXED,
verbose=verbose,
_spinner=spinner,
)