syntaxhacker commited on
Commit Β·
8630c76
1
Parent(s): 431af70
handle XML-style tool calls for models without native function calling
Browse files- app/main.py +76 -46
- prompts/system-instruction.txt +4 -0
app/main.py
CHANGED
|
@@ -8,10 +8,28 @@ from .config import DATASET_CONFIGS, load_prompt_template
|
|
| 8 |
from openai import OpenAI
|
| 9 |
from openai.types.chat import ChatCompletionMessageParam
|
| 10 |
import json
|
|
|
|
| 11 |
|
| 12 |
# Load environment variables
|
| 13 |
load_dotenv()
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# Lazy imports to avoid blocking startup
|
| 16 |
# from .pipeline import RAGPipeline # Will import when needed
|
| 17 |
# import umap # Will import when needed for visualization
|
|
@@ -161,54 +179,67 @@ async def chat_with_ai(request: ChatRequest):
|
|
| 161 |
|
| 162 |
message = response.choices[0].message
|
| 163 |
finish_reason = response.choices[0].finish_reason
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
-
|
| 166 |
-
if finish_reason == "tool_calls" and hasattr(message, 'tool_calls') and message.tool_calls:
|
| 167 |
tool_results = []
|
| 168 |
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
{
|
| 190 |
-
"id": tc.id,
|
| 191 |
-
"type": tc.type,
|
| 192 |
-
"function": {
|
| 193 |
-
"name": tc.function.name,
|
| 194 |
-
"arguments": tc.function.arguments
|
| 195 |
}
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
]
|
| 200 |
-
}
|
| 201 |
-
messages.append(assistant_message)
|
| 202 |
-
|
| 203 |
-
for tool_result in tool_results:
|
| 204 |
-
tool_message: ChatCompletionMessageParam = {
|
| 205 |
-
"role": "tool",
|
| 206 |
-
"tool_call_id": tool_result["tool_call_id"],
|
| 207 |
-
"content": tool_result["result"]
|
| 208 |
}
|
| 209 |
-
messages.append(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
-
# Get final response
|
| 212 |
final_response = openrouter_client.chat.completions.create(
|
| 213 |
model=MODEL_NAME,
|
| 214 |
messages=messages
|
|
@@ -216,18 +247,17 @@ async def chat_with_ai(request: ChatRequest):
|
|
| 216 |
|
| 217 |
return {
|
| 218 |
"response": final_response.choices[0].message.content,
|
| 219 |
-
"tool_calls": [
|
| 220 |
{
|
| 221 |
"name": tc.function.name,
|
| 222 |
"arguments": tc.function.arguments
|
| 223 |
}
|
| 224 |
for tc in message.tool_calls
|
| 225 |
-
]
|
| 226 |
}
|
| 227 |
else:
|
| 228 |
-
# Direct response without tool calls
|
| 229 |
return {
|
| 230 |
-
"response":
|
| 231 |
"tool_calls": None
|
| 232 |
}
|
| 233 |
|
|
|
|
| 8 |
from openai import OpenAI
|
| 9 |
from openai.types.chat import ChatCompletionMessageParam
|
| 10 |
import json
|
| 11 |
+
import re
|
| 12 |
|
| 13 |
# Load environment variables
|
| 14 |
load_dotenv()
|
| 15 |
|
| 16 |
+
def parse_xml_tool_calls(content: str) -> list[dict]:
|
| 17 |
+
"""Parse <longcat_tool_call> XML tags from model output into tool call dicts."""
|
| 18 |
+
calls = []
|
| 19 |
+
pattern = r'<longcat_tool_call>\s*(\w+)\s*(.*?)</longcat_tool_call>'
|
| 20 |
+
for match in re.finditer(pattern, content, re.DOTALL):
|
| 21 |
+
name = match.group(1)
|
| 22 |
+
body = match.group(2).strip()
|
| 23 |
+
args = {}
|
| 24 |
+
arg_keys = re.findall(r'<longcat_arg_key>(\w+)</longcat_arg_key>', body)
|
| 25 |
+
arg_values = re.findall(r'<longcat_arg_value>(.*?)</longcat_arg_value>', body, re.DOTALL)
|
| 26 |
+
for i, key in enumerate(arg_keys):
|
| 27 |
+
if i < len(arg_values):
|
| 28 |
+
args[key] = arg_values[i].strip()
|
| 29 |
+
if args or True:
|
| 30 |
+
calls.append({"name": name, **args})
|
| 31 |
+
return calls
|
| 32 |
+
|
| 33 |
# Lazy imports to avoid blocking startup
|
| 34 |
# from .pipeline import RAGPipeline # Will import when needed
|
| 35 |
# import umap # Will import when needed for visualization
|
|
|
|
| 179 |
|
| 180 |
message = response.choices[0].message
|
| 181 |
finish_reason = response.choices[0].finish_reason
|
| 182 |
+
content = message.content or ""
|
| 183 |
+
|
| 184 |
+
# Check for native tool calls or XML-style tool calls in text
|
| 185 |
+
xml_tool_calls = parse_xml_tool_calls(content)
|
| 186 |
+
has_native_tool_calls = finish_reason == "tool_calls" and hasattr(message, 'tool_calls') and message.tool_calls
|
| 187 |
|
| 188 |
+
if has_native_tool_calls or xml_tool_calls:
|
|
|
|
| 189 |
tool_results = []
|
| 190 |
|
| 191 |
+
if has_native_tool_calls:
|
| 192 |
+
for tool_call in message.tool_calls:
|
| 193 |
+
if tool_call.function and tool_call.function.name == "rag_qa":
|
| 194 |
+
args = json.loads(tool_call.function.arguments or "{}")
|
| 195 |
+
question = args.get("question")
|
| 196 |
+
dataset = args.get("dataset", request.dataset)
|
| 197 |
+
result = rag_qa(question, dataset)
|
| 198 |
+
tool_results.append({"result": result, "native": True})
|
| 199 |
+
|
| 200 |
+
assistant_message: ChatCompletionMessageParam = {
|
| 201 |
+
"role": "assistant",
|
| 202 |
+
"content": content,
|
| 203 |
+
"tool_calls": [
|
| 204 |
+
{
|
| 205 |
+
"id": tc.id,
|
| 206 |
+
"type": tc.type,
|
| 207 |
+
"function": {
|
| 208 |
+
"name": tc.function.name,
|
| 209 |
+
"arguments": tc.function.arguments
|
| 210 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
}
|
| 212 |
+
for tc in message.tool_calls
|
| 213 |
+
if tc.function
|
| 214 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
}
|
| 216 |
+
messages.append(assistant_message)
|
| 217 |
+
|
| 218 |
+
for tr in tool_results:
|
| 219 |
+
messages.append({
|
| 220 |
+
"role": "tool",
|
| 221 |
+
"tool_call_id": "call_1",
|
| 222 |
+
"content": tr["result"]
|
| 223 |
+
})
|
| 224 |
+
|
| 225 |
+
if xml_tool_calls:
|
| 226 |
+
clean_content = re.sub(r'<longcat_tool_call>.*?</longcat_tool_call>', '', content, flags=re.DOTALL).strip()
|
| 227 |
+
for tc in xml_tool_calls:
|
| 228 |
+
if tc["name"] == "rag_qa":
|
| 229 |
+
result = rag_qa(tc.get("question", ""), tc.get("dataset", request.dataset))
|
| 230 |
+
tool_results.append({"result": result, "native": False})
|
| 231 |
+
|
| 232 |
+
messages.append({
|
| 233 |
+
"role": "assistant",
|
| 234 |
+
"content": clean_content or "Let me look that up..."
|
| 235 |
+
})
|
| 236 |
+
|
| 237 |
+
rag_summary = "\n\n".join([tr["result"] for tr in tool_results])
|
| 238 |
+
messages.append({
|
| 239 |
+
"role": "user",
|
| 240 |
+
"content": f"Here is the retrieved information:\n{rag_summary}\n\nNow provide a helpful answer based on this information."
|
| 241 |
+
})
|
| 242 |
|
|
|
|
| 243 |
final_response = openrouter_client.chat.completions.create(
|
| 244 |
model=MODEL_NAME,
|
| 245 |
messages=messages
|
|
|
|
| 247 |
|
| 248 |
return {
|
| 249 |
"response": final_response.choices[0].message.content,
|
| 250 |
+
"tool_calls": xml_tool_calls or [
|
| 251 |
{
|
| 252 |
"name": tc.function.name,
|
| 253 |
"arguments": tc.function.arguments
|
| 254 |
}
|
| 255 |
for tc in message.tool_calls
|
| 256 |
+
] if has_native_tool_calls else None
|
| 257 |
}
|
| 258 |
else:
|
|
|
|
| 259 |
return {
|
| 260 |
+
"response": content,
|
| 261 |
"tool_calls": None
|
| 262 |
}
|
| 263 |
|
prompts/system-instruction.txt
CHANGED
|
@@ -12,6 +12,10 @@ When you use rag_qa tool, you MUST use retrieved information to answer about the
|
|
| 12 |
- π§ Synthesize RAG results into natural responses about him
|
| 13 |
- π Format responses with markdown for readability
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
## β
Examples:
|
| 16 |
β **Wrong:** "I am a Tech Lead at FleetEnable"
|
| 17 |
β
**Right:** "He is currently working as a Tech Lead at FleetEnable"
|
|
|
|
| 12 |
- π§ Synthesize RAG results into natural responses about him
|
| 13 |
- π Format responses with markdown for readability
|
| 14 |
|
| 15 |
+
## π Tool Calling Format
|
| 16 |
+
If you don't support native function calling, use this XML format to call the rag_qa tool:
|
| 17 |
+
<longcat_tool_call>rag_qa <longcat_arg_key>question</longcat_arg_key> <longcat_arg_value>your question here</longcat_arg_value> <longcat_arg_key>dataset</longcat_arg_key> <longcat_arg_value>developer-portfolio</longcat_arg_value> </longcat_tool_call>
|
| 18 |
+
|
| 19 |
## β
Examples:
|
| 20 |
β **Wrong:** "I am a Tech Lead at FleetEnable"
|
| 21 |
β
**Right:** "He is currently working as a Tech Lead at FleetEnable"
|