Joar Paganus commited on
Commit
89030a0
·
1 Parent(s): 9f120d8

update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +60 -262
agent.py CHANGED
@@ -1,254 +1,48 @@
1
- import random
2
  import inspect
3
  import re
 
4
 
5
  from llama_cpp import Llama
6
 
7
 
8
- # # ------------- TOOLS / FUNCTIONS --------------
9
- # # Some of the structure of the agent have been inspired by:
10
- # # https://github.com/Pirner/zettelkasten/blob/main/main_notes/1_0_tool_calling_with_llama.py?source=post_page-----23e3d783a6d8---------------------------------------
11
-
12
-
13
- # def get_weather(location: str) -> str:
14
- # """This tool returns the current weather situation.
15
- # Args:
16
- # location: The city or place to check
17
- # Returns:
18
- # str: Weather situation (e.g. cloudy, rainy, sunny)
19
- # """
20
- # weather_situations = ["cloudy", "rainy", "sunny", "foobar"]
21
- # return random.choice(weather_situations)
22
-
23
-
24
- # def get_temperature(location: str) -> str:
25
- # """This tool returns the current temperature.
26
- # Args:
27
- # location: The city or place to check
28
- # Returns:
29
- # str: Temperature
30
- # """
31
- # temperature = ["-10", "0", "20", "30"]
32
- # return random.choice(temperature)
33
-
34
-
35
- # TOOLS = [get_weather, get_temperature]
36
- # TOOL_REGISTRY = {f.__name__: f for f in TOOLS}
37
-
38
-
39
- # def function_to_json(func) -> dict:
40
- # """
41
- # Converts a Python function into a JSON-serializable dictionary
42
- # that describes the function's signature, including its name,
43
- # description, and parameters.
44
- # """
45
- # type_map = {
46
- # str: "string",
47
- # int: "integer",
48
- # float: "number",
49
- # bool: "boolean",
50
- # list: "array",
51
- # dict: "object",
52
- # type(None): "null",
53
- # }
54
-
55
- # try:
56
- # signature = inspect.signature(func)
57
- # except ValueError as e:
58
- # raise ValueError(
59
- # f"Failed to get signature for function {func.__name__}: {str(e)}"
60
- # )
61
-
62
- # parameters = {}
63
- # for param in signature.parameters.values():
64
- # param_type = type_map.get(param.annotation, "string")
65
- # parameters[param.name] = {"type": param_type}
66
-
67
- # required = [
68
- # param.name
69
- # for param in signature.parameters.values()
70
- # if param.default == inspect._empty
71
- # ]
72
-
73
- # return {
74
- # "type": "function",
75
- # "function": {
76
- # "name": func.__name__,
77
- # "description": func.__doc__ or "",
78
- # "parameters": {
79
- # "type": "object",
80
- # "properties": parameters,
81
- # "required": required,
82
- # },
83
- # },
84
- # }
85
-
86
-
87
- # TOOLS_SCHEMA = [function_to_json(f) for f in TOOLS]
88
-
89
-
90
- # def parse_tool_calls(tool_output: str):
91
- # """
92
- # Very simple parser for outputs like:
93
- # [get_weather(location="Berlin")]
94
- # Returns a list of (func_name, kwargs) tuples.
95
- # """
96
- # calls = []
97
- # # Find patterns like func_name(...)
98
- # for match in re.finditer(r"(\w+)\((.*?)\)", tool_output, re.DOTALL):
99
- # func_name, arg_str = match.groups()
100
- # func_name = func_name.strip()
101
- # kwargs = {}
102
-
103
- # arg_str = arg_str.strip()
104
- # if arg_str:
105
- # parts = re.split(r",\s*", arg_str)
106
- # for part in parts:
107
- # if "=" not in part:
108
- # continue
109
- # key, val = part.split("=", 1)
110
- # key = key.strip()
111
- # val = val.strip().strip('"').strip("'")
112
-
113
- # # Try to cast numbers, else keep as string
114
- # try:
115
- # if "." in val:
116
- # parsed_val = float(val)
117
- # else:
118
- # parsed_val = int(val)
119
- # except ValueError:
120
- # parsed_val = val
121
- # kwargs[key] = parsed_val
122
-
123
- # calls.append((func_name, kwargs))
124
-
125
- # return calls
126
-
127
-
128
- # # ------------- HELPER: GENERATION -------------
129
-
130
- # def generate_non_stream(llm, prompt, max_tokens=256, temperature=0.2, top_p=0.95):
131
- # """One-shot generation for internal agent/tool prompts."""
132
- # out = llm(
133
- # prompt,
134
- # max_tokens=max_tokens,
135
- # temperature=temperature,
136
- # top_p=top_p,
137
- # stop=["User:", "System:"],
138
- # stream=False,
139
- # )
140
- # return out["choices"][0]["text"]
141
-
142
-
143
- # def build_prompt(system_message, history, user_message):
144
- # prompt = f"System: {system_message}\n"
145
- # for turn in history:
146
- # role = turn["role"]
147
- # content = turn["content"]
148
- # prompt += f"{role.capitalize()}: {content}\n"
149
- # prompt += f"User: {user_message}\nAssistant:"
150
- # return prompt
151
-
152
-
153
- # def select_tools_with_llm(llm, user_message: str) -> list:
154
- # """
155
- # Ask the model which tools to call.
156
- # Returns a list of (func_name, kwargs) from parse_tool_calls.
157
- # """
158
- # tool_selection_system = f"""
159
- # You are an expert in composing functions.
160
- # You are given a user question and a set of possible functions (tools).
161
-
162
- # Your job is to decide which tools to call and with what arguments.
163
-
164
- # Rules:
165
- # - If you decide to invoke any function(s), you MUST put them in the format:
166
- # [func_name1(param1=value1, param2=value2), func_name2(param1=value1)]
167
- # - If none of the functions are suitable, respond with: []
168
- # - Do NOT include any explanation or extra text, only the list.
169
- # - If the question lacks required parameters, respond with [].
170
-
171
- # Here is a list of functions in JSON format that you can invoke:
172
- # {TOOLS_SCHEMA}
173
- # """
174
-
175
- # prompt = (
176
- # f"System: {tool_selection_system}\n"
177
- # f"User: {user_message}\n"
178
- # f"Assistant:"
179
- # )
180
-
181
- # raw = generate_non_stream(
182
- # llm,
183
- # prompt,
184
- # max_tokens=256,
185
- # temperature=0.2,
186
- # top_p=0.95,
187
- # )
188
-
189
- # return parse_tool_calls(raw)
190
-
191
-
192
- # def call_tools(tool_calls):
193
- # """
194
- # Execute the tools chosen by the model.
195
- # Returns a list of dicts: {name, args, result}.
196
- # """
197
- # results = []
198
- # for func_name, kwargs in tool_calls:
199
- # func = TOOL_REGISTRY.get(func_name)
200
- # if func is None:
201
- # results.append(
202
- # {
203
- # "name": func_name,
204
- # "args": kwargs,
205
- # "result": f"Unknown tool '{func_name}'.",
206
- # }
207
- # )
208
- # continue
209
-
210
- # try:
211
- # res = func(**kwargs)
212
- # except Exception as e:
213
- # res = f"Error while calling {func_name}: {e}"
214
-
215
- # results.append({"name": func_name, "args": kwargs, "result": res})
216
- # return results
217
-
218
-
219
- # # ------------- CHAT + AGENT LOGIC -------------
220
-
221
- # def respond(message, history, system_message, llm):
222
- # # ---- 1) Let the model decide if any tools should be used ----
223
- # tool_calls = select_tools_with_llm(llm, message)
224
- # tool_results = call_tools(tool_calls) if tool_calls else []
225
-
226
- # # ---- 2) Build final system message including tool results ----
227
- # if tool_results:
228
- # tool_info_str = "\nYou have executed the following tools (name, args, result):\n"
229
- # for tr in tool_results:
230
- # tool_info_str += f"- {tr['name']}({tr['args']}) -> {tr['result']}\n"
231
- # final_system_message = system_message + tool_info_str
232
- # else:
233
- # final_system_message = system_message
234
-
235
- # # ---- 3) Use normal chat-style prompt to answer the user ----
236
- # prompt = build_prompt(final_system_message, history, message)
237
-
238
- # stream = llm(
239
- # prompt,
240
- # max_tokens=256,
241
- # temperature=0.7,
242
- # top_p=0.9,
243
- # stop=["User:", "System:"],
244
- # stream=True,
245
- # )
246
 
247
- # partial = ""
248
- # for out in stream:
249
- # token = out["choices"][0]["text"]
250
- # partial += token
251
- # yield partial
252
 
253
  import inspect
254
  import re
@@ -338,19 +132,16 @@ def build_prompt(system_message, history, user_message):
338
  return prompt
339
 
340
  def select_tools_with_llm(llm, user_message: str, tools_schema: list) -> list:
341
- tool_selection_system = f"""You are an expert in composing functions.
342
- You are given a user question and a set of possible functions (tools).
343
- Your job is to decide which tools to call and with what arguments.
344
 
345
- Rules:
346
- - If you decide to invoke any function(s), you MUST put them in the format:
347
- [func_name1(param1=value1, param2=value2), func_name2(param1=value1)]
348
- - If none of the functions are suitable, respond with: []
349
- - Do NOT include any explanation or extra text, only the list.
350
- - If the question lacks required parameters, respond with [].
351
 
352
  Here is a list of functions in JSON format that you can invoke:
353
- {tools_schema}
 
354
  """
355
  prompt = (
356
  f"System: {tool_selection_system}\n"
@@ -364,7 +155,7 @@ Here is a list of functions in JSON format that you can invoke:
364
  temperature=0.2,
365
  top_p=0.95,
366
  )
367
- return parse_tool_calls(raw)
368
 
369
  def call_tools(tool_calls, tool_registry):
370
  results = []
@@ -390,23 +181,30 @@ def respond(message, history, system_message, llm, tools=None):
390
  if tools is None:
391
  tools = []
392
 
393
- # 1. Build Schema & Registry on the fly
394
  tool_registry = {f.__name__: f for f in tools}
395
  tools_schema = [function_to_json(f) for f in tools]
396
 
397
- # 2. Pass them down
398
- tool_calls = select_tools_with_llm(llm, message, tools_schema)
399
- tool_results = call_tools(tool_calls, tool_registry) if tool_calls else []
 
 
 
400
 
401
- if tool_results:
402
- tool_info_str = "\nYou have executed the following tools (name, args, result):\n"
 
 
403
  for tr in tool_results:
404
  tool_info_str += f"- {tr['name']}({tr['args']}) -> {tr['result']}\n"
405
- final_system_message = system_message + tool_info_str
406
  else:
407
  final_system_message = system_message
408
 
 
409
  prompt = build_prompt(final_system_message, history, message)
 
410
  stream = llm(
411
  prompt,
412
  max_tokens=256,
 
 
1
  import inspect
2
  import re
3
+ import json
4
 
5
  from llama_cpp import Llama
6
 
7
 
8
+ # ------------- TOOLS / FUNCTIONS --------------
9
+ # Some of the structure of the agent have been inspired by:
10
+ # https://github.com/Pirner/zettelkasten/blob/main/main_notes/1_0_tool_calling_with_llama.py?source=post_page-----23e3d783a6d8---------------------------------------
11
+
12
+ # ------------- CONIFG ---------------------
13
+ LAST_SYSTEM_MESSAGE = """.
14
+
15
+ GENERAL BEHAVIOR:
16
+ - Answer the user’s question clearly, concisely, and in natural language.
17
+ - Always speak as a normal assistant. Do not mention tools, tool calls, or system messages.
18
+
19
+ TOOL RESULTS:
20
+ - The system message may include a <tool_results> block.
21
+ - A <tool_results> block lists one or more tool outputs in the form:
22
+ <tool_results>
23
+ - tool_name(args_dict) -> result_value
24
+ </tool_results>
25
+
26
+ INSTRUCTIONS FOR USING TOOL RESULTS:
27
+ - Treat the information inside <tool_results> as *absolute ground truth*.
28
+ - Use those results to answer the user’s latest question.
29
+ - Summarize the results naturally. Do NOT restate the log format.
30
+ - NEVER reproduce or invent <tool_results> blocks.
31
+ - NEVER output lines of the form:
32
+ "You have executed the following tools..."
33
+ "- tool_name(args_dict) -> result"
34
+ - NEVER fabricate new tool calls or logs.
35
+
36
+ WHEN THERE ARE NO TOOL RESULTS:
37
+ - Answer based on weather knowledge, common sense, and conversation context.
38
+ - Still avoid any mention of “tools”, “calls”, “executions”, or logs.
39
+
40
+ YOUR OUTPUT:
41
+ - Your entire reply must be ONLY natural language directed to the user.
42
+ - Under no circumstance output raw tool logs or anything resembling them.
43
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
+ # -------------------------------------------
 
 
 
 
46
 
47
  import inspect
48
  import re
 
132
  return prompt
133
 
134
  def select_tools_with_llm(llm, user_message: str, tools_schema: list) -> list:
135
+ tool_selection_system = f"""You are an expert in composing functions. You are given a question and a set of possible functions.
136
+ Based on the question, you will need to make one or more function/tool calls to achieve the purpose.
137
+ If none of the functions can be used, point it out. If the given question lacks the parameters required by the function, also point it out. You should only return the function call in tools call sections.
138
 
139
+ If you decide to invoke any of the function(s), you MUST put it in the format of [func_name1(params_name1=params_value1, params_name2=params_value2...), func_name2(params)]
140
+ You SHOULD NOT include any other text in the response.
 
 
 
 
141
 
142
  Here is a list of functions in JSON format that you can invoke:
143
+
144
+ {json.dumps(tools_schema, indent=2)}
145
  """
146
  prompt = (
147
  f"System: {tool_selection_system}\n"
 
155
  temperature=0.2,
156
  top_p=0.95,
157
  )
158
+ return parse_tool_calls(raw), raw
159
 
160
  def call_tools(tool_calls, tool_registry):
161
  results = []
 
181
  if tools is None:
182
  tools = []
183
 
184
+ # 1. Build Schema & Registry
185
  tool_registry = {f.__name__: f for f in tools}
186
  tools_schema = [function_to_json(f) for f in tools]
187
 
188
+ # 2. Let the LLM select tools based on the message
189
+ tool_calls, initial_message = select_tools_with_llm(llm, message, tools_schema)
190
+
191
+ # We wanted to use the inital_message above in the response, for example
192
+ # when parameters are missing but the model was too bad at making such responses
193
+ # that it had to be omitted for now.
194
 
195
+ # 3. Call tools if needed, otherwise respond
196
+ if tool_calls and tools:
197
+ tool_results = call_tools(tool_calls, tool_registry)
198
+ tool_info_str = "\n<tool_results>\n"
199
  for tr in tool_results:
200
  tool_info_str += f"- {tr['name']}({tr['args']}) -> {tr['result']}\n"
201
+ final_system_message = f"{system_message}{LAST_SYSTEM_MESSAGE} {tool_info_str}</tool_results>\n"
202
  else:
203
  final_system_message = system_message
204
 
205
+ # Call the LLM again with the results from the tools used
206
  prompt = build_prompt(final_system_message, history, message)
207
+
208
  stream = llm(
209
  prompt,
210
  max_tokens=256,