Skip to content

Commit 7e427a1

Browse files
committed
cookbook: add tool search & programmatic tool calling demo
- this cookbook introduced a simple demo that: - search tool with embeddings - progressively loading tool info - single tool calling - programmatic tool calling - scalable framework to add rich tool metadata and massive tools Signed-off-by: Tiequan <tieqluo@qti.qualcomm.com>
1 parent 0eca4d4 commit 7e427a1

4 files changed

Lines changed: 748 additions & 0 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# PocketFlow Advanced Tool Calling Demo
2+
3+
This project shows how to build an agent that performs 2 advanced methods of tool calling (brought by Claude).
4+
5+
1st is to encode tool metadata that could be search with embedding, instead of loading all tool info into context. **Progressively disclose** most matching tool info into context window, hence tool info could be more rich like adding best examples.
6+
7+
2nd is (quote) "**Programmatic Tool Calling (PTC)** enables Claude to orchestrate tools **through code** rather than **through individual API round-trips**. Instead of Claude requesting tools one at a time with each result being returned to its context, Claude writes code that calls multiple tools, processes their outputs, and controls what information actually enters its context window."
8+
9+
This implementation is based on :
10+
11+
- the article: [Introducing advanced tool use on the Claude Developer Platform](https://www.anthropic.com/engineering/advanced-tool-use)
12+
13+
- the cookbook: [Programmatic Tool Calling (PTC)](https://github.com/anthropics/claude-cookbooks/blob/main/tool_use/programmatic_tool_calling_ptc.ipynb)
14+
15+
- the cookbook: [Tool Search With Embeddings](https://github.com/anthropics/claude-cookbooks/blob/main/tool_use/tool_search_with_embeddings.ipynb)
16+
17+
## Features
18+
19+
- Search relatively a big number of tools based on embedding
20+
- Match with similairy score (easy to scale), progressively loading tool info into context
21+
- Support both single tool calling based on PocketFlow
22+
- Support multiple tool calling (by generated code using provided tools)
23+
24+
## How to Run
25+
26+
1. Set your API key:
27+
```bash
28+
export OPENAI_API_KEY="your-api-key-here"
29+
```
30+
Or update it directly in `utils.py`
31+
32+
2. Install and run:
33+
```bash
34+
pip install -r requirements.txt
35+
python main.py
36+
```
37+
38+
## An Easier Way for Multiple Tool Calling and Save Lots of Tokens
39+
40+
### Typical Tool Calling Flow
41+
42+
- User input a complex question (like a sequence of compound SQL statements) that needed rounds of tool calling (depending on how smart of the model either).
43+
44+
- **Model Loaded all tool info into context**
45+
46+
- Model run the **1st call and get the 1st result** .
47+
48+
- Pass result into next round of prompt .
49+
50+
- Start the **2nd round , and then 3rd , 4th**....
51+
52+
- According Claude's article , this will cause 10x more token usage
53+
54+
### New Method
55+
56+
- User input a complex question that needed rounds of tool calling.
57+
58+
- Model search most-relevant tools and get tool name , params, input schema into context .
59+
60+
- Model knew the tool could be called with python scripting , so generate python code to run .
61+
62+
- Code finished running in sandbox and return results back to Model .
63+
64+
- Then Model will process all the results in next round .
65+
66+
67+
## How It Works
68+
69+
```mermaid
70+
flowchart LR
71+
reason[ReasonNode] -->|search| tools[ToolsNode]
72+
tools[ToolsNode] -->|reason| reason[ReasonNode]
73+
reason -->|execute| execute[ExecuteToolNode]
74+
reason -->|execute_coding| execute[ExecuteToolNode]
75+
execute -->|reason| reason
76+
reason -->|answer| answer[AnswerQuestion]
77+
```
78+
79+
reason_node - "tool_search" >> tools_node
80+
tools_node - "reason" >> reason_node
81+
reason_node - "tool_execute" >> exec_node
82+
reason_node - "tool_execute_coding" >> exec_node
83+
exec_node - "reason" >> reason_node
84+
reason_node - "answer" >> answer_node
85+
86+
The agent uses PocketFlow to create a workflow where:
87+
1. ReasonNode takes user input about Stock Tickers
88+
2. ReasonNode choose what string to search with Tools (embeddings)
89+
3. ToolsNode returned search results (similarity score, name, parameters)
90+
4. ReasonNode choose to make single tool calling , or generate python code for multiple rounds of tool calling in one shot (which saved A LOT of tokens!)
91+
5. AnswerNode craft final answers based on ReasonNode's context
92+
93+
## Files
94+
95+
- [`main.py`](./main.py): Implementation of nodes and flow assembling
96+
- [`utils.py`](./utils.py): Helper functions tool calling and tool coding running (minimal unsafe way of using subprocess, only for demo purpose)
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
from pocketflow import Node, Flow
2+
from utils import TOOL_LIBRARY, TOOL_SEARCH_DEFINITION, handle_tool_search, call_llm, create_all_tools_embedding, run_user_code
3+
import yaml
4+
import sys
5+
from datetime import datetime
6+
7+
class ToolsNode(Node):
8+
def prep(self, shared):
9+
"""Initialize and get tools"""
10+
# The question is now passed from main via shared
11+
tool_embeddings = create_all_tools_embedding()
12+
return shared["query"], shared["top_k"], tool_embeddings
13+
14+
def exec(self, inputs):
15+
"""Retrieve tools from the MCP server"""
16+
tool_query, top_k, tool_embeddings = inputs
17+
res = handle_tool_search(tool_query, top_k, tool_embeddings)
18+
return res
19+
20+
def post(self, shared, prep_res, exec_res):
21+
"""Store tools and process to decision node"""
22+
tools_search_result = exec_res
23+
shared["tools_search_result"] = tools_search_result
24+
return "reason"
25+
26+
class ReasonNode(Node):
27+
def prep(self, shared):
28+
"""Prepare the prompt for LLM to process the question"""
29+
question = shared["question"]
30+
tools_search_result = shared.get("tools_search_result","no tool list")
31+
tools_exec_result = shared.get("tools_exec_result","no tool execute result")
32+
date = datetime.now().strftime("%Y-%m-%d")
33+
34+
# Now is the time to combine with PocketFlow
35+
# refer from #77 of https://github.com/anthropics/claude-cookbooks/blob/main/tool_use/tool_search_with_embeddings.ipynb
36+
prompt = f"""
37+
#### PROMPT START
38+
39+
#### CONTEXT
40+
You are a reasoning assistant.
41+
CURRENT DATE:{date}
42+
Question: {question}
43+
Tool Recall List: {tools_search_result}
44+
Tool Execute Result : {tools_exec_result}
45+
46+
#### ACTION SPACE
47+
[1] tool_search
48+
{TOOL_SEARCH_DEFINITION}
49+
50+
[2] tool_execute
51+
Based on Tool Recall List and Question, use tool with proper parameters to answer Question.
52+
53+
[3] tool_execute_coding
54+
When single tool calling is not enough to answer the Question, follow provided tool input schema to generate python code.
55+
All your generated python code will be executed within single python file.
56+
ONLY ALLOW to call provided tool, tool calling result will be provided with Tool Execute Result of next round.
57+
58+
[4] answer
59+
Answer Question with proper reason
60+
61+
#### NEXT ACTION
62+
Decide the next action based on the context and available actions.
63+
Return your response in this format:
64+
65+
```yaml
66+
thinking: |
67+
<your step-by-step reasoning process>
68+
action: tool_search or tool_execute or tool_execute_coding or answer
69+
reason: <why you chose this action>
70+
query: <your_query_string if action is tool_search>
71+
top_k: <your_top_k if action is tool_search>
72+
tool_name: <your chosen tool name if action is tool_execute>
73+
tool_param: <your chosen tool parameters if action is tool_execute>
74+
code_block: <your generated tool calling python code if action is tool_execute_coding>
75+
conclusion: <your answer content if action is answer>
76+
```
77+
IMPORTANT: Make sure to:
78+
1. Use proper indentation (4 spaces) for all multi-line fields
79+
2. Use the | character for multi-line text fields
80+
3. Keep single-line fields without the | character
81+
82+
#### END OF PROMPT
83+
"""
84+
return prompt
85+
86+
def exec(self, prompt):
87+
"""Call LLM to process the question and decide which tool to use"""
88+
print("🤔 Analyzing question and deciding which tool to use...")
89+
response = call_llm(prompt)
90+
return response
91+
92+
def post(self, shared, prep_res, exec_res):
93+
"""Extract decision from YAML and save to shared context"""
94+
try:
95+
yaml_str = exec_res.split("```yaml")[1].split("```")[0].strip()
96+
decision = yaml.safe_load(yaml_str)
97+
shared["action"] = decision["action"]
98+
except Exception as e:
99+
print(f"❌ Error parsing LLM response: {e}")
100+
print("Raw response:", exec_res)
101+
exit(1)
102+
if decision["action"] == "tool_search":
103+
shared["reason"] = decision["reason"]
104+
shared["query"] = decision["query"]
105+
shared["top_k"] = decision["top_k"]
106+
print(f"💡 Reason Node Decide to query tool with string: {decision['query']}")
107+
return "tool_search"
108+
elif decision["action"] == "tool_execute":
109+
shared["tool_name"] = decision["tool_name"]
110+
shared["tool_param"] = decision["tool_param"]
111+
print(f"💡 Reason Node Decide to use tool. \n 💡NAME: {decision['tool_name']} , PARAMS : {decision['tool_param']}")
112+
exit(1)
113+
return "tool_execute"
114+
elif decision["action"] == "tool_execute_coding":
115+
shared["code_block"] = decision["code_block"]
116+
print(f"💡 Reason Node Decide to use tool coding \n 💡CODE : {decision['code_block']}")
117+
return "tool_execute_coding"
118+
elif decision["action"] == "answer":
119+
print(" 🟢 Reason Node Decide to answer ")
120+
shared["context"] = "Latest Reasonning : \n" + decision["thinking"] + "\n Tool Calling Result : \n" + decision["conclusion"]
121+
return "answer"
122+
else :
123+
print("Action NOT DEFINED !! ")
124+
exit(1)
125+
126+
class ExecuteToolNode(Node):
127+
def prep(self, shared):
128+
"""Prepare tool execution parameters"""
129+
if shared["action"] == "tool_execute" :
130+
return shared["action"], shared["tool_name"], shared["parameters"]
131+
elif shared["action"] == "tool_execute_coding" :
132+
print(" exec node : coding ")
133+
return shared["action"], shared["code_block"]
134+
else:
135+
print(" WRONG INPUT FOR EXEC NODE ")
136+
exit(1)
137+
138+
def exec(self, inputs):
139+
"""Execute the chosen tool"""
140+
if inputs[0] == "tool_execute" :
141+
intent, tool_name, parameters = inputs
142+
exit()
143+
elif inputs[0] == "tool_execute_coding" :
144+
intent, code_block = inputs
145+
result = run_user_code(code_block)
146+
else :
147+
print("❌ no tool chosen or code generated")
148+
exit(1)
149+
150+
return result
151+
152+
def post(self, shared, prep_res, exec_res):
153+
print(f"\n✅ Tool Execution Result is : {exec_res}")
154+
shared["tools_exec_result"] = exec_res
155+
return "reason"
156+
157+
158+
class AnswerQuestion(Node):
159+
def prep(self, shared):
160+
"""Get the question and context for answering."""
161+
return shared["question"], shared.get("context", "")
162+
163+
def exec(self, inputs):
164+
"""Call the LLM to generate a final answer."""
165+
question, context = inputs
166+
167+
print(f"✍️ Crafting final answer...")
168+
169+
# Create a prompt for the LLM to answer the question
170+
prompt = f"""
171+
### CONTEXT
172+
Based on the following information, answer the question.
173+
Question: {question}
174+
Conclusion: {context}
175+
176+
## YOUR ANSWER:
177+
Provide a comprehensive answer using the research results.
178+
"""
179+
# Call the LLM to generate an answer
180+
answer = call_llm(prompt)
181+
return answer
182+
183+
def post(self, shared, prep_res, exec_res):
184+
"""Save the final answer and complete the flow."""
185+
# Save the answer in the shared store
186+
shared["answer"] = exec_res
187+
188+
print(f"✅ Answer generated successfully")
189+
190+
# We're done - no need to continue the flow
191+
return "done"
192+
193+
if __name__ == "__main__":
194+
# Default question
195+
default_question = "I want to know the latest price of NVDA/MSFT/QCOM"
196+
197+
# Get question from command line if provided with --
198+
question = default_question
199+
for arg in sys.argv[1:]:
200+
if arg.startswith("--"):
201+
question = arg[2:]
202+
break
203+
204+
print(f"🤔 Processing question: {question}")
205+
206+
# Create nodes
207+
reason_node = ReasonNode()
208+
tools_node = ToolsNode()
209+
exec_node = ExecuteToolNode()
210+
answer_node = AnswerQuestion()
211+
212+
# Connect nodes
213+
reason_node - "tool_search" >> tools_node
214+
tools_node - "reason" >> reason_node
215+
reason_node - "tool_execute" >> exec_node
216+
reason_node - "tool_execute_coding" >> exec_node
217+
exec_node - "reason" >> reason_node
218+
reason_node - "answer" >> answer_node
219+
220+
# Create and run flow
221+
flow = Flow(start=reason_node)
222+
shared = {"question": question}
223+
flow.run(shared)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pocketflow>=0.0.1
2+
openai>=1.0.0
3+
fastmcp
4+
pyyaml
5+
numpy

0 commit comments

Comments
 (0)