Spaces:
Runtime error
Runtime error
Joar Paganus commited on
Commit ·
89030a0
1
Parent(s): 9f120d8
update agent.py
Browse files
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 |
-
#
|
| 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 |
-
# 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 |
-
#
|
| 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 |
-
|
| 343 |
-
|
| 344 |
|
| 345 |
-
|
| 346 |
-
|
| 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 |
-
|
|
|
|
| 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
|
| 394 |
tool_registry = {f.__name__: f for f in tools}
|
| 395 |
tools_schema = [function_to_json(f) for f in tools]
|
| 396 |
|
| 397 |
-
# 2.
|
| 398 |
-
tool_calls = select_tools_with_llm(llm, message, tools_schema)
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
| 400 |
|
| 401 |
-
if
|
| 402 |
-
|
|
|
|
|
|
|
| 403 |
for tr in tool_results:
|
| 404 |
tool_info_str += f"- {tr['name']}({tr['args']}) -> {tr['result']}\n"
|
| 405 |
-
final_system_message = system_message
|
| 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,
|