Repository: The-Pocket/PocketFlow
Branch: main
Commit: 0271a7ea6d38
Files: 410
Total size: 1.7 MB
Directory structure:
gitextract_1kfwxukd/
├── .cursor/
│ └── rules/
│ ├── core_abstraction/
│ │ ├── async.mdc
│ │ ├── batch.mdc
│ │ ├── communication.mdc
│ │ ├── flow.mdc
│ │ ├── node.mdc
│ │ └── parallel.mdc
│ ├── design_pattern/
│ │ ├── agent.mdc
│ │ ├── mapreduce.mdc
│ │ ├── multi_agent.mdc
│ │ ├── rag.mdc
│ │ ├── structure.mdc
│ │ └── workflow.mdc
│ ├── guide_for_pocketflow.mdc
│ └── utility_function/
│ ├── chunking.mdc
│ ├── embedding.mdc
│ ├── llm.mdc
│ ├── text_to_speech.mdc
│ ├── vector.mdc
│ ├── viz.mdc
│ └── websearch.mdc
├── .cursorrules
├── .gitignore
├── LICENSE
├── README.md
├── cookbook/
│ ├── README.md
│ ├── data/
│ │ └── PaulGrahamEssaysLarge/
│ │ ├── addiction.txt
│ │ ├── aord.txt
│ │ ├── apple.txt
│ │ ├── avg.txt
│ │ └── before.txt
│ ├── pocketflow-a2a/
│ │ ├── README.md
│ │ ├── a2a_client.py
│ │ ├── a2a_server.py
│ │ ├── common/
│ │ │ ├── __init__.py
│ │ │ ├── client/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── card_resolver.py
│ │ │ │ └── client.py
│ │ │ ├── server/
│ │ │ │ ├── __init__.py
│ │ │ │ ├── server.py
│ │ │ │ ├── task_manager.py
│ │ │ │ └── utils.py
│ │ │ ├── types.py
│ │ │ └── utils/
│ │ │ ├── in_memory_cache.py
│ │ │ └── push_notification_auth.py
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ ├── task_manager.py
│ │ └── utils.py
│ ├── pocketflow-agent/
│ │ ├── README.md
│ │ ├── demo.ipynb
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils.py
│ ├── pocketflow-agent-skills/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ ├── skills/
│ │ │ ├── checklist_writer.md
│ │ │ └── executive_brief.md
│ │ └── utils.py
│ ├── pocketflow-async-basic/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils.py
│ ├── pocketflow-batch/
│ │ ├── README.md
│ │ ├── main.py
│ │ ├── requirements.txt
│ │ ├── translations/
│ │ │ ├── README_CHINESE.md
│ │ │ ├── README_FRENCH.md
│ │ │ ├── README_GERMAN.md
│ │ │ ├── README_JAPANESE.md
│ │ │ ├── README_KOREAN.md
│ │ │ ├── README_PORTUGUESE.md
│ │ │ ├── README_RUSSIAN.md
│ │ │ └── README_SPANISH.md
│ │ └── utils.py
│ ├── pocketflow-batch-flow/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ └── requirements.txt
│ ├── pocketflow-batch-node/
│ │ ├── README.md
│ │ ├── data/
│ │ │ └── sales.csv
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ └── requirements.txt
│ ├── pocketflow-chat/
│ │ ├── README.md
│ │ ├── main.py
│ │ ├── requirements.txt
│ │ └── utils.py
│ ├── pocketflow-chat-guardrail/
│ │ ├── README.md
│ │ ├── main.py
│ │ ├── requirements.txt
│ │ └── utils.py
│ ├── pocketflow-chat-memory/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils/
│ │ ├── __init__.py
│ │ ├── call_llm.py
│ │ ├── get_embedding.py
│ │ └── vector_index.py
│ ├── pocketflow-cli-hitl/
│ │ ├── README.md
│ │ ├── docs/
│ │ │ └── design.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── call_llm.py
│ ├── pocketflow-code-generator/
│ │ ├── README.md
│ │ ├── doc/
│ │ │ └── design.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils/
│ │ ├── __init__.py
│ │ ├── call_llm.py
│ │ └── code_executor.py
│ ├── pocketflow-communication/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ └── requirements.txt
│ ├── pocketflow-fastapi-background/
│ │ ├── README.md
│ │ ├── docs/
│ │ │ └── design.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ ├── static/
│ │ │ ├── index.html
│ │ │ └── progress.html
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── call_llm.py
│ ├── pocketflow-fastapi-hitl/
│ │ ├── README.md
│ │ ├── docs/
│ │ │ └── design.md
│ │ ├── flow.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ ├── server.py
│ │ ├── static/
│ │ │ └── style.css
│ │ ├── templates/
│ │ │ └── index.html
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── process_task.py
│ ├── pocketflow-fastapi-websocket/
│ │ ├── README.md
│ │ ├── docs/
│ │ │ └── design.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ ├── static/
│ │ │ └── index.html
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── stream_llm.py
│ ├── pocketflow-flow/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ └── requirements.txt
│ ├── pocketflow-google-calendar/
│ │ ├── .gitignore
│ │ ├── Pipfile
│ │ ├── README.md
│ │ ├── main.py
│ │ ├── nodes.py
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── google_calendar.py
│ ├── pocketflow-gradio-hitl/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils/
│ │ ├── call_llm.py
│ │ ├── call_mock_api.py
│ │ ├── conversation.py
│ │ └── format_chat_history.py
│ ├── pocketflow-hello-world/
│ │ ├── README.md
│ │ ├── docs/
│ │ │ └── design.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── call_llm.py
│ ├── pocketflow-llm-streaming/
│ │ ├── README.md
│ │ ├── main.py
│ │ ├── requirements.txt
│ │ └── utils.py
│ ├── pocketflow-majority-vote/
│ │ ├── README.md
│ │ ├── main.py
│ │ ├── requirements.txt
│ │ └── utils.py
│ ├── pocketflow-map-reduce/
│ │ ├── README.md
│ │ ├── data/
│ │ │ ├── resume1.txt
│ │ │ ├── resume2.txt
│ │ │ ├── resume3.txt
│ │ │ ├── resume4.txt
│ │ │ └── resume5.txt
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils.py
│ ├── pocketflow-mcp/
│ │ ├── README.md
│ │ ├── main.py
│ │ ├── requirements.txt
│ │ ├── simple_server.py
│ │ └── utils.py
│ ├── pocketflow-multi-agent/
│ │ ├── README.md
│ │ ├── main.py
│ │ ├── requirements.txt
│ │ └── utils.py
│ ├── pocketflow-nested-batch/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── school/
│ │ ├── class_a/
│ │ │ ├── student1.txt
│ │ │ └── student2.txt
│ │ └── class_b/
│ │ ├── student3.txt
│ │ └── student4.txt
│ ├── pocketflow-node/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── requirements.txt
│ │ └── utils/
│ │ └── call_llm.py
│ ├── pocketflow-parallel-batch/
│ │ ├── README.md
│ │ ├── main.py
│ │ ├── requirements.txt
│ │ ├── translations/
│ │ │ ├── README_CHINESE.md
│ │ │ ├── README_FRENCH.md
│ │ │ ├── README_GERMAN.md
│ │ │ ├── README_JAPANESE.md
│ │ │ ├── README_KOREAN.md
│ │ │ ├── README_PORTUGUESE.md
│ │ │ ├── README_RUSSIAN.md
│ │ │ └── README_SPANISH.md
│ │ └── utils.py
│ ├── pocketflow-parallel-batch-flow/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ └── requirements.txt
│ ├── pocketflow-rag/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils.py
│ ├── pocketflow-streamlit-fsm/
│ │ ├── README.md
│ │ ├── app.py
│ │ ├── docs/
│ │ │ └── design.md
│ │ ├── flow.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── generate_image.py
│ ├── pocketflow-structured-output/
│ │ ├── README.md
│ │ ├── data.txt
│ │ ├── main.py
│ │ ├── requirements.txt
│ │ └── utils.py
│ ├── pocketflow-supervisor/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils.py
│ ├── pocketflow-tao/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ └── utils.py
│ ├── pocketflow-text2sql/
│ │ ├── README.md
│ │ ├── docs/
│ │ │ └── design.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── populate_db.py
│ │ ├── requirements.txt
│ │ └── utils/
│ │ └── call_llm.py
│ ├── pocketflow-thinking/
│ │ ├── README.md
│ │ ├── design.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils.py
│ ├── pocketflow-tool-crawler/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ ├── tools/
│ │ │ ├── crawler.py
│ │ │ └── parser.py
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── call_llm.py
│ ├── pocketflow-tool-database/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ ├── tools/
│ │ │ └── database.py
│ │ └── utils/
│ │ └── __init__.py
│ ├── pocketflow-tool-embeddings/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ ├── tools/
│ │ │ └── embeddings.py
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── call_llm.py
│ ├── pocketflow-tool-pdf-vision/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ ├── tools/
│ │ │ ├── pdf.py
│ │ │ └── vision.py
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── call_llm.py
│ ├── pocketflow-tool-search/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ ├── tools/
│ │ │ ├── parser.py
│ │ │ └── search.py
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── call_llm.py
│ ├── pocketflow-tracing/
│ │ ├── README.md
│ │ ├── examples/
│ │ │ ├── async_example.py
│ │ │ └── basic_example.py
│ │ ├── requirements.txt
│ │ ├── setup.py
│ │ ├── test_tracing.py
│ │ ├── tracing/
│ │ │ ├── __init__.py
│ │ │ ├── config.py
│ │ │ ├── core.py
│ │ │ └── decorator.py
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── setup.py
│ ├── pocketflow-visualization/
│ │ ├── README.md
│ │ ├── async_flow.py
│ │ ├── async_loop_flow.py
│ │ ├── visualize.py
│ │ └── viz/
│ │ ├── flow_visualization.html
│ │ └── flow_visualization.json
│ ├── pocketflow-voice-chat/
│ │ ├── README.md
│ │ ├── docs/
│ │ │ └── design.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils/
│ │ ├── __init__.py
│ │ ├── audio_utils.py
│ │ ├── call_llm.py
│ │ ├── speech_to_text.py
│ │ └── text_to_speech.py
│ ├── pocketflow-workflow/
│ │ ├── README.md
│ │ ├── flow.py
│ │ ├── main.py
│ │ ├── nodes.py
│ │ ├── requirements.txt
│ │ └── utils/
│ │ └── call_llm.py
│ └── pocketflow_demo.ipynb
├── docs/
│ ├── _config.yml
│ ├── _includes/
│ │ └── footer_custom.html
│ ├── core_abstraction/
│ │ ├── async.md
│ │ ├── batch.md
│ │ ├── communication.md
│ │ ├── flow.md
│ │ ├── index.md
│ │ ├── node.md
│ │ └── parallel.md
│ ├── design_pattern/
│ │ ├── agent.md
│ │ ├── index.md
│ │ ├── mapreduce.md
│ │ ├── multi_agent.md
│ │ ├── rag.md
│ │ ├── structure.md
│ │ └── workflow.md
│ ├── guide.md
│ ├── index.md
│ └── utility_function/
│ ├── chunking.md
│ ├── embedding.md
│ ├── index.md
│ ├── llm.md
│ ├── text_to_speech.md
│ ├── vector.md
│ ├── viz.md
│ └── websearch.md
├── pocketflow/
│ ├── __init__.py
│ └── __init__.pyi
├── setup.py
├── tests/
│ ├── test_async_batch_flow.py
│ ├── test_async_batch_node.py
│ ├── test_async_flow.py
│ ├── test_async_parallel_batch_flow.py
│ ├── test_async_parallel_batch_node.py
│ ├── test_batch_flow.py
│ ├── test_batch_node.py
│ ├── test_fall_back.py
│ ├── test_flow_basic.py
│ └── test_flow_composition.py
└── utils/
└── update_pocketflow_mdc.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .cursor/rules/core_abstraction/async.mdc
================================================
---
description: Guidelines for using PocketFlow, Core Abstraction, (Advanced) Async
globs:
alwaysApply: false
---
# (Advanced) Async
**Async** Nodes implement `prep_async()`, `exec_async()`, `exec_fallback_async()`, and/or `post_async()`. This is useful for:
1. **prep_async()**: For *fetching/reading data (files, APIs, DB)* in an I/O-friendly way.
2. **exec_async()**: Typically used for async LLM calls.
3. **post_async()**: For *awaiting user feedback*, *coordinating across multi-agents* or any additional async steps after `exec_async()`.
**Note**: `AsyncNode` must be wrapped in `AsyncFlow`. `AsyncFlow` can also include regular (sync) nodes.
### Example
```python
class SummarizeThenVerify(AsyncNode):
async def prep_async(self, shared):
# Example: read a file asynchronously
doc_text = await read_file_async(shared["doc_path"])
return doc_text
async def exec_async(self, prep_res):
# Example: async LLM call
summary = await call_llm_async(f"Summarize: {prep_res}")
return summary
async def post_async(self, shared, prep_res, exec_res):
# Example: wait for user feedback
decision = await gather_user_feedback(exec_res)
if decision == "approve":
shared["summary"] = exec_res
return "approve"
return "deny"
summarize_node = SummarizeThenVerify()
final_node = Finalize()
# Define transitions
summarize_node - "approve" >> final_node
summarize_node - "deny" >> summarize_node # retry
flow = AsyncFlow(start=summarize_node)
async def main():
shared = {"doc_path": "document.txt"}
await flow.run_async(shared)
print("Final Summary:", shared.get("summary"))
asyncio.run(main())
```
================================================
FILE: .cursor/rules/core_abstraction/batch.mdc
================================================
---
description: Guidelines for using PocketFlow, Core Abstraction, Batch
globs:
alwaysApply: false
---
# Batch
**Batch** makes it easier to handle large inputs in one Node or **rerun** a Flow multiple times. Example use cases:
- **Chunk-based** processing (e.g., splitting large texts).
- **Iterative** processing over lists of input items (e.g., user queries, files, URLs).
## 1. BatchNode
A **BatchNode** extends `Node` but changes `prep()` and `exec()`:
- **`prep(shared)`**: returns an **iterable** (e.g., list, generator).
- **`exec(item)`**: called **once** per item in that iterable.
- **`post(shared, prep_res, exec_res_list)`**: after all items are processed, receives a **list** of results (`exec_res_list`) and returns an **Action**.
### Example: Summarize a Large File
```python
class MapSummaries(BatchNode):
def prep(self, shared):
# Suppose we have a big file; chunk it
content = shared["data"]
chunk_size = 10000
chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
return chunks
def exec(self, chunk):
prompt = f"Summarize this chunk in 10 words: {chunk}"
summary = call_llm(prompt)
return summary
def post(self, shared, prep_res, exec_res_list):
combined = "\n".join(exec_res_list)
shared["summary"] = combined
return "default"
map_summaries = MapSummaries()
flow = Flow(start=map_summaries)
flow.run(shared)
```
---
## 2. BatchFlow
A **BatchFlow** runs a **Flow** multiple times, each time with different `params`. Think of it as a loop that replays the Flow for each parameter set.
### Key Differences from BatchNode
**Important**: Unlike BatchNode, which processes items and modifies the shared store:
1. BatchFlow returns **parameters to pass to the child Flow**, not data to process
2. These parameters are accessed in child nodes via `self.params`, not from the shared store
3. Each child Flow runs independently with a different set of parameters
4. Child nodes can be regular Nodes, not BatchNodes (the batching happens at the Flow level)
### Example: Summarize Many Files
```python
class SummarizeAllFiles(BatchFlow):
def prep(self, shared):
# IMPORTANT: Return a list of param dictionaries (not data for processing)
filenames = list(shared["data"].keys()) # e.g., ["file1.txt", "file2.txt", ...]
return [{"filename": fn} for fn in filenames]
# Child node that accesses filename from params, not shared store
class LoadFile(Node):
def prep(self, shared):
# Access filename from params (not from shared)
filename = self.params["filename"] # Important! Use self.params, not shared
return filename
def exec(self, filename):
with open(filename, 'r') as f:
return f.read()
def post(self, shared, prep_res, exec_res):
# Store file content in shared
shared["current_file_content"] = exec_res
return "default"
# Summarize node that works on the currently loaded file
class Summarize(Node):
def prep(self, shared):
return shared["current_file_content"]
def exec(self, content):
prompt = f"Summarize this file in 50 words: {content}"
return call_llm(prompt)
def post(self, shared, prep_res, exec_res):
# Store summary in shared, indexed by current filename
filename = self.params["filename"] # Again, using params
if "summaries" not in shared:
shared["summaries"] = {}
shared["summaries"][filename] = exec_res
return "default"
# Create a per-file flow
load_file = LoadFile()
summarize = Summarize()
load_file >> summarize
summarize_file = Flow(start=load_file)
# Wrap in a BatchFlow to process all files
summarize_all_files = SummarizeAllFiles(start=summarize_file)
summarize_all_files.run(shared)
```
### Under the Hood
1. `prep(shared)` in the BatchFlow returns a list of param dicts—e.g., `[{"filename": "file1.txt"}, {"filename": "file2.txt"}, ...]`.
2. The **BatchFlow** loops through each dict. For each one:
- It merges the dict with the BatchFlow's own `params` (if any): `{**batch_flow.params, **dict_from_prep}`
- It calls `flow.run(shared)` using the merged parameters
- **IMPORTANT**: These parameters are passed to the child Flow's nodes via `self.params`, NOT via the shared store
3. This means the sub-Flow is run **repeatedly**, once for every param dict, with each node in the flow accessing the parameters via `self.params`.
---
## 3. Nested or Multi-Level Batches
You can nest a **BatchFlow** in another **BatchFlow**. For instance:
- **Outer** batch: returns a list of directory param dicts (e.g., `{"directory": "/pathA"}`, `{"directory": "/pathB"}`, ...).
- **Inner** batch: returning a list of per-file param dicts.
At each level, **BatchFlow** merges its own param dict with the parent’s. By the time you reach the **innermost** node, the final `params` is the merged result of **all** parents in the chain. This way, a nested structure can keep track of the entire context (e.g., directory + file name) at once.
```python
class FileBatchFlow(BatchFlow):
def prep(self, shared):
# Access directory from params (set by parent)
directory = self.params["directory"]
# e.g., files = ["file1.txt", "file2.txt", ...]
files = [f for f in os.listdir(directory) if f.endswith(".txt")]
return [{"filename": f} for f in files]
class DirectoryBatchFlow(BatchFlow):
def prep(self, shared):
directories = [ "/path/to/dirA", "/path/to/dirB"]
return [{"directory": d} for d in directories]
# The actual processing node
class ProcessFile(Node):
def prep(self, shared):
# Access both directory and filename from params
directory = self.params["directory"] # From outer batch
filename = self.params["filename"] # From inner batch
full_path = os.path.join(directory, filename)
return full_path
def exec(self, full_path):
# Process the file...
return f"Processed {full_path}"
def post(self, shared, prep_res, exec_res):
# Store results, perhaps indexed by path
if "results" not in shared:
shared["results"] = {}
shared["results"][prep_res] = exec_res
return "default"
# Set up the nested batch structure
process_node = ProcessFile()
inner_flow = FileBatchFlow(start=process_node)
outer_flow = DirectoryBatchFlow(start=inner_flow)
# Run it
outer_flow.run(shared)
```
================================================
FILE: .cursor/rules/core_abstraction/communication.mdc
================================================
---
description: Guidelines for using PocketFlow, Core Abstraction, Communication
globs:
alwaysApply: false
---
# Communication
Nodes and Flows **communicate** in 2 ways:
1. **Shared Store (for almost all the cases)**
- A global data structure (often an in-mem dict) that all nodes can read ( `prep()`) and write (`post()`).
- Great for data results, large content, or anything multiple nodes need.
- You shall design the data structure and populate it ahead.
- > **Separation of Concerns:** Use `Shared Store` for almost all cases to separate *Data Schema* from *Compute Logic*! This approach is both flexible and easy to manage, resulting in more maintainable code. `Params` is more a syntax sugar for [Batch](mdc:./batch.md).
{: .best-practice }
2. **Params (only for [Batch](mdc:./batch.md))**
- Each node has a local, ephemeral `params` dict passed in by the **parent Flow**, used as an identifier for tasks. Parameter keys and values shall be **immutable**.
- Good for identifiers like filenames or numeric IDs, in Batch mode.
If you know memory management, think of the **Shared Store** like a **heap** (shared by all function calls), and **Params** like a **stack** (assigned by the caller).
---
## 1. Shared Store
### Overview
A shared store is typically an in-mem dictionary, like:
```python
shared = {"data": {}, "summary": {}, "config": {...}, ...}
```
It can also contain local file handlers, DB connections, or a combination for persistence. We recommend deciding the data structure or DB schema first based on your app requirements.
### Example
```python
class LoadData(Node):
def post(self, shared, prep_res, exec_res):
# We write data to shared store
shared["data"] = "Some text content"
return None
class Summarize(Node):
def prep(self, shared):
# We read data from shared store
return shared["data"]
def exec(self, prep_res):
# Call LLM to summarize
prompt = f"Summarize: {prep_res}"
summary = call_llm(prompt)
return summary
def post(self, shared, prep_res, exec_res):
# We write summary to shared store
shared["summary"] = exec_res
return "default"
load_data = LoadData()
summarize = Summarize()
load_data >> summarize
flow = Flow(start=load_data)
shared = {}
flow.run(shared)
```
Here:
- `LoadData` writes to `shared["data"]`.
- `Summarize` reads from `shared["data"]`, summarizes, and writes to `shared["summary"]`.
---
## 2. Params
**Params** let you store *per-Node* or *per-Flow* config that doesn't need to live in the shared store. They are:
- **Immutable** during a Node's run cycle (i.e., they don't change mid-`prep->exec->post`).
- **Set** via `set_params()`.
- **Cleared** and updated each time a parent Flow calls it.
> Only set the uppermost Flow params because others will be overwritten by the parent Flow.
>
> If you need to set child node params, see [Batch](mdc:./batch.md).
{: .warning }
Typically, **Params** are identifiers (e.g., file name, page number). Use them to fetch the task you assigned or write to a specific part of the shared store.
### Example
```python
# 1) Create a Node that uses params
class SummarizeFile(Node):
def prep(self, shared):
# Access the node's param
filename = self.params["filename"]
return shared["data"].get(filename, "")
def exec(self, prep_res):
prompt = f"Summarize: {prep_res}"
return call_llm(prompt)
def post(self, shared, prep_res, exec_res):
filename = self.params["filename"]
shared["summary"][filename] = exec_res
return "default"
# 2) Set params
node = SummarizeFile()
# 3) Set Node params directly (for testing)
node.set_params({"filename": "doc1.txt"})
node.run(shared)
# 4) Create Flow
flow = Flow(start=node)
# 5) Set Flow params (overwrites node params)
flow.set_params({"filename": "doc2.txt"})
flow.run(shared) # The node summarizes doc2, not doc1
```
================================================
FILE: .cursor/rules/core_abstraction/flow.mdc
================================================
---
description: Guidelines for using PocketFlow, Core Abstraction, Flow
globs:
alwaysApply: false
---
# Flow
A **Flow** orchestrates a graph of Nodes. You can chain Nodes in a sequence or create branching depending on the **Actions** returned from each Node's `post()`.
## 1. Action-based Transitions
Each Node's `post()` returns an **Action** string. By default, if `post()` doesn't return anything, we treat that as `"default"`.
You define transitions with the syntax:
1. **Basic default transition**: `node_a >> node_b`
This means if `node_a.post()` returns `"default"`, go to `node_b`.
(Equivalent to `node_a - "default" >> node_b`)
2. **Named action transition**: `node_a - "action_name" >> node_b`
This means if `node_a.post()` returns `"action_name"`, go to `node_b`.
It's possible to create loops, branching, or multi-step flows.
## 2. Creating a Flow
A **Flow** begins with a **start** node. You call `Flow(start=some_node)` to specify the entry point. When you call `flow.run(shared)`, it executes the start node, looks at its returned Action from `post()`, follows the transition, and continues until there's no next node.
### Example: Simple Sequence
Here's a minimal flow of two nodes in a chain:
```python
node_a >> node_b
flow = Flow(start=node_a)
flow.run(shared)
```
- When you run the flow, it executes `node_a`.
- Suppose `node_a.post()` returns `"default"`.
- The flow then sees `"default"` Action is linked to `node_b` and runs `node_b`.
- `node_b.post()` returns `"default"` but we didn't define `node_b >> something_else`. So the flow ends there.
### Example: Branching & Looping
Here's a simple expense approval flow that demonstrates branching and looping. The `ReviewExpense` node can return three possible Actions:
- `"approved"`: expense is approved, move to payment processing
- `"needs_revision"`: expense needs changes, send back for revision
- `"rejected"`: expense is denied, finish the process
We can wire them like this:
```python
# Define the flow connections
review - "approved" >> payment # If approved, process payment
review - "needs_revision" >> revise # If needs changes, go to revision
review - "rejected" >> finish # If rejected, finish the process
revise >> review # After revision, go back for another review
payment >> finish # After payment, finish the process
flow = Flow(start=review)
```
Let's see how it flows:
1. If `review.post()` returns `"approved"`, the expense moves to the `payment` node
2. If `review.post()` returns `"needs_revision"`, it goes to the `revise` node, which then loops back to `review`
3. If `review.post()` returns `"rejected"`, it moves to the `finish` node and stops
```mermaid
flowchart TD
review[Review Expense] -->|approved| payment[Process Payment]
review -->|needs_revision| revise[Revise Report]
review -->|rejected| finish[Finish Process]
revise --> review
payment --> finish
```
### Running Individual Nodes vs. Running a Flow
- `node.run(shared)`: Just runs that node alone (calls `prep->exec->post()`), returns an Action.
- `flow.run(shared)`: Executes from the start node, follows Actions to the next node, and so on until the flow can't continue.
> `node.run(shared)` **does not** proceed to the successor.
> This is mainly for debugging or testing a single node.
>
> Always use `flow.run(...)` in production to ensure the full pipeline runs correctly.
{: .warning }
## 3. Nested Flows
A **Flow** can act like a Node, which enables powerful composition patterns. This means you can:
1. Use a Flow as a Node within another Flow's transitions.
2. Combine multiple smaller Flows into a larger Flow for reuse.
3. Node `params` will be a merging of **all** parents' `params`.
### Flow's Node Methods
A **Flow** is also a **Node**, so it will run `prep()` and `post()`. However:
- It **won't** run `exec()`, as its main logic is to orchestrate its nodes.
- `post()` always receives `None` for `exec_res` and should instead get the flow execution results from the shared store.
### Basic Flow Nesting
Here's how to connect a flow to another node:
```python
# Create a sub-flow
node_a >> node_b
subflow = Flow(start=node_a)
# Connect it to another node
subflow >> node_c
# Create the parent flow
parent_flow = Flow(start=subflow)
```
When `parent_flow.run()` executes:
1. It starts `subflow`
2. `subflow` runs through its nodes (`node_a->node_b`)
3. After `subflow` completes, execution continues to `node_c`
### Example: Order Processing Pipeline
Here's a practical example that breaks down order processing into nested flows:
```python
# Payment processing sub-flow
validate_payment >> process_payment >> payment_confirmation
payment_flow = Flow(start=validate_payment)
# Inventory sub-flow
check_stock >> reserve_items >> update_inventory
inventory_flow = Flow(start=check_stock)
# Shipping sub-flow
create_label >> assign_carrier >> schedule_pickup
shipping_flow = Flow(start=create_label)
# Connect the flows into a main order pipeline
payment_flow >> inventory_flow >> shipping_flow
# Create the master flow
order_pipeline = Flow(start=payment_flow)
# Run the entire pipeline
order_pipeline.run(shared_data)
```
This creates a clean separation of concerns while maintaining a clear execution path:
```mermaid
flowchart LR
subgraph order_pipeline[Order Pipeline]
subgraph paymentFlow["Payment Flow"]
A[Validate Payment] --> B[Process Payment] --> C[Payment Confirmation]
end
subgraph inventoryFlow["Inventory Flow"]
D[Check Stock] --> E[Reserve Items] --> F[Update Inventory]
end
subgraph shippingFlow["Shipping Flow"]
G[Create Label] --> H[Assign Carrier] --> I[Schedule Pickup]
end
paymentFlow --> inventoryFlow
inventoryFlow --> shippingFlow
end
```
================================================
FILE: .cursor/rules/core_abstraction/node.mdc
================================================
---
description: Guidelines for using PocketFlow, Core Abstraction, Node
globs:
alwaysApply: false
---
# Node
A **Node** is the smallest building block. Each Node has 3 steps `prep->exec->post`:
1. `prep(shared)`
- **Read and preprocess data** from `shared` store.
- Examples: *query DB, read files, or serialize data into a string*.
- Return `prep_res`, which is used by `exec()` and `post()`.
2. `exec(prep_res)`
- **Execute compute logic**, with optional retries and error handling (below).
- Examples: *(mostly) LLM calls, remote APIs, tool use*.
- ⚠️ This shall be only for compute and **NOT** access `shared`.
- ⚠️ If retries enabled, ensure idempotent implementation.
- ⚠️ Defer exception handling to the Node's built-in retry mechanism.
- Return `exec_res`, which is passed to `post()`.
3. `post(shared, prep_res, exec_res)`
- **Postprocess and write data** back to `shared`.
- Examples: *update DB, change states, log results*.
- **Decide the next action** by returning a *string* (`action = "default"` if *None*).
> **Why 3 steps?** To enforce the principle of *separation of concerns*. The data storage and data processing are operated separately.
>
> All steps are *optional*. E.g., you can only implement `prep` and `post` if you just need to process data.
{: .note }
### Fault Tolerance & Retries
You can **retry** `exec()` if it raises an exception via two parameters when define the Node:
- `max_retries` (int): Max times to run `exec()`. The default is `1` (**no** retry).
- `wait` (int): The time to wait (in **seconds**) before next retry. By default, `wait=0` (no waiting).
`wait` is helpful when you encounter rate-limits or quota errors from your LLM provider and need to back off.
```python
my_node = SummarizeFile(max_retries=3, wait=10)
```
When an exception occurs in `exec()`, the Node automatically retries until:
- It either succeeds, or
- The Node has retried `max_retries - 1` times already and fails on the last attempt.
You can get the current retry times (0-based) from `self.cur_retry`.
```python
class RetryNode(Node):
def exec(self, prep_res):
print(f"Retry {self.cur_retry} times")
raise Exception("Failed")
```
### Graceful Fallback
To **gracefully handle** the exception (after all retries) rather than raising it, override:
```python
def exec_fallback(self, prep_res, exc):
raise exc
```
By default, it just re-raises exception. But you can return a fallback result instead, which becomes the `exec_res` passed to `post()`.
### Example: Summarize file
```python
class SummarizeFile(Node):
def prep(self, shared):
return shared["data"]
def exec(self, prep_res):
if not prep_res:
return "Empty file content"
prompt = f"Summarize this text in 10 words: {prep_res}"
summary = call_llm(prompt) # might fail
return summary
def exec_fallback(self, prep_res, exc):
# Provide a simple fallback instead of crashing
return "There was an error processing your request."
def post(self, shared, prep_res, exec_res):
shared["summary"] = exec_res
# Return "default" by not returning
summarize_node = SummarizeFile(max_retries=3)
# node.run() calls prep->exec->post
# If exec() fails, it retries up to 3 times before calling exec_fallback()
action_result = summarize_node.run(shared)
print("Action returned:", action_result) # "default"
print("Summary stored:", shared["summary"])
```
================================================
FILE: .cursor/rules/core_abstraction/parallel.mdc
================================================
---
description: Guidelines for using PocketFlow, Core Abstraction, (Advanced) Parallel
globs:
alwaysApply: false
---
# (Advanced) Parallel
**Parallel** Nodes and Flows let you run multiple **Async** Nodes and Flows **concurrently**—for example, summarizing multiple texts at once. This can improve performance by overlapping I/O and compute.
> Because of Python’s GIL, parallel nodes and flows can’t truly parallelize CPU-bound tasks (e.g., heavy numerical computations). However, they excel at overlapping I/O-bound work—like LLM calls, database queries, API requests, or file I/O.
{: .warning }
> - **Ensure Tasks Are Independent**: If each item depends on the output of a previous item, **do not** parallelize.
>
> - **Beware of Rate Limits**: Parallel calls can **quickly** trigger rate limits on LLM services. You may need a **throttling** mechanism (e.g., semaphores or sleep intervals).
>
> - **Consider Single-Node Batch APIs**: Some LLMs offer a **batch inference** API where you can send multiple prompts in a single call. This is more complex to implement but can be more efficient than launching many parallel requests and mitigates rate limits.
{: .best-practice }
## AsyncParallelBatchNode
Like **AsyncBatchNode**, but run `exec_async()` in **parallel**:
```python
class ParallelSummaries(AsyncParallelBatchNode):
async def prep_async(self, shared):
# e.g., multiple texts
return shared["texts"]
async def exec_async(self, text):
prompt = f"Summarize: {text}"
return await call_llm_async(prompt)
async def post_async(self, shared, prep_res, exec_res_list):
shared["summary"] = "\n\n".join(exec_res_list)
return "default"
node = ParallelSummaries()
flow = AsyncFlow(start=node)
```
## AsyncParallelBatchFlow
Parallel version of **BatchFlow**. Each iteration of the sub-flow runs **concurrently** using different parameters:
```python
class SummarizeMultipleFiles(AsyncParallelBatchFlow):
async def prep_async(self, shared):
return [{"filename": f} for f in shared["files"]]
sub_flow = AsyncFlow(start=LoadAndSummarizeFile())
parallel_flow = SummarizeMultipleFiles(start=sub_flow)
await parallel_flow.run_async(shared)
```
================================================
FILE: .cursor/rules/design_pattern/agent.mdc
================================================
---
description: Guidelines for using PocketFlow, Design Pattern, Agent
globs:
alwaysApply: false
---
# Agent
Agent is a powerful design pattern in which nodes can take dynamic actions based on the context.
## Implement Agent with Graph
1. **Context and Action:** Implement nodes that supply context and perform actions.
2. **Branching:** Use branching to connect each action node to an agent node. Use action to allow the agent to direct the [flow](../core_abstraction/flow.md) between nodes—and potentially loop back for multi-step.
3. **Agent Node:** Provide a prompt to decide action—for example:
```python
f"""
### CONTEXT
Task: {task_description}
Previous Actions: {previous_actions}
Current State: {current_state}
### ACTION SPACE
[1] search
Description: Use web search to get results
Parameters:
- query (str): What to search for
[2] answer
Description: Conclude based on the results
Parameters:
- result (str): Final answer to provide
### NEXT ACTION
Decide the next action based on the current context and available action space.
Return your response in the following format:
```yaml
thinking: |
action:
parameters:
:
```"""
```
The core of building **high-performance** and **reliable** agents boils down to:
1. **Context Management:** Provide *relevant, minimal context.* For example, rather than including an entire chat history, retrieve the most relevant via [RAG](mdc:./rag.md). Even with larger context windows, LLMs still fall victim to ["lost in the middle"](https://arxiv.org/abs/2307.03172), overlooking mid-prompt content.
2. **Action Space:** Provide *a well-structured and unambiguous* set of actions—avoiding overlap like separate `read_databases` or `read_csvs`. Instead, import CSVs into the database.
## Example Good Action Design
- **Incremental:** Feed content in manageable chunks (500 lines or 1 page) instead of all at once.
- **Overview-zoom-in:** First provide high-level structure (table of contents, summary), then allow drilling into details (raw texts).
- **Parameterized/Programmable:** Instead of fixed actions, enable parameterized (columns to select) or programmable (SQL queries) actions, for example, to read CSV files.
- **Backtracking:** Let the agent undo the last step instead of restarting entirely, preserving progress when encountering errors or dead ends.
## Example: Search Agent
This agent:
1. Decides whether to search or answer
2. If searches, loops back to decide if more search needed
3. Answers when enough context gathered
```python
class DecideAction(Node):
def prep(self, shared):
context = shared.get("context", "No previous search")
query = shared["query"]
return query, context
def exec(self, inputs):
query, context = inputs
prompt = f"""
Given input: {query}
Previous search results: {context}
Should I: 1) Search web for more info 2) Answer with current knowledge
Output in yaml:
```yaml
action: search/answer
reason: why this action
search_term: search phrase if action is search
```"""
resp = call_llm(prompt)
yaml_str = resp.split("```yaml")[1].split("```")[0].strip()
result = yaml.safe_load(yaml_str)
assert isinstance(result, dict)
assert "action" in result
assert "reason" in result
assert result["action"] in ["search", "answer"]
if result["action"] == "search":
assert "search_term" in result
return result
def post(self, shared, prep_res, exec_res):
if exec_res["action"] == "search":
shared["search_term"] = exec_res["search_term"]
return exec_res["action"]
class SearchWeb(Node):
def prep(self, shared):
return shared["search_term"]
def exec(self, search_term):
return search_web(search_term)
def post(self, shared, prep_res, exec_res):
prev_searches = shared.get("context", [])
shared["context"] = prev_searches + [
{"term": shared["search_term"], "result": exec_res}
]
return "decide"
class DirectAnswer(Node):
def prep(self, shared):
return shared["query"], shared.get("context", "")
def exec(self, inputs):
query, context = inputs
return call_llm(f"Context: {context}\nAnswer: {query}")
def post(self, shared, prep_res, exec_res):
print(f"Answer: {exec_res}")
shared["answer"] = exec_res
# Connect nodes
decide = DecideAction()
search = SearchWeb()
answer = DirectAnswer()
decide - "search" >> search
decide - "answer" >> answer
search - "decide" >> decide # Loop back
flow = Flow(start=decide)
flow.run({"query": "Who won the Nobel Prize in Physics 2024?"})
```
================================================
FILE: .cursor/rules/design_pattern/mapreduce.mdc
================================================
---
description: Guidelines for using PocketFlow, Design Pattern, Map Reduce
globs:
alwaysApply: false
---
# Map Reduce
MapReduce is a design pattern suitable when you have either:
- Large input data (e.g., multiple files to process), or
- Large output data (e.g., multiple forms to fill)
and there is a logical way to break the task into smaller, ideally independent parts.
You first break down the task using [BatchNode](../core_abstraction/batch.md) in the map phase, followed by aggregation in the reduce phase.
### Example: Document Summarization
```python
class SummarizeAllFiles(BatchNode):
def prep(self, shared):
files_dict = shared["files"] # e.g. 10 files
return list(files_dict.items()) # [("file1.txt", "aaa..."), ("file2.txt", "bbb..."), ...]
def exec(self, one_file):
filename, file_content = one_file
summary_text = call_llm(f"Summarize the following file:\n{file_content}")
return (filename, summary_text)
def post(self, shared, prep_res, exec_res_list):
shared["file_summaries"] = dict(exec_res_list)
class CombineSummaries(Node):
def prep(self, shared):
return shared["file_summaries"]
def exec(self, file_summaries):
# format as: "File1: summary\nFile2: summary...\n"
text_list = []
for fname, summ in file_summaries.items():
text_list.append(f"{fname} summary:\n{summ}\n")
big_text = "\n---\n".join(text_list)
return call_llm(f"Combine these file summaries into one final summary:\n{big_text}")
def post(self, shared, prep_res, final_summary):
shared["all_files_summary"] = final_summary
batch_node = SummarizeAllFiles()
combine_node = CombineSummaries()
batch_node >> combine_node
flow = Flow(start=batch_node)
shared = {
"files": {
"file1.txt": "Alice was beginning to get very tired of sitting by her sister...",
"file2.txt": "Some other interesting text ...",
# ...
}
}
flow.run(shared)
print("Individual Summaries:", shared["file_summaries"])
print("\nFinal Summary:\n", shared["all_files_summary"])
```
> **Performance Tip**: The example above works sequentially. You can speed up the map phase by running it in parallel. See [(Advanced) Parallel](../core_abstraction/parallel.md) for more details.
{: .note }
================================================
FILE: .cursor/rules/design_pattern/multi_agent.mdc
================================================
---
description: Guidelines for using PocketFlow, Design Pattern, (Advanced) Multi-Agents
globs:
alwaysApply: false
---
# (Advanced) Multi-Agents
Multiple [Agents](mdc:./flow.md) can work together by handling subtasks and communicating the progress.
Communication between agents is typically implemented using message queues in shared storage.
> Most of time, you don't need Multi-Agents. Start with a simple solution first.
{: .best-practice }
### Example Agent Communication: Message Queue
Here's a simple example showing how to implement agent communication using `asyncio.Queue`.
The agent listens for messages, processes them, and continues listening:
```python
class AgentNode(AsyncNode):
async def prep_async(self, _):
message_queue = self.params["messages"]
message = await message_queue.get()
print(f"Agent received: {message}")
return message
# Create node and flow
agent = AgentNode()
agent >> agent # connect to self
flow = AsyncFlow(start=agent)
# Create heartbeat sender
async def send_system_messages(message_queue):
counter = 0
messages = [
"System status: all systems operational",
"Memory usage: normal",
"Network connectivity: stable",
"Processing load: optimal"
]
while True:
message = f"{messages[counter % len(messages)]} | timestamp_{counter}"
await message_queue.put(message)
counter += 1
await asyncio.sleep(1)
async def main():
message_queue = asyncio.Queue()
shared = {}
flow.set_params({"messages": message_queue})
# Run both coroutines
await asyncio.gather(
flow.run_async(shared),
send_system_messages(message_queue)
)
asyncio.run(main())
```
The output:
```
Agent received: System status: all systems operational | timestamp_0
Agent received: Memory usage: normal | timestamp_1
Agent received: Network connectivity: stable | timestamp_2
Agent received: Processing load: optimal | timestamp_3
```
### Interactive Multi-Agent Example: Taboo Game
Here's a more complex example where two agents play the word-guessing game Taboo.
One agent provides hints while avoiding forbidden words, and another agent tries to guess the target word:
```python
class AsyncHinter(AsyncNode):
async def prep_async(self, shared):
guess = await shared["hinter_queue"].get()
if guess == "GAME_OVER":
return None
return shared["target_word"], shared["forbidden_words"], shared.get("past_guesses", [])
async def exec_async(self, inputs):
if inputs is None:
return None
target, forbidden, past_guesses = inputs
prompt = f"Generate hint for '{target}'\nForbidden words: {forbidden}"
if past_guesses:
prompt += f"\nPrevious wrong guesses: {past_guesses}\nMake hint more specific."
prompt += "\nUse at most 5 words."
hint = call_llm(prompt)
print(f"\nHinter: Here's your hint - {hint}")
return hint
async def post_async(self, shared, prep_res, exec_res):
if exec_res is None:
return "end"
await shared["guesser_queue"].put(exec_res)
return "continue"
class AsyncGuesser(AsyncNode):
async def prep_async(self, shared):
hint = await shared["guesser_queue"].get()
return hint, shared.get("past_guesses", [])
async def exec_async(self, inputs):
hint, past_guesses = inputs
prompt = f"Given hint: {hint}, past wrong guesses: {past_guesses}, make a new guess. Directly reply a single word:"
guess = call_llm(prompt)
print(f"Guesser: I guess it's - {guess}")
return guess
async def post_async(self, shared, prep_res, exec_res):
if exec_res.lower() == shared["target_word"].lower():
print("Game Over - Correct guess!")
await shared["hinter_queue"].put("GAME_OVER")
return "end"
if "past_guesses" not in shared:
shared["past_guesses"] = []
shared["past_guesses"].append(exec_res)
await shared["hinter_queue"].put(exec_res)
return "continue"
async def main():
# Set up game
shared = {
"target_word": "nostalgia",
"forbidden_words": ["memory", "past", "remember", "feeling", "longing"],
"hinter_queue": asyncio.Queue(),
"guesser_queue": asyncio.Queue()
}
print("Game starting!")
print(f"Target word: {shared['target_word']}")
print(f"Forbidden words: {shared['forbidden_words']}")
# Initialize by sending empty guess to hinter
await shared["hinter_queue"].put("")
# Create nodes and flows
hinter = AsyncHinter()
guesser = AsyncGuesser()
# Set up flows
hinter_flow = AsyncFlow(start=hinter)
guesser_flow = AsyncFlow(start=guesser)
# Connect nodes to themselves
hinter - "continue" >> hinter
guesser - "continue" >> guesser
# Run both agents concurrently
await asyncio.gather(
hinter_flow.run_async(shared),
guesser_flow.run_async(shared)
)
asyncio.run(main())
```
The Output:
```
Game starting!
Target word: nostalgia
Forbidden words: ['memory', 'past', 'remember', 'feeling', 'longing']
Hinter: Here's your hint - Thinking of childhood summer days
Guesser: I guess it's - popsicle
Hinter: Here's your hint - When childhood cartoons make you emotional
Guesser: I guess it's - nostalgic
Hinter: Here's your hint - When old songs move you
Guesser: I guess it's - memories
Hinter: Here's your hint - That warm emotion about childhood
Guesser: I guess it's - nostalgia
Game Over - Correct guess!
```
================================================
FILE: .cursor/rules/design_pattern/rag.mdc
================================================
---
description: Guidelines for using PocketFlow, Design Pattern, RAG
globs:
alwaysApply: false
---
# RAG (Retrieval Augmented Generation)
For certain LLM tasks like answering questions, providing relevant context is essential. One common architecture is a **two-stage** RAG pipeline:
1. **Offline stage**: Preprocess and index documents ("building the index").
2. **Online stage**: Given a question, generate answers by retrieving the most relevant context.
---
## Stage 1: Offline Indexing
We create three Nodes:
1. `ChunkDocs` – [chunks](../utility_function/chunking.md) raw text.
2. `EmbedDocs` – [embeds](../utility_function/embedding.md) each chunk.
3. `StoreIndex` – stores embeddings into a [vector database](../utility_function/vector.md).
```python
class ChunkDocs(BatchNode):
def prep(self, shared):
# A list of file paths in shared["files"]. We process each file.
return shared["files"]
def exec(self, filepath):
# read file content. In real usage, do error handling.
with open(filepath, "r", encoding="utf-8") as f:
text = f.read()
# chunk by 100 chars each
chunks = []
size = 100
for i in range(0, len(text), size):
chunks.append(text[i : i + size])
return chunks
def post(self, shared, prep_res, exec_res_list):
# exec_res_list is a list of chunk-lists, one per file.
# flatten them all into a single list of chunks.
all_chunks = []
for chunk_list in exec_res_list:
all_chunks.extend(chunk_list)
shared["all_chunks"] = all_chunks
class EmbedDocs(BatchNode):
def prep(self, shared):
return shared["all_chunks"]
def exec(self, chunk):
return get_embedding(chunk)
def post(self, shared, prep_res, exec_res_list):
# Store the list of embeddings.
shared["all_embeds"] = exec_res_list
print(f"Total embeddings: {len(exec_res_list)}")
class StoreIndex(Node):
def prep(self, shared):
# We'll read all embeds from shared.
return shared["all_embeds"]
def exec(self, all_embeds):
# Create a vector index (faiss or other DB in real usage).
index = create_index(all_embeds)
return index
def post(self, shared, prep_res, index):
shared["index"] = index
# Wire them in sequence
chunk_node = ChunkDocs()
embed_node = EmbedDocs()
store_node = StoreIndex()
chunk_node >> embed_node >> store_node
OfflineFlow = Flow(start=chunk_node)
```
Usage example:
```python
shared = {
"files": ["doc1.txt", "doc2.txt"], # any text files
}
OfflineFlow.run(shared)
```
---
## Stage 2: Online Query & Answer
We have 3 nodes:
1. `EmbedQuery` – embeds the user’s question.
2. `RetrieveDocs` – retrieves top chunk from the index.
3. `GenerateAnswer` – calls the LLM with the question + chunk to produce the final answer.
```python
class EmbedQuery(Node):
def prep(self, shared):
return shared["question"]
def exec(self, question):
return get_embedding(question)
def post(self, shared, prep_res, q_emb):
shared["q_emb"] = q_emb
class RetrieveDocs(Node):
def prep(self, shared):
# We'll need the query embedding, plus the offline index/chunks
return shared["q_emb"], shared["index"], shared["all_chunks"]
def exec(self, inputs):
q_emb, index, chunks = inputs
I, D = search_index(index, q_emb, top_k=1)
best_id = I[0][0]
relevant_chunk = chunks[best_id]
return relevant_chunk
def post(self, shared, prep_res, relevant_chunk):
shared["retrieved_chunk"] = relevant_chunk
print("Retrieved chunk:", relevant_chunk[:60], "...")
class GenerateAnswer(Node):
def prep(self, shared):
return shared["question"], shared["retrieved_chunk"]
def exec(self, inputs):
question, chunk = inputs
prompt = f"Question: {question}\nContext: {chunk}\nAnswer:"
return call_llm(prompt)
def post(self, shared, prep_res, answer):
shared["answer"] = answer
print("Answer:", answer)
embed_qnode = EmbedQuery()
retrieve_node = RetrieveDocs()
generate_node = GenerateAnswer()
embed_qnode >> retrieve_node >> generate_node
OnlineFlow = Flow(start=embed_qnode)
```
Usage example:
```python
# Suppose we already ran OfflineFlow and have:
# shared["all_chunks"], shared["index"], etc.
shared["question"] = "Why do people like cats?"
OnlineFlow.run(shared)
# final answer in shared["answer"]
```
================================================
FILE: .cursor/rules/design_pattern/structure.mdc
================================================
---
description: Guidelines for using PocketFlow, Design Pattern, Structured Output
globs:
alwaysApply: false
---
# Structured Output
In many use cases, you may want the LLM to output a specific structure, such as a list or a dictionary with predefined keys.
There are several approaches to achieve a structured output:
- **Prompting** the LLM to strictly return a defined structure.
- Using LLMs that natively support **schema enforcement**.
- **Post-processing** the LLM's response to extract structured content.
In practice, **Prompting** is simple and reliable for modern LLMs.
### Example Use Cases
- Extracting Key Information
```yaml
product:
name: Widget Pro
price: 199.99
description: |
A high-quality widget designed for professionals.
Recommended for advanced users.
```
- Summarizing Documents into Bullet Points
```yaml
summary:
- This product is easy to use.
- It is cost-effective.
- Suitable for all skill levels.
```
- Generating Configuration Files
```yaml
server:
host: 127.0.0.1
port: 8080
ssl: true
```
## Prompt Engineering
When prompting the LLM to produce **structured** output:
1. **Wrap** the structure in code fences (e.g., `yaml`).
2. **Validate** that all required fields exist (and let `Node` handles retry).
### Example Text Summarization
```python
class SummarizeNode(Node):
def exec(self, prep_res):
# Suppose `prep_res` is the text to summarize.
prompt = f"""
Please summarize the following text as YAML, with exactly 3 bullet points
{prep_res}
Now, output:
```yaml
summary:
- bullet 1
- bullet 2
- bullet 3
```"""
response = call_llm(prompt)
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
import yaml
structured_result = yaml.safe_load(yaml_str)
assert "summary" in structured_result
assert isinstance(structured_result["summary"], list)
return structured_result
```
> Besides using `assert` statements, another popular way to validate schemas is [Pydantic](https://github.com/pydantic/pydantic)
{: .note }
### Why YAML instead of JSON?
Current LLMs struggle with escaping. YAML is easier with strings since they don't always need quotes.
**In JSON**
```json
{
"dialogue": "Alice said: \"Hello Bob.\\nHow are you?\\nI am good.\""
}
```
- Every double quote inside the string must be escaped with `\"`.
- Each newline in the dialogue must be represented as `\n`.
**In YAML**
```yaml
dialogue: |
Alice said: "Hello Bob.
How are you?
I am good."
```
- No need to escape interior quotes—just place the entire text under a block literal (`|`).
- Newlines are naturally preserved without needing `\n`.
================================================
FILE: .cursor/rules/design_pattern/workflow.mdc
================================================
---
description: Guidelines for using PocketFlow, Design Pattern, Workflow
globs:
alwaysApply: false
---
# Workflow
Many real-world tasks are too complex for one LLM call. The solution is to **Task Decomposition**: decompose them into a [chain](../core_abstraction/flow.md) of multiple Nodes.
> - You don't want to make each task **too coarse**, because it may be *too complex for one LLM call*.
> - You don't want to make each task **too granular**, because then *the LLM call doesn't have enough context* and results are *not consistent across nodes*.
>
> You usually need multiple *iterations* to find the *sweet spot*. If the task has too many *edge cases*, consider using [Agents](mdc:./agent.md).
{: .best-practice }
### Example: Article Writing
```python
class GenerateOutline(Node):
def prep(self, shared): return shared["topic"]
def exec(self, topic): return call_llm(f"Create a detailed outline for an article about {topic}")
def post(self, shared, prep_res, exec_res): shared["outline"] = exec_res
class WriteSection(Node):
def prep(self, shared): return shared["outline"]
def exec(self, outline): return call_llm(f"Write content based on this outline: {outline}")
def post(self, shared, prep_res, exec_res): shared["draft"] = exec_res
class ReviewAndRefine(Node):
def prep(self, shared): return shared["draft"]
def exec(self, draft): return call_llm(f"Review and improve this draft: {draft}")
def post(self, shared, prep_res, exec_res): shared["final_article"] = exec_res
# Connect nodes
outline = GenerateOutline()
write = WriteSection()
review = ReviewAndRefine()
outline >> write >> review
# Create and run flow
writing_flow = Flow(start=outline)
shared = {"topic": "AI Safety"}
writing_flow.run(shared)
```
For *dynamic cases*, consider using [Agents](mdc:./agent.md).
================================================
FILE: .cursor/rules/guide_for_pocketflow.mdc
================================================
---
description: Guidelines for using PocketFlow, Agentic Coding
globs: **/*.py
alwaysApply: true
---
# DOCUMENTATION FIRST POLICY
**CRITICAL INSTRUCTION**: When implementing a Pocket Flow app:
1. **ALWAYS REQUEST MDC FILES FIRST** - Before writing any code, request and review all relevant MDC documentation files. This doc provides an explaination of the documents.
2. **UNDERSTAND THE FRAMEWORK** - Gain comprehensive understanding of the Pocket Flow framework from documentation
3. **AVOID ASSUMPTION-DRIVEN DEVELOPMENT** - Do not base your implementation on assumptions or guesswork. Even if the human didn't explicitly mention pocket flow in their request, if the code you are editing is using pocket flow, you should request relevant docs to help you understand best practice as well before editing.
**VERIFICATION**: Begin each implementation with a brief summary of the documentation you've reviewed to inform your approach.
# Agentic Coding: Humans Design, Agents code!
> If you are an AI agent involved in building LLM Systems, read this guide **VERY, VERY** carefully! This is the most important chapter in the entire document. Throughout development, you should always (1) start with a small and simple solution, (2) design at a high level (`docs/design.md`) before implementation, and (3) frequently ask humans for feedback and clarification.
{: .warning }
## Agentic Coding Steps
Agentic Coding should be a collaboration between Human System Design and Agent Implementation:
| Steps | Human | AI | Comment |
|:-----------------------|:----------:|:---------:|:------------------------------------------------------------------------|
| 1. Requirements | ★★★ High | ★☆☆ Low | Humans understand the requirements and context. |
| 2. Flow | ★★☆ Medium | ★★☆ Medium | Humans specify the high-level design, and the AI fills in the details. |
| 3. Utilities | ★★☆ Medium | ★★☆ Medium | Humans provide available external APIs and integrations, and the AI helps with implementation. |
| 4. Data | ★☆☆ Low | ★★★ High | AI designs the data schema, and humans verify. |
| 5. Node | ★☆☆ Low | ★★★ High | The AI helps design the node based on the flow. |
| 6. Implementation | ★☆☆ Low | ★★★ High | The AI implements the flow based on the design. |
| 7. Optimization | ★★☆ Medium | ★★☆ Medium | Humans evaluate the results, and the AI helps optimize. |
| 8. Reliability | ★☆☆ Low | ★★★ High | The AI writes test cases and addresses corner cases. |
1. **Requirements**: Clarify the requirements for your project, and evaluate whether an AI system is a good fit.
- Understand AI systems' strengths and limitations:
- **Good for**: Routine tasks requiring common sense (filling forms, replying to emails)
- **Good for**: Creative tasks with well-defined inputs (building slides, writing SQL)
- **Not good for**: Ambiguous problems requiring complex decision-making (business strategy, startup planning)
- **Keep It User-Centric:** Explain the "problem" from the user's perspective rather than just listing features.
- **Balance complexity vs. impact**: Aim to deliver the highest value features with minimal complexity early.
2. **Flow Design**: Outline at a high level, describe how your AI system orchestrates nodes.
- Identify applicable design patterns (e.g., [Map Reduce], [Agent], [RAG]).
- For each node in the flow, start with a high-level one-line description of what it does.
- If using **Map Reduce**, specify how to map (what to split) and how to reduce (how to combine).
- If using **Agent**, specify what are the inputs (context) and what are the possible actions.
- If using **RAG**, specify what to embed, noting that there's usually both offline (indexing) and online (retrieval) workflows.
- Outline the flow and draw it in a mermaid diagram. For example:
```mermaid
flowchart LR
start[Start] --> batch[Batch]
batch --> check[Check]
check -->|OK| process
check -->|Error| fix[Fix]
fix --> check
subgraph process[Process]
step1[Step 1] --> step2[Step 2]
end
process --> endNode[End]
```
- > **If Humans can't specify the flow, AI Agents can't automate it!** Before building an LLM system, thoroughly understand the problem and potential solution by manually solving example inputs to develop intuition.
{: .best-practice }
3. **Utilities**: Based on the Flow Design, identify and implement necessary utility functions.
- Think of your AI system as the brain. It needs a body—these *external utility functions*—to interact with the real world:
- Reading inputs (e.g., retrieving Slack messages, reading emails)
- Writing outputs (e.g., generating reports, sending emails)
- Using external tools (e.g., calling LLMs, searching the web)
- **NOTE**: *LLM-based tasks* (e.g., summarizing text, analyzing sentiment) are **NOT** utility functions; rather, they are *core functions* internal in the AI system.
- For each utility function, implement it and write a simple test.
- Document their input/output, as well as why they are necessary. For example:
- `name`: `get_embedding` (`utils/get_embedding.py`)
- `input`: `str`
- `output`: a vector of 3072 floats
- `necessity`: Used by the second node to embed text
- Example utility implementation:
```python
# utils/call_llm.py
from openai import OpenAI
def call_llm(prompt):
client = OpenAI(api_key="YOUR_API_KEY_HERE")
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
if __name__ == "__main__":
prompt = "What is the meaning of life?"
print(call_llm(prompt))
```
- > **Sometimes, design Utilities before Flow:** For example, for an LLM project to automate a legacy system, the bottleneck will likely be the available interface to that system. Start by designing the hardest utilities for interfacing, and then build the flow around them.
{: .best-practice }
- > **Avoid Exception Handling in Utilities**: If a utility function is called from a Node's `exec()` method, avoid using `try...except` blocks within the utility. Let the Node's built-in retry mechanism handle failures.
{: .warning }
4. **Data Design**: Design the shared store that nodes will use to communicate.
- One core design principle for PocketFlow is to use a well-designed [shared store]—a data contract that all nodes agree upon to retrieve and store data.
- For simple systems, use an in-memory dictionary.
- For more complex systems or when persistence is required, use a database.
- **Don't Repeat Yourself**: Use in-memory references or foreign keys.
- Example shared store design:
```python
shared = {
"user": {
"id": "user123",
"context": { # Another nested dict
"weather": {"temp": 72, "condition": "sunny"},
"location": "San Francisco"
}
},
"results": {} # Empty dict to store outputs
}
```
5. **Node Design**: Plan how each node will read and write data, and use utility functions.
- For each [Node], describe its type, how it reads and writes data, and which utility function it uses. Keep it specific but high-level without codes. For example:
- `type`: Regular (or Batch, or Async)
- `prep`: Read "text" from the shared store
- `exec`: Call the embedding utility function. **Avoid exception handling here**; let the Node's retry mechanism manage failures.
- `post`: Write "embedding" to the shared store
6. **Implementation**: Implement the initial nodes and flows based on the design.
- 🎉 If you've reached this step, humans have finished the design. Now *Agentic Coding* begins!
- **"Keep it simple, stupid!"** Avoid complex features and full-scale type checking.
- **FAIL FAST**! Leverage the built-in [Node] retry and fallback mechanisms to handle failures gracefully. This helps you quickly identify weak points in the system.
- Add logging throughout the code to facilitate debugging.
7. **Optimization**:
- **Use Intuition**: For a quick initial evaluation, human intuition is often a good start.
- **Redesign Flow (Back to Step 3)**: Consider breaking down tasks further, introducing agentic decisions, or better managing input contexts.
- If your flow design is already solid, move on to micro-optimizations:
- **Prompt Engineering**: Use clear, specific instructions with examples to reduce ambiguity.
- **In-Context Learning**: Provide robust examples for tasks that are difficult to specify with instructions alone.
- > **You'll likely iterate a lot!** Expect to repeat Steps 3–6 hundreds of times.
>
>
{: .best-practice }
8. **Reliability**
- **Node Retries**: Add checks in the node `exec` to ensure outputs meet requirements, and consider increasing `max_retries` and `wait` times.
- **Logging and Visualization**: Maintain logs of all attempts and visualize node results for easier debugging.
- **Self-Evaluation**: Add a separate node (powered by an LLM) to review outputs when results are uncertain.
## Example LLM Project File Structure
```
my_project/
├── main.py
├── nodes.py
├── flow.py
├── utils/
│ ├── __init__.py
│ ├── call_llm.py
│ └── search_web.py
├── requirements.txt
└── docs/
└── design.md
```
- **`docs/design.md`**: Contains project documentation for each step above. This should be *high-level* and *no-code*.
- **`utils/`**: Contains all utility functions.
- It's recommended to dedicate one Python file to each API call, for example `call_llm.py` or `search_web.py`.
- Each file should also include a `main()` function to try that API call
- **`nodes.py`**: Contains all the node definitions.
```python
# nodes.py
from pocketflow import Node
from utils.call_llm import call_llm
class GetQuestionNode(Node):
def exec(self, _):
# Get question directly from user input
user_question = input("Enter your question: ")
return user_question
def post(self, shared, prep_res, exec_res):
# Store the user's question
shared["question"] = exec_res
return "default" # Go to the next node
class AnswerNode(Node):
def prep(self, shared):
# Read question from shared
return shared["question"]
def exec(self, question):
# Call LLM to get the answer
return call_llm(question)
def post(self, shared, prep_res, exec_res):
# Store the answer in shared
shared["answer"] = exec_res
```
- **`flow.py`**: Implements functions that create flows by importing node definitions and connecting them.
```python
# flow.py
from pocketflow import Flow
from nodes import GetQuestionNode, AnswerNode
def create_qa_flow():
"""Create and return a question-answering flow."""
# Create nodes
get_question_node = GetQuestionNode()
answer_node = AnswerNode()
# Connect nodes in sequence
get_question_node >> answer_node
# Create flow starting with input node
return Flow(start=get_question_node)
```
- **`main.py`**: Serves as the project's entry point.
```python
# main.py
from flow import create_qa_flow
# Example main function
# Please replace this with your own main function
def main():
shared = {
"question": None, # Will be populated by GetQuestionNode from user input
"answer": None # Will be populated by AnswerNode
}
# Create the flow and run it
qa_flow = create_qa_flow()
qa_flow.run(shared)
print(f"Question: {shared['question']}")
print(f"Answer: {shared['answer']}")
if __name__ == "__main__":
main()
```
# Pocket Flow
A [100-line](https://github.com/the-pocket/PocketFlow/blob/main/pocketflow/__init__.py) minimalist LLM framework for *Agents, Task Decomposition, RAG, etc*.
- **Lightweight**: Just the core graph abstraction in 100 lines. ZERO dependencies, and vendor lock-in.
- **Expressive**: Everything you love from larger frameworks—([Multi-])[Agents], [Workflow], [RAG], and more.
- **Agentic-Coding**: Intuitive enough for AI agents to help humans build complex LLM applications.
## Core Abstraction
We model the LLM workflow as a **Graph + Shared Store**:
- [Node] handles simple (LLM) tasks.
- [Flow] connects nodes through **Actions** (labeled edges).
- [Shared Store] enables communication between nodes within flows.
- [Batch] nodes/flows allow for data-intensive tasks.
- [Async] nodes/flows allow waiting for asynchronous tasks.
- [(Advanced) Parallel] nodes/flows handle I/O-bound tasks.
## Design Pattern
From there, it’s easy to implement popular design patterns:
- [Agent] autonomously makes decisions.
- [Workflow] chains multiple tasks into pipelines.
- [RAG] integrates data retrieval with generation.
- [Map Reduce] splits data tasks into Map and Reduce steps.
- [Structured Output] formats outputs consistently.
- [(Advanced) Multi-Agents] coordinate multiple agents.
## Utility Function
We **do not** provide built-in utilities. Instead, we offer *examples*—please *implement your own*:
- [LLM Wrapper]
- [Viz and Debug]
- [Web Search]
- [Chunking]
- [Embedding]
- [Vector Databases]
- [Text-to-Speech]
**Why not built-in?**: I believe it's a *bad practice* for vendor-specific APIs in a general framework:
- *API Volatility*: Frequent changes lead to heavy maintenance for hardcoded APIs.
- *Flexibility*: You may want to switch vendors, use fine-tuned models, or run them locally.
- *Optimizations*: Prompt caching, batching, and streaming are easier without vendor lock-in.
## Ready to build your Apps?
Check out [Agentic Coding Guidance], the fastest way to develop LLM projects with Pocket Flow!
================================================
FILE: .cursor/rules/utility_function/chunking.mdc
================================================
---
description: Guidelines for using PocketFlow, Utility Function, Text Chunking
globs:
alwaysApply: false
---
# Text Chunking
We recommend some implementations of commonly used text chunking approaches.
> Text Chunking is more a micro optimization, compared to the Flow Design.
>
> It's recommended to start with the Naive Chunking and optimize later.
{: .best-practice }
---
## Example Python Code Samples
### 1. Naive (Fixed-Size) Chunking
Splits text by a fixed number of words, ignoring sentence or semantic boundaries.
```python
def fixed_size_chunk(text, chunk_size=100):
chunks = []
for i in range(0, len(text), chunk_size):
chunks.append(text[i : i + chunk_size])
return chunks
```
However, sentences are often cut awkwardly, losing coherence.
### 2. Sentence-Based Chunking
```python
import nltk
def sentence_based_chunk(text, max_sentences=2):
sentences = nltk.sent_tokenize(text)
chunks = []
for i in range(0, len(sentences), max_sentences):
chunks.append(" ".join(sentences[i : i + max_sentences]))
return chunks
```
However, might not handle very long sentences or paragraphs well.
### 3. Other Chunking
- **Paragraph-Based**: Split text by paragraphs (e.g., newlines). Large paragraphs can create big chunks.
- **Semantic**: Use embeddings or topic modeling to chunk by semantic boundaries.
- **Agentic**: Use an LLM to decide chunk boundaries based on context or meaning.
================================================
FILE: .cursor/rules/utility_function/embedding.mdc
================================================
---
description: Guidelines for using PocketFlow, Utility Function, Embedding
globs:
alwaysApply: false
---
# Embedding
Below you will find an overview table of various text embedding APIs, along with example Python code.
> Embedding is more a micro optimization, compared to the Flow Design.
>
> It's recommended to start with the most convenient one and optimize later.
{: .best-practice }
| **API** | **Free Tier** | **Pricing Model** | **Docs** |
| --- | --- | --- | --- |
| **OpenAI** | ~$5 credit | ~$0.0001/1K tokens | [OpenAI Embeddings](https://platform.openai.com/docs/api-reference/embeddings) |
| **Azure OpenAI** | $200 credit | Same as OpenAI (~$0.0001/1K tokens) | [Azure OpenAI Embeddings](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?tabs=portal) |
| **Google Vertex AI** | $300 credit | ~$0.025 / million chars | [Vertex AI Embeddings](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) |
| **AWS Bedrock** | No free tier, but AWS credits may apply | ~$0.00002/1K tokens (Titan V2) | [Amazon Bedrock](https://docs.aws.amazon.com/bedrock/) |
| **Cohere** | Limited free tier | ~$0.0001/1K tokens | [Cohere Embeddings](https://docs.cohere.com/docs/cohere-embed) |
| **Hugging Face** | ~$0.10 free compute monthly | Pay per second of compute | [HF Inference API](https://huggingface.co/docs/api-inference) |
| **Jina** | 1M tokens free | Pay per token after | [Jina Embeddings](https://jina.ai/embeddings/) |
## Example Python Code
### 1. OpenAI
```python
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY")
response = client.embeddings.create(
model="text-embedding-ada-002",
input=text
)
# Extract the embedding vector from the response
embedding = response.data[0].embedding
embedding = np.array(embedding, dtype=np.float32)
print(embedding)
```
### 2. Azure OpenAI
```python
import openai
openai.api_type = "azure"
openai.api_base = "https://YOUR_RESOURCE_NAME.openai.azure.com"
openai.api_version = "2023-03-15-preview"
openai.api_key = "YOUR_AZURE_API_KEY"
resp = openai.Embedding.create(engine="ada-embedding", input="Hello world")
vec = resp["data"][0]["embedding"]
print(vec)
```
### 3. Google Vertex AI
```python
from vertexai.preview.language_models import TextEmbeddingModel
import vertexai
vertexai.init(project="YOUR_GCP_PROJECT_ID", location="us-central1")
model = TextEmbeddingModel.from_pretrained("textembedding-gecko@001")
emb = model.get_embeddings(["Hello world"])
print(emb[0])
```
### 4. AWS Bedrock
```python
import boto3, json
client = boto3.client("bedrock-runtime", region_name="us-east-1")
body = {"inputText": "Hello world"}
resp = client.invoke_model(modelId="amazon.titan-embed-text-v2:0", contentType="application/json", body=json.dumps(body))
resp_body = json.loads(resp["body"].read())
vec = resp_body["embedding"]
print(vec)
```
### 5. Cohere
```python
import cohere
co = cohere.Client("YOUR_API_KEY")
resp = co.embed(texts=["Hello world"])
vec = resp.embeddings[0]
print(vec)
```
### 6. Hugging Face
```python
import requests
API_URL = "https://api-inference.huggingface.co/models/sentence-transformers/all-MiniLM-L6-v2"
HEADERS = {"Authorization": "Bearer YOUR_HF_TOKEN"}
res = requests.post(API_URL, headers=HEADERS, json={"inputs": "Hello world"})
vec = res.json()[0]
print(vec)
```
### 7. Jina
```python
import requests
url = "https://api.jina.ai/v2/embed"
headers = {"Authorization": "Bearer YOUR_JINA_TOKEN"}
payload = {"data": ["Hello world"], "model": "jina-embeddings-v3"}
res = requests.post(url, headers=headers, json=payload)
vec = res.json()["data"][0]["embedding"]
print(vec)
```
================================================
FILE: .cursor/rules/utility_function/llm.mdc
================================================
---
description: Guidelines for using PocketFlow, Utility Function, LLM Wrapper
globs:
alwaysApply: false
---
# LLM Wrappers
Check out libraries like [litellm](https://github.com/BerriAI/litellm).
Here, we provide some minimal example implementations:
1. OpenAI
```python
def call_llm(prompt):
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY_HERE")
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
# Example usage
call_llm("How are you?")
```
> Store the API key in an environment variable like OPENAI_API_KEY for security.
{: .best-practice }
2. Claude (Anthropic)
```python
def call_llm(prompt):
from anthropic import Anthropic
client = Anthropic(api_key="YOUR_API_KEY_HERE")
r = client.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=3000,
messages=[
{"role": "user", "content": prompt}
]
)
return r.content[0].text
```
3. Google (Generative AI Studio / PaLM API)
```python
def call_llm(prompt):
from google import genai
client = genai.Client(api_key='GEMINI_API_KEY')
response = client.models.generate_content(
model='gemini-2.0-flash-001',
contents=prompt
)
return response.text
```
4. Azure (Azure OpenAI)
```python
def call_llm(prompt):
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint="https://.openai.azure.com/",
api_key="YOUR_API_KEY_HERE",
api_version="2023-05-15"
)
r = client.chat.completions.create(
model="",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
```
5. Ollama (Local LLM)
```python
def call_llm(prompt):
from ollama import chat
response = chat(
model="llama2",
messages=[{"role": "user", "content": prompt}]
)
return response.message.content
```
6. DeepSeek
```python
def call_llm(prompt):
from openai import OpenAI
client = OpenAI(api_key="YOUR_DEEPSEEK_API_KEY", base_url="https://api.deepseek.com")
r = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
```
## Improvements
Feel free to enhance your `call_llm` function as needed. Here are examples:
- Handle chat history:
```python
def call_llm(messages):
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY_HERE")
r = client.chat.completions.create(
model="gpt-4o",
messages=messages
)
return r.choices[0].message.content
```
- Add in-memory caching
```python
from functools import lru_cache
@lru_cache(maxsize=1000)
def call_llm(prompt):
# Your implementation here
pass
```
> ⚠️ Caching conflicts with Node retries, as retries yield the same result.
>
> To address this, you could use cached results only if not retried.
{: .warning }
```python
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_call(prompt):
pass
def call_llm(prompt, use_cache):
if use_cache:
return cached_call(prompt)
# Call the underlying function directly
return cached_call.__wrapped__(prompt)
class SummarizeNode(Node):
def exec(self, text):
return call_llm(f"Summarize: {text}", self.cur_retry==0)
```
- Enable logging:
```python
def call_llm(prompt):
import logging
logging.info(f"Prompt: {prompt}")
response = ... # Your implementation here
logging.info(f"Response: {response}")
return response
```
================================================
FILE: .cursor/rules/utility_function/text_to_speech.mdc
================================================
---
description: Guidelines for using PocketFlow, Utility Function, Text-to-Speech
globs:
alwaysApply: false
---
# Text-to-Speech
| **Service** | **Free Tier** | **Pricing Model** | **Docs** |
|----------------------|-----------------------|--------------------------------------------------------------|---------------------------------------------------------------------|
| **Amazon Polly** | 5M std + 1M neural | ~$4 /M (std), ~$16 /M (neural) after free tier | [Polly Docs](https://aws.amazon.com/polly/) |
| **Google Cloud TTS** | 4M std + 1M WaveNet | ~$4 /M (std), ~$16 /M (WaveNet) pay-as-you-go | [Cloud TTS Docs](https://cloud.google.com/text-to-speech) |
| **Azure TTS** | 500K neural ongoing | ~$15 /M (neural), discount at higher volumes | [Azure TTS Docs](https://azure.microsoft.com/products/cognitive-services/text-to-speech/) |
| **IBM Watson TTS** | 10K chars Lite plan | ~$0.02 /1K (i.e. ~$20 /M). Enterprise options available | [IBM Watson Docs](https://www.ibm.com/cloud/watson-text-to-speech) |
| **ElevenLabs** | 10K chars monthly | From ~$5/mo (30K chars) up to $330/mo (2M chars). Enterprise | [ElevenLabs Docs](https://elevenlabs.io) |
## Example Python Code
### Amazon Polly
```python
import boto3
polly = boto3.client("polly", region_name="us-east-1",
aws_access_key_id="YOUR_AWS_ACCESS_KEY_ID",
aws_secret_access_key="YOUR_AWS_SECRET_ACCESS_KEY")
resp = polly.synthesize_speech(
Text="Hello from Polly!",
OutputFormat="mp3",
VoiceId="Joanna"
)
with open("polly.mp3", "wb") as f:
f.write(resp["AudioStream"].read())
```
### Google Cloud TTS
```python
from google.cloud import texttospeech
client = texttospeech.TextToSpeechClient()
input_text = texttospeech.SynthesisInput(text="Hello from Google Cloud TTS!")
voice = texttospeech.VoiceSelectionParams(language_code="en-US")
audio_cfg = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.MP3)
resp = client.synthesize_speech(input=input_text, voice=voice, audio_config=audio_cfg)
with open("gcloud_tts.mp3", "wb") as f:
f.write(resp.audio_content)
```
### Azure TTS
```python
import azure.cognitiveservices.speech as speechsdk
speech_config = speechsdk.SpeechConfig(
subscription="AZURE_KEY", region="AZURE_REGION")
audio_cfg = speechsdk.audio.AudioConfig(filename="azure_tts.wav")
synthesizer = speechsdk.SpeechSynthesizer(
speech_config=speech_config,
audio_config=audio_cfg
)
synthesizer.speak_text_async("Hello from Azure TTS!").get()
```
### IBM Watson TTS
```python
from ibm_watson import TextToSpeechV1
from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
auth = IAMAuthenticator("IBM_API_KEY")
service = TextToSpeechV1(authenticator=auth)
service.set_service_url("IBM_SERVICE_URL")
resp = service.synthesize(
"Hello from IBM Watson!",
voice="en-US_AllisonV3Voice",
accept="audio/mp3"
).get_result()
with open("ibm_tts.mp3", "wb") as f:
f.write(resp.content)
```
### ElevenLabs
```python
import requests
api_key = "ELEVENLABS_KEY"
voice_id = "ELEVENLABS_VOICE"
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
headers = {"xi-api-key": api_key, "Content-Type": "application/json"}
json_data = {
"text": "Hello from ElevenLabs!",
"voice_settings": {"stability": 0.75, "similarity_boost": 0.75}
}
resp = requests.post(url, headers=headers, json=json_data)
with open("elevenlabs.mp3", "wb") as f:
f.write(resp.content)
```
================================================
FILE: .cursor/rules/utility_function/vector.mdc
================================================
---
description: Guidelines for using PocketFlow, Utility Function, Vector Databases
globs:
alwaysApply: false
---
# Vector Databases
Below is a table of the popular vector search solutions:
| **Tool** | **Free Tier** | **Pricing Model** | **Docs** |
| --- | --- | --- | --- |
| **FAISS** | N/A, self-host | Open-source | [Faiss.ai](https://faiss.ai) |
| **Pinecone** | 2GB free | From $25/mo | [pinecone.io](https://pinecone.io) |
| **Qdrant** | 1GB free cloud | Pay-as-you-go | [qdrant.tech](https://qdrant.tech) |
| **Weaviate** | 14-day sandbox | From $25/mo | [weaviate.io](https://weaviate.io) |
| **Milvus** | 5GB free cloud | PAYG or $99/mo dedicated | [milvus.io](https://milvus.io) |
| **Chroma** | N/A, self-host | Free (Apache 2.0) | [trychroma.com](https://trychroma.com) |
| **Redis** | 30MB free | From $5/mo | [redis.io](https://redis.io) |
---
## Example Python Code
Below are basic usage snippets for each tool.
### FAISS
```python
import faiss
import numpy as np
# Dimensionality of embeddings
d = 128
# Create a flat L2 index
index = faiss.IndexFlatL2(d)
# Random vectors
data = np.random.random((1000, d)).astype('float32')
index.add(data)
# Query
query = np.random.random((1, d)).astype('float32')
D, I = index.search(query, k=5)
print("Distances:", D)
print("Neighbors:", I)
```
### Pinecone
```python
import pinecone
pinecone.init(api_key="YOUR_API_KEY", environment="YOUR_ENV")
index_name = "my-index"
# Create the index if it doesn't exist
if index_name not in pinecone.list_indexes():
pinecone.create_index(name=index_name, dimension=128)
# Connect
index = pinecone.Index(index_name)
# Upsert
vectors = [
("id1", [0.1]*128),
("id2", [0.2]*128)
]
index.upsert(vectors)
# Query
response = index.query([[0.15]*128], top_k=3)
print(response)
```
### Qdrant
```python
import qdrant_client
from qdrant_client.models import Distance, VectorParams, PointStruct
client = qdrant_client.QdrantClient(
url="https://YOUR-QDRANT-CLOUD-ENDPOINT",
api_key="YOUR_API_KEY"
)
collection = "my_collection"
client.recreate_collection(
collection_name=collection,
vectors_config=VectorParams(size=128, distance=Distance.COSINE)
)
points = [
PointStruct(id=1, vector=[0.1]*128, payload={"type": "doc1"}),
PointStruct(id=2, vector=[0.2]*128, payload={"type": "doc2"}),
]
client.upsert(collection_name=collection, points=points)
results = client.search(
collection_name=collection,
query_vector=[0.15]*128,
limit=2
)
print(results)
```
### Weaviate
```python
import weaviate
client = weaviate.Client("https://YOUR-WEAVIATE-CLOUD-ENDPOINT")
schema = {
"classes": [
{
"class": "Article",
"vectorizer": "none"
}
]
}
client.schema.create(schema)
obj = {
"title": "Hello World",
"content": "Weaviate vector search"
}
client.data_object.create(obj, "Article", vector=[0.1]*128)
resp = (
client.query
.get("Article", ["title", "content"])
.with_near_vector({"vector": [0.15]*128})
.with_limit(3)
.do()
)
print(resp)
```
### Milvus
```python
from pymilvus import connections, FieldSchema, CollectionSchema, DataType, Collection
import numpy as np
connections.connect(alias="default", host="localhost", port="19530")
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=128)
]
schema = CollectionSchema(fields)
collection = Collection("MyCollection", schema)
emb = np.random.rand(10, 128).astype('float32')
ids = list(range(10))
collection.insert([ids, emb])
index_params = {
"index_type": "IVF_FLAT",
"params": {"nlist": 128},
"metric_type": "L2"
}
collection.create_index("embedding", index_params)
collection.load()
query_emb = np.random.rand(1, 128).astype('float32')
results = collection.search(query_emb, "embedding", param={"nprobe": 10}, limit=3)
print(results)
```
### Chroma
```python
import chromadb
from chromadb.config import Settings
client = chromadb.Client(Settings(
chroma_db_impl="duckdb+parquet",
persist_directory="./chroma_data"
))
coll = client.create_collection("my_collection")
vectors = [[0.1, 0.2, 0.3], [0.2, 0.2, 0.2]]
metas = [{"doc": "text1"}, {"doc": "text2"}]
ids = ["id1", "id2"]
coll.add(embeddings=vectors, metadatas=metas, ids=ids)
res = coll.query(query_embeddings=[[0.15, 0.25, 0.3]], n_results=2)
print(res)
```
### Redis
```python
import redis
import struct
r = redis.Redis(host="localhost", port=6379)
# Create index
r.execute_command(
"FT.CREATE", "my_idx", "ON", "HASH",
"SCHEMA", "embedding", "VECTOR", "FLAT", "6",
"TYPE", "FLOAT32", "DIM", "128",
"DISTANCE_METRIC", "L2"
)
# Insert
vec = struct.pack('128f', *[0.1]*128)
r.hset("doc1", mapping={"embedding": vec})
# Search
qvec = struct.pack('128f', *[0.15]*128)
q = "*=>[KNN 3 @embedding $BLOB AS dist]"
res = r.ft("my_idx").search(q, query_params={"BLOB": qvec})
print(res.docs)
```
================================================
FILE: .cursor/rules/utility_function/viz.mdc
================================================
---
description: Guidelines for using PocketFlow, Utility Function, Viz and Debug
globs:
alwaysApply: false
---
# Visualization and Debugging
Similar to LLM wrappers, we **don't** provide built-in visualization and debugging. Here, we recommend some *minimal* (and incomplete) implementations These examples can serve as a starting point for your own tooling.
## 1. Visualization with Mermaid
This code recursively traverses the nested graph, assigns unique IDs to each node, and treats Flow nodes as subgraphs to generate Mermaid syntax for a hierarchical visualization.
{% raw %}
```python
def build_mermaid(start):
ids, visited, lines = {}, set(), ["graph LR"]
ctr = 1
def get_id(n):
nonlocal ctr
return ids[n] if n in ids else (ids.setdefault(n, f"N{ctr}"), (ctr := ctr + 1))[0]
def link(a, b):
lines.append(f" {a} --> {b}")
def walk(node, parent=None):
if node in visited:
return parent and link(parent, get_id(node))
visited.add(node)
if isinstance(node, Flow):
node.start_node and parent and link(parent, get_id(node.start_node))
lines.append(f"\n subgraph sub_flow_{get_id(node)}[{type(node).__name__}]")
node.start_node and walk(node.start_node)
for nxt in node.successors.values():
node.start_node and walk(nxt, get_id(node.start_node)) or (parent and link(parent, get_id(nxt))) or walk(nxt)
lines.append(" end\n")
else:
lines.append(f" {(nid := get_id(node))}['{type(node).__name__}']")
parent and link(parent, nid)
[walk(nxt, nid) for nxt in node.successors.values()]
walk(start)
return "\n".join(lines)
```
{% endraw %}
For example, suppose we have a complex Flow for data science:
```python
class DataPrepBatchNode(BatchNode):
def prep(self,shared): return []
class ValidateDataNode(Node): pass
class FeatureExtractionNode(Node): pass
class TrainModelNode(Node): pass
class EvaluateModelNode(Node): pass
class ModelFlow(Flow): pass
class DataScienceFlow(Flow):pass
feature_node = FeatureExtractionNode()
train_node = TrainModelNode()
evaluate_node = EvaluateModelNode()
feature_node >> train_node >> evaluate_node
model_flow = ModelFlow(start=feature_node)
data_prep_node = DataPrepBatchNode()
validate_node = ValidateDataNode()
data_prep_node >> validate_node >> model_flow
data_science_flow = DataScienceFlow(start=data_prep_node)
result = build_mermaid(start=data_science_flow)
```
The code generates a Mermaid diagram:
```mermaid
graph LR
subgraph sub_flow_N1[DataScienceFlow]
N2['DataPrepBatchNode']
N3['ValidateDataNode']
N2 --> N3
N3 --> N4
subgraph sub_flow_N5[ModelFlow]
N4['FeatureExtractionNode']
N6['TrainModelNode']
N4 --> N6
N7['EvaluateModelNode']
N6 --> N7
end
end
```
For visualization based on d3.js, check out [the cookbook](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-visualization).
## 2. Call Stack Debugging
It would be useful to print the Node call stacks for debugging. This can be achieved by inspecting the runtime call stack:
```python
import inspect
def get_node_call_stack():
stack = inspect.stack()
node_names = []
seen_ids = set()
for frame_info in stack[1:]:
local_vars = frame_info.frame.f_locals
if 'self' in local_vars:
caller_self = local_vars['self']
if isinstance(caller_self, BaseNode) and id(caller_self) not in seen_ids:
seen_ids.add(id(caller_self))
node_names.append(type(caller_self).__name__)
return node_names
```
For example, suppose we have a complex Flow for data science:
```python
class DataPrepBatchNode(BatchNode):
def prep(self, shared): return []
class ValidateDataNode(Node): pass
class FeatureExtractionNode(Node): pass
class TrainModelNode(Node): pass
class EvaluateModelNode(Node):
def prep(self, shared):
stack = get_node_call_stack()
print("Call stack:", stack)
class ModelFlow(Flow): pass
class DataScienceFlow(Flow):pass
feature_node = FeatureExtractionNode()
train_node = TrainModelNode()
evaluate_node = EvaluateModelNode()
feature_node >> train_node >> evaluate_node
model_flow = ModelFlow(start=feature_node)
data_prep_node = DataPrepBatchNode()
validate_node = ValidateDataNode()
data_prep_node >> validate_node >> model_flow
data_science_flow = DataScienceFlow(start=data_prep_node)
data_science_flow.run({})
```
The output would be: `Call stack: ['EvaluateModelNode', 'ModelFlow', 'DataScienceFlow']`
For a more complete implementation, check out [the cookbook](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-tracing).
================================================
FILE: .cursor/rules/utility_function/websearch.mdc
================================================
---
description: Guidelines for using PocketFlow, Utility Function, Web Search
globs:
alwaysApply: false
---
# Web Search
We recommend some implementations of commonly used web search tools.
| **API** | **Free Tier** | **Pricing Model** | **Docs** |
|---------------------------------|-----------------------------------------------|-----------------------------------------------------------------|------------------------------------------------------------------------|
| **Google Custom Search JSON API** | 100 queries/day free | $5 per 1000 queries. | [Link](https://developers.google.com/custom-search/v1/overview) |
| **Bing Web Search API** | 1,000 queries/month | $15–$25 per 1,000 queries. | [Link](https://azure.microsoft.com/en-us/services/cognitive-services/bing-web-search-api/) |
| **DuckDuckGo Instant Answer** | Completely free (Instant Answers only, **no URLs**) | No paid plans; usage unlimited, but data is limited | [Link](https://duckduckgo.com/api) |
| **Brave Search API** | 2,000 queries/month free | $3 per 1k queries for Base, $5 per 1k for Pro | [Link](https://brave.com/search/api/) |
| **SerpApi** | 100 searches/month free | Start at $75/month for 5,000 searches| [Link](https://serpapi.com/) |
| **RapidAPI** | Many options | Many options | [Link](https://rapidapi.com/search?term=search&sortBy=ByRelevance) |
## Example Python Code
### 1. Google Custom Search JSON API
```python
import requests
API_KEY = "YOUR_API_KEY"
CX_ID = "YOUR_CX_ID"
query = "example"
url = "https://www.googleapis.com/customsearch/v1"
params = {
"key": API_KEY,
"cx": CX_ID,
"q": query
}
response = requests.get(url, params=params)
results = response.json()
print(results)
```
### 2. Bing Web Search API
```python
import requests
SUBSCRIPTION_KEY = "YOUR_BING_API_KEY"
query = "example"
url = "https://api.bing.microsoft.com/v7.0/search"
headers = {"Ocp-Apim-Subscription-Key": SUBSCRIPTION_KEY}
params = {"q": query}
response = requests.get(url, headers=headers, params=params)
results = response.json()
print(results)
```
### 3. DuckDuckGo Instant Answer
```python
import requests
query = "example"
url = "https://api.duckduckgo.com/"
params = {
"q": query,
"format": "json"
}
response = requests.get(url, params=params)
results = response.json()
print(results)
```
### 4. Brave Search API
```python
import requests
SUBSCRIPTION_TOKEN = "YOUR_BRAVE_API_TOKEN"
query = "example"
url = "https://api.search.brave.com/res/v1/web/search"
headers = {
"X-Subscription-Token": SUBSCRIPTION_TOKEN
}
params = {
"q": query
}
response = requests.get(url, headers=headers, params=params)
results = response.json()
print(results)
```
### 5. SerpApi
```python
import requests
API_KEY = "YOUR_SERPAPI_KEY"
query = "example"
url = "https://serpapi.com/search"
params = {
"engine": "google",
"q": query,
"api_key": API_KEY
}
response = requests.get(url, params=params)
results = response.json()
print(results)
```
================================================
FILE: .cursorrules
================================================
---
layout: default
title: "Agentic Coding"
---
# Agentic Coding: Humans Design, Agents code!
> If you are an AI agent involved in building LLM Systems, read this guide **VERY, VERY** carefully! This is the most important chapter in the entire document. Throughout development, you should always (1) start with a small and simple solution, (2) design at a high level (`docs/design.md`) before implementation, and (3) frequently ask humans for feedback and clarification.
{: .warning }
## Agentic Coding Steps
Agentic Coding should be a collaboration between Human System Design and Agent Implementation:
| Steps | Human | AI | Comment |
|:-----------------------|:----------:|:---------:|:------------------------------------------------------------------------|
| 1. Requirements | ★★★ High | ★☆☆ Low | Humans understand the requirements and context. |
| 2. Flow | ★★☆ Medium | ★★☆ Medium | Humans specify the high-level design, and the AI fills in the details. |
| 3. Utilities | ★★☆ Medium | ★★☆ Medium | Humans provide available external APIs and integrations, and the AI helps with implementation. |
| 4. Data | ★☆☆ Low | ★★★ High | AI designs the data schema, and humans verify. |
| 5. Node | ★☆☆ Low | ★★★ High | The AI helps design the node based on the flow. |
| 6. Implementation | ★☆☆ Low | ★★★ High | The AI implements the flow based on the design. |
| 7. Optimization | ★★☆ Medium | ★★☆ Medium | Humans evaluate the results, and the AI helps optimize. |
| 8. Reliability | ★☆☆ Low | ★★★ High | The AI writes test cases and addresses corner cases. |
1. **Requirements**: Clarify the requirements for your project, and evaluate whether an AI system is a good fit.
- Understand AI systems' strengths and limitations:
- **Good for**: Routine tasks requiring common sense (filling forms, replying to emails)
- **Good for**: Creative tasks with well-defined inputs (building slides, writing SQL)
- **Not good for**: Ambiguous problems requiring complex decision-making (business strategy, startup planning)
- **Keep It User-Centric:** Explain the "problem" from the user's perspective rather than just listing features.
- **Balance complexity vs. impact**: Aim to deliver the highest value features with minimal complexity early.
2. **Flow Design**: Outline at a high level, describe how your AI system orchestrates nodes.
- Identify applicable design patterns (e.g., [Map Reduce](./design_pattern/mapreduce.md), [Agent](./design_pattern/agent.md), [RAG](./design_pattern/rag.md)).
- For each node in the flow, start with a high-level one-line description of what it does.
- If using **Map Reduce**, specify how to map (what to split) and how to reduce (how to combine).
- If using **Agent**, specify what are the inputs (context) and what are the possible actions.
- If using **RAG**, specify what to embed, noting that there's usually both offline (indexing) and online (retrieval) workflows.
- Outline the flow and draw it in a mermaid diagram. For example:
```mermaid
flowchart LR
start[Start] --> batch[Batch]
batch --> check[Check]
check -->|OK| process
check -->|Error| fix[Fix]
fix --> check
subgraph process[Process]
step1[Step 1] --> step2[Step 2]
end
process --> endNode[End]
```
- > **If Humans can't specify the flow, AI Agents can't automate it!** Before building an LLM system, thoroughly understand the problem and potential solution by manually solving example inputs to develop intuition.
{: .best-practice }
3. **Utilities**: Based on the Flow Design, identify and implement necessary utility functions.
- Think of your AI system as the brain. It needs a body—these *external utility functions*—to interact with the real world:
- Reading inputs (e.g., retrieving Slack messages, reading emails)
- Writing outputs (e.g., generating reports, sending emails)
- Using external tools (e.g., calling LLMs, searching the web)
- **NOTE**: *LLM-based tasks* (e.g., summarizing text, analyzing sentiment) are **NOT** utility functions; rather, they are *core functions* internal in the AI system.
- For each utility function, implement it and write a simple test.
- Document their input/output, as well as why they are necessary. For example:
- `name`: `get_embedding` (`utils/get_embedding.py`)
- `input`: `str`
- `output`: a vector of 3072 floats
- `necessity`: Used by the second node to embed text
- Example utility implementation:
```python
# utils/call_llm.py
from openai import OpenAI
def call_llm(prompt):
client = OpenAI(api_key="YOUR_API_KEY_HERE")
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
if __name__ == "__main__":
prompt = "What is the meaning of life?"
print(call_llm(prompt))
```
- > **Sometimes, design Utilities before Flow:** For example, for an LLM project to automate a legacy system, the bottleneck will likely be the available interface to that system. Start by designing the hardest utilities for interfacing, and then build the flow around them.
{: .best-practice }
- > **Avoid Exception Handling in Utilities**: If a utility function is called from a Node's `exec()` method, avoid using `try...except` blocks within the utility. Let the Node's built-in retry mechanism handle failures.
{: .warning }
4. **Data Design**: Design the shared store that nodes will use to communicate.
- One core design principle for PocketFlow is to use a well-designed [shared store](./core_abstraction/communication.md)—a data contract that all nodes agree upon to retrieve and store data.
- For simple systems, use an in-memory dictionary.
- For more complex systems or when persistence is required, use a database.
- **Don't Repeat Yourself**: Use in-memory references or foreign keys.
- Example shared store design:
```python
shared = {
"user": {
"id": "user123",
"context": { # Another nested dict
"weather": {"temp": 72, "condition": "sunny"},
"location": "San Francisco"
}
},
"results": {} # Empty dict to store outputs
}
```
5. **Node Design**: Plan how each node will read and write data, and use utility functions.
- For each [Node](./core_abstraction/node.md), describe its type, how it reads and writes data, and which utility function it uses. Keep it specific but high-level without codes. For example:
- `type`: Regular (or Batch, or Async)
- `prep`: Read "text" from the shared store
- `exec`: Call the embedding utility function. **Avoid exception handling here**; let the Node's retry mechanism manage failures.
- `post`: Write "embedding" to the shared store
6. **Implementation**: Implement the initial nodes and flows based on the design.
- 🎉 If you've reached this step, humans have finished the design. Now *Agentic Coding* begins!
- **"Keep it simple, stupid!"** Avoid complex features and full-scale type checking.
- **FAIL FAST**! Leverage the built-in [Node](./core_abstraction/node.md) retry and fallback mechanisms to handle failures gracefully. This helps you quickly identify weak points in the system.
- Add logging throughout the code to facilitate debugging.
7. **Optimization**:
- **Use Intuition**: For a quick initial evaluation, human intuition is often a good start.
- **Redesign Flow (Back to Step 3)**: Consider breaking down tasks further, introducing agentic decisions, or better managing input contexts.
- If your flow design is already solid, move on to micro-optimizations:
- **Prompt Engineering**: Use clear, specific instructions with examples to reduce ambiguity.
- **In-Context Learning**: Provide robust examples for tasks that are difficult to specify with instructions alone.
- > **You'll likely iterate a lot!** Expect to repeat Steps 3–6 hundreds of times.
>
>
{: .best-practice }
8. **Reliability**
- **Node Retries**: Add checks in the node `exec` to ensure outputs meet requirements, and consider increasing `max_retries` and `wait` times.
- **Logging and Visualization**: Maintain logs of all attempts and visualize node results for easier debugging.
- **Self-Evaluation**: Add a separate node (powered by an LLM) to review outputs when results are uncertain.
## Example LLM Project File Structure
```
my_project/
├── main.py
├── nodes.py
├── flow.py
├── utils/
│ ├── __init__.py
│ ├── call_llm.py
│ └── search_web.py
├── requirements.txt
└── docs/
└── design.md
```
- **`requirements.txt`**: Lists the Python dependencies for the project.
```
PyYAML
pocketflow
```
- **`docs/design.md`**: Contains project documentation for each step above. This should be *high-level* and *no-code*.
~~~
# Design Doc: Your Project Name
> Please DON'T remove notes for AI
## Requirements
> Notes for AI: Keep it simple and clear.
> If the requirements are abstract, write concrete user stories
## Flow Design
> Notes for AI:
> 1. Consider the design patterns of agent, map-reduce, rag, and workflow. Apply them if they fit.
> 2. Present a concise, high-level description of the workflow.
### Applicable Design Pattern:
1. Map the file summary into chunks, then reduce these chunks into a final summary.
2. Agentic file finder
- *Context*: The entire summary of the file
- *Action*: Find the file
### Flow high-level Design:
1. **First Node**: This node is for ...
2. **Second Node**: This node is for ...
3. **Third Node**: This node is for ...
```mermaid
flowchart TD
firstNode[First Node] --> secondNode[Second Node]
secondNode --> thirdNode[Third Node]
```
## Utility Functions
> Notes for AI:
> 1. Understand the utility function definition thoroughly by reviewing the doc.
> 2. Include only the necessary utility functions, based on nodes in the flow.
1. **Call LLM** (`utils/call_llm.py`)
- *Input*: prompt (str)
- *Output*: response (str)
- Generally used by most nodes for LLM tasks
2. **Embedding** (`utils/get_embedding.py`)
- *Input*: str
- *Output*: a vector of 3072 floats
- Used by the second node to embed text
## Node Design
### Shared Store
> Notes for AI: Try to minimize data redundancy
The shared store structure is organized as follows:
```python
shared = {
"key": "value"
}
```
### Node Steps
> Notes for AI: Carefully decide whether to use Batch/Async Node/Flow.
1. First Node
- *Purpose*: Provide a short explanation of the node’s function
- *Type*: Decide between Regular, Batch, or Async
- *Steps*:
- *prep*: Read "key" from the shared store
- *exec*: Call the utility function
- *post*: Write "key" to the shared store
2. Second Node
...
~~~
- **`utils/`**: Contains all utility functions.
- It's recommended to dedicate one Python file to each API call, for example `call_llm.py` or `search_web.py`.
- Each file should also include a `main()` function to try that API call
```python
from google import genai
import os
def call_llm(prompt: str) -> str:
client = genai.Client(
api_key=os.getenv("GEMINI_API_KEY", ""),
)
model = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
response = client.models.generate_content(model=model, contents=[prompt])
return response.text
if __name__ == "__main__":
test_prompt = "Hello, how are you?"
# First call - should hit the API
print("Making call...")
response1 = call_llm(test_prompt, use_cache=False)
print(f"Response: {response1}")
```
- **`nodes.py`**: Contains all the node definitions.
```python
# nodes.py
from pocketflow import Node
from utils.call_llm import call_llm
class GetQuestionNode(Node):
def exec(self, _):
# Get question directly from user input
user_question = input("Enter your question: ")
return user_question
def post(self, shared, prep_res, exec_res):
# Store the user's question
shared["question"] = exec_res
return "default" # Go to the next node
class AnswerNode(Node):
def prep(self, shared):
# Read question from shared
return shared["question"]
def exec(self, question):
# Call LLM to get the answer
return call_llm(question)
def post(self, shared, prep_res, exec_res):
# Store the answer in shared
shared["answer"] = exec_res
```
- **`flow.py`**: Implements functions that create flows by importing node definitions and connecting them.
```python
# flow.py
from pocketflow import Flow
from nodes import GetQuestionNode, AnswerNode
def create_qa_flow():
"""Create and return a question-answering flow."""
# Create nodes
get_question_node = GetQuestionNode()
answer_node = AnswerNode()
# Connect nodes in sequence
get_question_node >> answer_node
# Create flow starting with input node
return Flow(start=get_question_node)
```
- **`main.py`**: Serves as the project's entry point.
```python
# main.py
from flow import create_qa_flow
# Example main function
# Please replace this with your own main function
def main():
shared = {
"question": None, # Will be populated by GetQuestionNode from user input
"answer": None # Will be populated by AnswerNode
}
# Create the flow and run it
qa_flow = create_qa_flow()
qa_flow.run(shared)
print(f"Question: {shared['question']}")
print(f"Answer: {shared['answer']}")
if __name__ == "__main__":
main()
```
================================================
File: docs/index.md
================================================
---
layout: default
title: "Home"
nav_order: 1
---
# Pocket Flow
A [100-line](https://github.com/the-pocket/PocketFlow/blob/main/pocketflow/__init__.py) minimalist LLM framework for *Agents, Task Decomposition, RAG, etc*.
- **Lightweight**: Just the core graph abstraction in 100 lines. ZERO dependencies, and vendor lock-in.
- **Expressive**: Everything you love from larger frameworks—([Multi-](./design_pattern/multi_agent.html))[Agents](./design_pattern/agent.html), [Workflow](./design_pattern/workflow.html), [RAG](./design_pattern/rag.html), and more.
- **Agentic-Coding**: Intuitive enough for AI agents to help humans build complex LLM applications.
## Core Abstraction
We model the LLM workflow as a **Graph + Shared Store**:
- [Node](./core_abstraction/node.md) handles simple (LLM) tasks.
- [Flow](./core_abstraction/flow.md) connects nodes through **Actions** (labeled edges).
- [Shared Store](./core_abstraction/communication.md) enables communication between nodes within flows.
- [Batch](./core_abstraction/batch.md) nodes/flows allow for data-intensive tasks.
- [Async](./core_abstraction/async.md) nodes/flows allow waiting for asynchronous tasks.
- [(Advanced) Parallel](./core_abstraction/parallel.md) nodes/flows handle I/O-bound tasks.
## Design Pattern
From there, it’s easy to implement popular design patterns:
- [Agent](./design_pattern/agent.md) autonomously makes decisions.
- [Workflow](./design_pattern/workflow.md) chains multiple tasks into pipelines.
- [RAG](./design_pattern/rag.md) integrates data retrieval with generation.
- [Map Reduce](./design_pattern/mapreduce.md) splits data tasks into Map and Reduce steps.
- [Structured Output](./design_pattern/structure.md) formats outputs consistently.
- [(Advanced) Multi-Agents](./design_pattern/multi_agent.md) coordinate multiple agents.
## Utility Function
We **do not** provide built-in utilities. Instead, we offer *examples*—please *implement your own*:
- [LLM Wrapper](./utility_function/llm.md)
- [Viz and Debug](./utility_function/viz.md)
- [Web Search](./utility_function/websearch.md)
- [Chunking](./utility_function/chunking.md)
- [Embedding](./utility_function/embedding.md)
- [Vector Databases](./utility_function/vector.md)
- [Text-to-Speech](./utility_function/text_to_speech.md)
**Why not built-in?**: I believe it's a *bad practice* for vendor-specific APIs in a general framework:
- *API Volatility*: Frequent changes lead to heavy maintenance for hardcoded APIs.
- *Flexibility*: You may want to switch vendors, use fine-tuned models, or run them locally.
- *Optimizations*: Prompt caching, batching, and streaming are easier without vendor lock-in.
## Ready to build your Apps?
Check out [Agentic Coding Guidance](./guide.md), the fastest way to develop LLM projects with Pocket Flow!
================================================
File: docs/core_abstraction/async.md
================================================
---
layout: default
title: "(Advanced) Async"
parent: "Core Abstraction"
nav_order: 5
---
# (Advanced) Async
**Async** Nodes implement `prep_async()`, `exec_async()`, `exec_fallback_async()`, and/or `post_async()`. This is useful for:
1. **prep_async()**: For *fetching/reading data (files, APIs, DB)* in an I/O-friendly way.
2. **exec_async()**: Typically used for async LLM calls.
3. **post_async()**: For *awaiting user feedback*, *coordinating across multi-agents* or any additional async steps after `exec_async()`.
**Note**: `AsyncNode` must be wrapped in `AsyncFlow`. `AsyncFlow` can also include regular (sync) nodes.
### Example
```python
class SummarizeThenVerify(AsyncNode):
async def prep_async(self, shared):
# Example: read a file asynchronously
doc_text = await read_file_async(shared["doc_path"])
return doc_text
async def exec_async(self, prep_res):
# Example: async LLM call
summary = await call_llm_async(f"Summarize: {prep_res}")
return summary
async def post_async(self, shared, prep_res, exec_res):
# Example: wait for user feedback
decision = await gather_user_feedback(exec_res)
if decision == "approve":
shared["summary"] = exec_res
return "approve"
return "deny"
summarize_node = SummarizeThenVerify()
final_node = Finalize()
# Define transitions
summarize_node - "approve" >> final_node
summarize_node - "deny" >> summarize_node # retry
flow = AsyncFlow(start=summarize_node)
async def main():
shared = {"doc_path": "document.txt"}
await flow.run_async(shared)
print("Final Summary:", shared.get("summary"))
asyncio.run(main())
```
================================================
File: docs/core_abstraction/batch.md
================================================
---
layout: default
title: "Batch"
parent: "Core Abstraction"
nav_order: 4
---
# Batch
**Batch** makes it easier to handle large inputs in one Node or **rerun** a Flow multiple times. Example use cases:
- **Chunk-based** processing (e.g., splitting large texts).
- **Iterative** processing over lists of input items (e.g., user queries, files, URLs).
## 1. BatchNode
A **BatchNode** extends `Node` but changes `prep()` and `exec()`:
- **`prep(shared)`**: returns an **iterable** (e.g., list, generator).
- **`exec(item)`**: called **once** per item in that iterable.
- **`post(shared, prep_res, exec_res_list)`**: after all items are processed, receives a **list** of results (`exec_res_list`) and returns an **Action**.
### Example: Summarize a Large File
```python
class MapSummaries(BatchNode):
def prep(self, shared):
# Suppose we have a big file; chunk it
content = shared["data"]
chunk_size = 10000
chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
return chunks
def exec(self, chunk):
prompt = f"Summarize this chunk in 10 words: {chunk}"
summary = call_llm(prompt)
return summary
def post(self, shared, prep_res, exec_res_list):
combined = "\n".join(exec_res_list)
shared["summary"] = combined
return "default"
map_summaries = MapSummaries()
flow = Flow(start=map_summaries)
flow.run(shared)
```
---
## 2. BatchFlow
A **BatchFlow** runs a **Flow** multiple times, each time with different `params`. Think of it as a loop that replays the Flow for each parameter set.
### Example: Summarize Many Files
```python
class SummarizeAllFiles(BatchFlow):
def prep(self, shared):
# Return a list of param dicts (one per file)
filenames = list(shared["data"].keys()) # e.g., ["file1.txt", "file2.txt", ...]
return [{"filename": fn} for fn in filenames]
# Suppose we have a per-file Flow (e.g., load_file >> summarize >> reduce):
summarize_file = SummarizeFile(start=load_file)
# Wrap that flow into a BatchFlow:
summarize_all_files = SummarizeAllFiles(start=summarize_file)
summarize_all_files.run(shared)
```
### Under the Hood
1. `prep(shared)` returns a list of param dicts—e.g., `[{filename: "file1.txt"}, {filename: "file2.txt"}, ...]`.
2. The **BatchFlow** loops through each dict. For each one:
- It merges the dict with the BatchFlow’s own `params`.
- It calls `flow.run(shared)` using the merged result.
3. This means the sub-Flow is run **repeatedly**, once for every param dict.
---
## 3. Nested or Multi-Level Batches
You can nest a **BatchFlow** in another **BatchFlow**. For instance:
- **Outer** batch: returns a list of diretory param dicts (e.g., `{"directory": "/pathA"}`, `{"directory": "/pathB"}`, ...).
- **Inner** batch: returning a list of per-file param dicts.
At each level, **BatchFlow** merges its own param dict with the parent’s. By the time you reach the **innermost** node, the final `params` is the merged result of **all** parents in the chain. This way, a nested structure can keep track of the entire context (e.g., directory + file name) at once.
```python
class FileBatchFlow(BatchFlow):
def prep(self, shared):
directory = self.params["directory"]
# e.g., files = ["file1.txt", "file2.txt", ...]
files = [f for f in os.listdir(directory) if f.endswith(".txt")]
return [{"filename": f} for f in files]
class DirectoryBatchFlow(BatchFlow):
def prep(self, shared):
directories = [ "/path/to/dirA", "/path/to/dirB"]
return [{"directory": d} for d in directories]
# MapSummaries have params like {"directory": "/path/to/dirA", "filename": "file1.txt"}
inner_flow = FileBatchFlow(start=MapSummaries())
outer_flow = DirectoryBatchFlow(start=inner_flow)
```
================================================
File: docs/core_abstraction/communication.md
================================================
---
layout: default
title: "Communication"
parent: "Core Abstraction"
nav_order: 3
---
# Communication
Nodes and Flows **communicate** in 2 ways:
1. **Shared Store (for almost all the cases)**
- A global data structure (often an in-mem dict) that all nodes can read ( `prep()`) and write (`post()`).
- Great for data results, large content, or anything multiple nodes need.
- You shall design the data structure and populate it ahead.
- > **Separation of Concerns:** Use `Shared Store` for almost all cases to separate *Data Schema* from *Compute Logic*! This approach is both flexible and easy to manage, resulting in more maintainable code. `Params` is more a syntax sugar for [Batch](./batch.md).
{: .best-practice }
2. **Params (only for [Batch](./batch.md))**
- Each node has a local, ephemeral `params` dict passed in by the **parent Flow**, used as an identifier for tasks. Parameter keys and values shall be **immutable**.
- Good for identifiers like filenames or numeric IDs, in Batch mode.
If you know memory management, think of the **Shared Store** like a **heap** (shared by all function calls), and **Params** like a **stack** (assigned by the caller).
---
## 1. Shared Store
### Overview
A shared store is typically an in-mem dictionary, like:
```python
shared = {"data": {}, "summary": {}, "config": {...}, ...}
```
It can also contain local file handlers, DB connections, or a combination for persistence. We recommend deciding the data structure or DB schema first based on your app requirements.
### Example
```python
class LoadData(Node):
def post(self, shared, prep_res, exec_res):
# We write data to shared store
shared["data"] = "Some text content"
return None
class Summarize(Node):
def prep(self, shared):
# We read data from shared store
return shared["data"]
def exec(self, prep_res):
# Call LLM to summarize
prompt = f"Summarize: {prep_res}"
summary = call_llm(prompt)
return summary
def post(self, shared, prep_res, exec_res):
# We write summary to shared store
shared["summary"] = exec_res
return "default"
load_data = LoadData()
summarize = Summarize()
load_data >> summarize
flow = Flow(start=load_data)
shared = {}
flow.run(shared)
```
Here:
- `LoadData` writes to `shared["data"]`.
- `Summarize` reads from `shared["data"]`, summarizes, and writes to `shared["summary"]`.
---
## 2. Params
**Params** let you store *per-Node* or *per-Flow* config that doesn't need to live in the shared store. They are:
- **Immutable** during a Node's run cycle (i.e., they don't change mid-`prep->exec->post`).
- **Set** via `set_params()`.
- **Cleared** and updated each time a parent Flow calls it.
> Only set the uppermost Flow params because others will be overwritten by the parent Flow.
>
> If you need to set child node params, see [Batch](./batch.md).
{: .warning }
Typically, **Params** are identifiers (e.g., file name, page number). Use them to fetch the task you assigned or write to a specific part of the shared store.
### Example
```python
# 1) Create a Node that uses params
class SummarizeFile(Node):
def prep(self, shared):
# Access the node's param
filename = self.params["filename"]
return shared["data"].get(filename, "")
def exec(self, prep_res):
prompt = f"Summarize: {prep_res}"
return call_llm(prompt)
def post(self, shared, prep_res, exec_res):
filename = self.params["filename"]
shared["summary"][filename] = exec_res
return "default"
# 2) Set params
node = SummarizeFile()
# 3) Set Node params directly (for testing)
node.set_params({"filename": "doc1.txt"})
node.run(shared)
# 4) Create Flow
flow = Flow(start=node)
# 5) Set Flow params (overwrites node params)
flow.set_params({"filename": "doc2.txt"})
flow.run(shared) # The node summarizes doc2, not doc1
```
================================================
File: docs/core_abstraction/flow.md
================================================
---
layout: default
title: "Flow"
parent: "Core Abstraction"
nav_order: 2
---
# Flow
A **Flow** orchestrates a graph of Nodes. You can chain Nodes in a sequence or create branching depending on the **Actions** returned from each Node's `post()`.
## 1. Action-based Transitions
Each Node's `post()` returns an **Action** string. By default, if `post()` doesn't return anything, we treat that as `"default"`.
You define transitions with the syntax:
1. **Basic default transition**: `node_a >> node_b`
This means if `node_a.post()` returns `"default"`, go to `node_b`.
(Equivalent to `node_a - "default" >> node_b`)
2. **Named action transition**: `node_a - "action_name" >> node_b`
This means if `node_a.post()` returns `"action_name"`, go to `node_b`.
It's possible to create loops, branching, or multi-step flows.
## 2. Creating a Flow
A **Flow** begins with a **start** node. You call `Flow(start=some_node)` to specify the entry point. When you call `flow.run(shared)`, it executes the start node, looks at its returned Action from `post()`, follows the transition, and continues until there's no next node.
### Example: Simple Sequence
Here's a minimal flow of two nodes in a chain:
```python
node_a >> node_b
flow = Flow(start=node_a)
flow.run(shared)
```
- When you run the flow, it executes `node_a`.
- Suppose `node_a.post()` returns `"default"`.
- The flow then sees `"default"` Action is linked to `node_b` and runs `node_b`.
- `node_b.post()` returns `"default"` but we didn't define `node_b >> something_else`. So the flow ends there.
### Example: Branching & Looping
Here's a simple expense approval flow that demonstrates branching and looping. The `ReviewExpense` node can return three possible Actions:
- `"approved"`: expense is approved, move to payment processing
- `"needs_revision"`: expense needs changes, send back for revision
- `"rejected"`: expense is denied, finish the process
We can wire them like this:
```python
# Define the flow connections
review - "approved" >> payment # If approved, process payment
review - "needs_revision" >> revise # If needs changes, go to revision
review - "rejected" >> finish # If rejected, finish the process
revise >> review # After revision, go back for another review
payment >> finish # After payment, finish the process
flow = Flow(start=review)
```
Let's see how it flows:
1. If `review.post()` returns `"approved"`, the expense moves to the `payment` node
2. If `review.post()` returns `"needs_revision"`, it goes to the `revise` node, which then loops back to `review`
3. If `review.post()` returns `"rejected"`, it moves to the `finish` node and stops
```mermaid
flowchart TD
review[Review Expense] -->|approved| payment[Process Payment]
review -->|needs_revision| revise[Revise Report]
review -->|rejected| finish[Finish Process]
revise --> review
payment --> finish
```
### Running Individual Nodes vs. Running a Flow
- `node.run(shared)`: Just runs that node alone (calls `prep->exec->post()`), returns an Action.
- `flow.run(shared)`: Executes from the start node, follows Actions to the next node, and so on until the flow can't continue.
> `node.run(shared)` **does not** proceed to the successor.
> This is mainly for debugging or testing a single node.
>
> Always use `flow.run(...)` in production to ensure the full pipeline runs correctly.
{: .warning }
## 3. Nested Flows
A **Flow** can act like a Node, which enables powerful composition patterns. This means you can:
1. Use a Flow as a Node within another Flow's transitions.
2. Combine multiple smaller Flows into a larger Flow for reuse.
3. Node `params` will be a merging of **all** parents' `params`.
### Flow's Node Methods
A **Flow** is also a **Node**, so it will run `prep()` and `post()`. However:
- It **won't** run `exec()`, as its main logic is to orchestrate its nodes.
- `post()` always receives `None` for `exec_res` and should instead get the flow execution results from the shared store.
### Basic Flow Nesting
Here's how to connect a flow to another node:
```python
# Create a sub-flow
node_a >> node_b
subflow = Flow(start=node_a)
# Connect it to another node
subflow >> node_c
# Create the parent flow
parent_flow = Flow(start=subflow)
```
When `parent_flow.run()` executes:
1. It starts `subflow`
2. `subflow` runs through its nodes (`node_a->node_b`)
3. After `subflow` completes, execution continues to `node_c`
### Example: Order Processing Pipeline
Here's a practical example that breaks down order processing into nested flows:
```python
# Payment processing sub-flow
validate_payment >> process_payment >> payment_confirmation
payment_flow = Flow(start=validate_payment)
# Inventory sub-flow
check_stock >> reserve_items >> update_inventory
inventory_flow = Flow(start=check_stock)
# Shipping sub-flow
create_label >> assign_carrier >> schedule_pickup
shipping_flow = Flow(start=create_label)
# Connect the flows into a main order pipeline
payment_flow >> inventory_flow >> shipping_flow
# Create the master flow
order_pipeline = Flow(start=payment_flow)
# Run the entire pipeline
order_pipeline.run(shared_data)
```
This creates a clean separation of concerns while maintaining a clear execution path:
```mermaid
flowchart LR
subgraph order_pipeline[Order Pipeline]
subgraph paymentFlow["Payment Flow"]
A[Validate Payment] --> B[Process Payment] --> C[Payment Confirmation]
end
subgraph inventoryFlow["Inventory Flow"]
D[Check Stock] --> E[Reserve Items] --> F[Update Inventory]
end
subgraph shippingFlow["Shipping Flow"]
G[Create Label] --> H[Assign Carrier] --> I[Schedule Pickup]
end
paymentFlow --> inventoryFlow
inventoryFlow --> shippingFlow
end
```
================================================
File: docs/core_abstraction/node.md
================================================
---
layout: default
title: "Node"
parent: "Core Abstraction"
nav_order: 1
---
# Node
A **Node** is the smallest building block. Each Node has 3 steps `prep->exec->post`:
1. `prep(shared)`
- **Read and preprocess data** from `shared` store.
- Examples: *query DB, read files, or serialize data into a string*.
- Return `prep_res`, which is used by `exec()` and `post()`.
2. `exec(prep_res)`
- **Execute compute logic**, with optional retries and error handling (below).
- Examples: *(mostly) LLM calls, remote APIs, tool use*.
- ⚠️ This shall be only for compute and **NOT** access `shared`.
- ⚠️ If retries enabled, ensure idempotent implementation.
- ⚠️ Defer exception handling to the Node's built-in retry mechanism.
- Return `exec_res`, which is passed to `post()`.
3. `post(shared, prep_res, exec_res)`
- **Postprocess and write data** back to `shared`.
- Examples: *update DB, change states, log results*.
- **Decide the next action** by returning a *string* (`action = "default"` if *None*).
> **Why 3 steps?** To enforce the principle of *separation of concerns*. The data storage and data processing are operated separately.
>
> All steps are *optional*. E.g., you can only implement `prep` and `post` if you just need to process data.
{: .note }
### Fault Tolerance & Retries
You can **retry** `exec()` if it raises an exception via two parameters when define the Node:
- `max_retries` (int): Max times to run `exec()`. The default is `1` (**no** retry).
- `wait` (int): The time to wait (in **seconds**) before next retry. By default, `wait=0` (no waiting).
`wait` is helpful when you encounter rate-limits or quota errors from your LLM provider and need to back off.
```python
my_node = SummarizeFile(max_retries=3, wait=10)
```
When an exception occurs in `exec()`, the Node automatically retries until:
- It either succeeds, or
- The Node has retried `max_retries - 1` times already and fails on the last attempt.
You can get the current retry times (0-based) from `self.cur_retry`.
```python
class RetryNode(Node):
def exec(self, prep_res):
print(f"Retry {self.cur_retry} times")
raise Exception("Failed")
```
### Graceful Fallback
To **gracefully handle** the exception (after all retries) rather than raising it, override:
```python
def exec_fallback(self, prep_res, exc):
raise exc
```
By default, it just re-raises exception. But you can return a fallback result instead, which becomes the `exec_res` passed to `post()`.
### Example: Summarize file
```python
class SummarizeFile(Node):
def prep(self, shared):
return shared["data"]
def exec(self, prep_res):
if not prep_res:
return "Empty file content"
prompt = f"Summarize this text in 10 words: {prep_res}"
summary = call_llm(prompt) # might fail
return summary
def exec_fallback(self, prep_res, exc):
# Provide a simple fallback instead of crashing
return "There was an error processing your request."
def post(self, shared, prep_res, exec_res):
shared["summary"] = exec_res
# Return "default" by not returning
summarize_node = SummarizeFile(max_retries=3)
# node.run() calls prep->exec->post
# If exec() fails, it retries up to 3 times before calling exec_fallback()
action_result = summarize_node.run(shared)
print("Action returned:", action_result) # "default"
print("Summary stored:", shared["summary"])
```
================================================
File: docs/core_abstraction/parallel.md
================================================
---
layout: default
title: "(Advanced) Parallel"
parent: "Core Abstraction"
nav_order: 6
---
# (Advanced) Parallel
**Parallel** Nodes and Flows let you run multiple **Async** Nodes and Flows **concurrently**—for example, summarizing multiple texts at once. This can improve performance by overlapping I/O and compute.
> Because of Python’s GIL, parallel nodes and flows can’t truly parallelize CPU-bound tasks (e.g., heavy numerical computations). However, they excel at overlapping I/O-bound work—like LLM calls, database queries, API requests, or file I/O.
{: .warning }
> - **Ensure Tasks Are Independent**: If each item depends on the output of a previous item, **do not** parallelize.
>
> - **Beware of Rate Limits**: Parallel calls can **quickly** trigger rate limits on LLM services. You may need a **throttling** mechanism (e.g., semaphores or sleep intervals).
>
> - **Consider Single-Node Batch APIs**: Some LLMs offer a **batch inference** API where you can send multiple prompts in a single call. This is more complex to implement but can be more efficient than launching many parallel requests and mitigates rate limits.
{: .best-practice }
## AsyncParallelBatchNode
Like **AsyncBatchNode**, but run `exec_async()` in **parallel**:
```python
class ParallelSummaries(AsyncParallelBatchNode):
async def prep_async(self, shared):
# e.g., multiple texts
return shared["texts"]
async def exec_async(self, text):
prompt = f"Summarize: {text}"
return await call_llm_async(prompt)
async def post_async(self, shared, prep_res, exec_res_list):
shared["summary"] = "\n\n".join(exec_res_list)
return "default"
node = ParallelSummaries()
flow = AsyncFlow(start=node)
```
## AsyncParallelBatchFlow
Parallel version of **BatchFlow**. Each iteration of the sub-flow runs **concurrently** using different parameters:
```python
class SummarizeMultipleFiles(AsyncParallelBatchFlow):
async def prep_async(self, shared):
return [{"filename": f} for f in shared["files"]]
sub_flow = AsyncFlow(start=LoadAndSummarizeFile())
parallel_flow = SummarizeMultipleFiles(start=sub_flow)
await parallel_flow.run_async(shared)
```
================================================
File: docs/design_pattern/agent.md
================================================
---
layout: default
title: "Agent"
parent: "Design Pattern"
nav_order: 1
---
# Agent
Agent is a powerful design pattern in which nodes can take dynamic actions based on the context.
## Implement Agent with Graph
1. **Context and Action:** Implement nodes that supply context and perform actions.
2. **Branching:** Use branching to connect each action node to an agent node. Use action to allow the agent to direct the [flow](../core_abstraction/flow.md) between nodes—and potentially loop back for multi-step.
3. **Agent Node:** Provide a prompt to decide action—for example:
```python
f"""
### CONTEXT
Task: {task_description}
Previous Actions: {previous_actions}
Current State: {current_state}
### ACTION SPACE
[1] search
Description: Use web search to get results
Parameters:
- query (str): What to search for
[2] answer
Description: Conclude based on the results
Parameters:
- result (str): Final answer to provide
### NEXT ACTION
Decide the next action based on the current context and available action space.
Return your response in the following format:
```yaml
thinking: |
action:
parameters:
:
```"""
```
The core of building **high-performance** and **reliable** agents boils down to:
1. **Context Management:** Provide *relevant, minimal context.* For example, rather than including an entire chat history, retrieve the most relevant via [RAG](./rag.md). Even with larger context windows, LLMs still fall victim to ["lost in the middle"](https://arxiv.org/abs/2307.03172), overlooking mid-prompt content.
2. **Action Space:** Provide *a well-structured and unambiguous* set of actions—avoiding overlap like separate `read_databases` or `read_csvs`. Instead, import CSVs into the database.
## Example Good Action Design
- **Incremental:** Feed content in manageable chunks (500 lines or 1 page) instead of all at once.
- **Overview-zoom-in:** First provide high-level structure (table of contents, summary), then allow drilling into details (raw texts).
- **Parameterized/Programmable:** Instead of fixed actions, enable parameterized (columns to select) or programmable (SQL queries) actions, for example, to read CSV files.
- **Backtracking:** Let the agent undo the last step instead of restarting entirely, preserving progress when encountering errors or dead ends.
## Example: Search Agent
This agent:
1. Decides whether to search or answer
2. If searches, loops back to decide if more search needed
3. Answers when enough context gathered
```python
class DecideAction(Node):
def prep(self, shared):
context = shared.get("context", "No previous search")
query = shared["query"]
return query, context
def exec(self, inputs):
query, context = inputs
prompt = f"""
Given input: {query}
Previous search results: {context}
Should I: 1) Search web for more info 2) Answer with current knowledge
Output in yaml:
```yaml
action: search/answer
reason: why this action
search_term: search phrase if action is search
```"""
resp = call_llm(prompt)
yaml_str = resp.split("```yaml")[1].split("```")[0].strip()
result = yaml.safe_load(yaml_str)
assert isinstance(result, dict)
assert "action" in result
assert "reason" in result
assert result["action"] in ["search", "answer"]
if result["action"] == "search":
assert "search_term" in result
return result
def post(self, shared, prep_res, exec_res):
if exec_res["action"] == "search":
shared["search_term"] = exec_res["search_term"]
return exec_res["action"]
class SearchWeb(Node):
def prep(self, shared):
return shared["search_term"]
def exec(self, search_term):
return search_web(search_term)
def post(self, shared, prep_res, exec_res):
prev_searches = shared.get("context", [])
shared["context"] = prev_searches + [
{"term": shared["search_term"], "result": exec_res}
]
return "decide"
class DirectAnswer(Node):
def prep(self, shared):
return shared["query"], shared.get("context", "")
def exec(self, inputs):
query, context = inputs
return call_llm(f"Context: {context}\nAnswer: {query}")
def post(self, shared, prep_res, exec_res):
print(f"Answer: {exec_res}")
shared["answer"] = exec_res
# Connect nodes
decide = DecideAction()
search = SearchWeb()
answer = DirectAnswer()
decide - "search" >> search
decide - "answer" >> answer
search - "decide" >> decide # Loop back
flow = Flow(start=decide)
flow.run({"query": "Who won the Nobel Prize in Physics 2024?"})
```
================================================
File: docs/design_pattern/mapreduce.md
================================================
---
layout: default
title: "Map Reduce"
parent: "Design Pattern"
nav_order: 4
---
# Map Reduce
MapReduce is a design pattern suitable when you have either:
- Large input data (e.g., multiple files to process), or
- Large output data (e.g., multiple forms to fill)
and there is a logical way to break the task into smaller, ideally independent parts.
You first break down the task using [BatchNode](../core_abstraction/batch.md) in the map phase, followed by aggregation in the reduce phase.
### Example: Document Summarization
```python
class SummarizeAllFiles(BatchNode):
def prep(self, shared):
files_dict = shared["files"] # e.g. 10 files
return list(files_dict.items()) # [("file1.txt", "aaa..."), ("file2.txt", "bbb..."), ...]
def exec(self, one_file):
filename, file_content = one_file
summary_text = call_llm(f"Summarize the following file:\n{file_content}")
return (filename, summary_text)
def post(self, shared, prep_res, exec_res_list):
shared["file_summaries"] = dict(exec_res_list)
class CombineSummaries(Node):
def prep(self, shared):
return shared["file_summaries"]
def exec(self, file_summaries):
# format as: "File1: summary\nFile2: summary...\n"
text_list = []
for fname, summ in file_summaries.items():
text_list.append(f"{fname} summary:\n{summ}\n")
big_text = "\n---\n".join(text_list)
return call_llm(f"Combine these file summaries into one final summary:\n{big_text}")
def post(self, shared, prep_res, final_summary):
shared["all_files_summary"] = final_summary
batch_node = SummarizeAllFiles()
combine_node = CombineSummaries()
batch_node >> combine_node
flow = Flow(start=batch_node)
shared = {
"files": {
"file1.txt": "Alice was beginning to get very tired of sitting by her sister...",
"file2.txt": "Some other interesting text ...",
# ...
}
}
flow.run(shared)
print("Individual Summaries:", shared["file_summaries"])
print("\nFinal Summary:\n", shared["all_files_summary"])
```
================================================
File: docs/design_pattern/rag.md
================================================
---
layout: default
title: "RAG"
parent: "Design Pattern"
nav_order: 3
---
# RAG (Retrieval Augmented Generation)
For certain LLM tasks like answering questions, providing relevant context is essential. One common architecture is a **two-stage** RAG pipeline:
1. **Offline stage**: Preprocess and index documents ("building the index").
2. **Online stage**: Given a question, generate answers by retrieving the most relevant context.
---
## Stage 1: Offline Indexing
We create three Nodes:
1. `ChunkDocs` – [chunks](../utility_function/chunking.md) raw text.
2. `EmbedDocs` – [embeds](../utility_function/embedding.md) each chunk.
3. `StoreIndex` – stores embeddings into a [vector database](../utility_function/vector.md).
```python
class ChunkDocs(BatchNode):
def prep(self, shared):
# A list of file paths in shared["files"]. We process each file.
return shared["files"]
def exec(self, filepath):
# read file content. In real usage, do error handling.
with open(filepath, "r", encoding="utf-8") as f:
text = f.read()
# chunk by 100 chars each
chunks = []
size = 100
for i in range(0, len(text), size):
chunks.append(text[i : i + size])
return chunks
def post(self, shared, prep_res, exec_res_list):
# exec_res_list is a list of chunk-lists, one per file.
# flatten them all into a single list of chunks.
all_chunks = []
for chunk_list in exec_res_list:
all_chunks.extend(chunk_list)
shared["all_chunks"] = all_chunks
class EmbedDocs(BatchNode):
def prep(self, shared):
return shared["all_chunks"]
def exec(self, chunk):
return get_embedding(chunk)
def post(self, shared, prep_res, exec_res_list):
# Store the list of embeddings.
shared["all_embeds"] = exec_res_list
print(f"Total embeddings: {len(exec_res_list)}")
class StoreIndex(Node):
def prep(self, shared):
# We'll read all embeds from shared.
return shared["all_embeds"]
def exec(self, all_embeds):
# Create a vector index (faiss or other DB in real usage).
index = create_index(all_embeds)
return index
def post(self, shared, prep_res, index):
shared["index"] = index
# Wire them in sequence
chunk_node = ChunkDocs()
embed_node = EmbedDocs()
store_node = StoreIndex()
chunk_node >> embed_node >> store_node
OfflineFlow = Flow(start=chunk_node)
```
Usage example:
```python
shared = {
"files": ["doc1.txt", "doc2.txt"], # any text files
}
OfflineFlow.run(shared)
```
---
## Stage 2: Online Query & Answer
We have 3 nodes:
1. `EmbedQuery` – embeds the user’s question.
2. `RetrieveDocs` – retrieves top chunk from the index.
3. `GenerateAnswer` – calls the LLM with the question + chunk to produce the final answer.
```python
class EmbedQuery(Node):
def prep(self, shared):
return shared["question"]
def exec(self, question):
return get_embedding(question)
def post(self, shared, prep_res, q_emb):
shared["q_emb"] = q_emb
class RetrieveDocs(Node):
def prep(self, shared):
# We'll need the query embedding, plus the offline index/chunks
return shared["q_emb"], shared["index"], shared["all_chunks"]
def exec(self, inputs):
q_emb, index, chunks = inputs
I, D = search_index(index, q_emb, top_k=1)
best_id = I[0][0]
relevant_chunk = chunks[best_id]
return relevant_chunk
def post(self, shared, prep_res, relevant_chunk):
shared["retrieved_chunk"] = relevant_chunk
print("Retrieved chunk:", relevant_chunk[:60], "...")
class GenerateAnswer(Node):
def prep(self, shared):
return shared["question"], shared["retrieved_chunk"]
def exec(self, inputs):
question, chunk = inputs
prompt = f"Question: {question}\nContext: {chunk}\nAnswer:"
return call_llm(prompt)
def post(self, shared, prep_res, answer):
shared["answer"] = answer
print("Answer:", answer)
embed_qnode = EmbedQuery()
retrieve_node = RetrieveDocs()
generate_node = GenerateAnswer()
embed_qnode >> retrieve_node >> generate_node
OnlineFlow = Flow(start=embed_qnode)
```
Usage example:
```python
# Suppose we already ran OfflineFlow and have:
# shared["all_chunks"], shared["index"], etc.
shared["question"] = "Why do people like cats?"
OnlineFlow.run(shared)
# final answer in shared["answer"]
```
================================================
File: docs/design_pattern/structure.md
================================================
---
layout: default
title: "Structured Output"
parent: "Design Pattern"
nav_order: 5
---
# Structured Output
In many use cases, you may want the LLM to output a specific structure, such as a list or a dictionary with predefined keys.
There are several approaches to achieve a structured output:
- **Prompting** the LLM to strictly return a defined structure.
- Using LLMs that natively support **schema enforcement**.
- **Post-processing** the LLM's response to extract structured content.
In practice, **Prompting** is simple and reliable for modern LLMs.
### Example Use Cases
- Extracting Key Information
```yaml
product:
name: Widget Pro
price: 199.99
description: |
A high-quality widget designed for professionals.
Recommended for advanced users.
```
- Summarizing Documents into Bullet Points
```yaml
summary:
- This product is easy to use.
- It is cost-effective.
- Suitable for all skill levels.
```
- Generating Configuration Files
```yaml
server:
host: 127.0.0.1
port: 8080
ssl: true
```
## Prompt Engineering
When prompting the LLM to produce **structured** output:
1. **Wrap** the structure in code fences (e.g., `yaml`).
2. **Validate** that all required fields exist (and let `Node` handles retry).
### Example Text Summarization
```python
class SummarizeNode(Node):
def exec(self, prep_res):
# Suppose `prep_res` is the text to summarize.
prompt = f"""
Please summarize the following text as YAML, with exactly 3 bullet points
{prep_res}
Now, output:
```yaml
summary:
- bullet 1
- bullet 2
- bullet 3
```"""
response = call_llm(prompt)
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
import yaml
structured_result = yaml.safe_load(yaml_str)
assert "summary" in structured_result
assert isinstance(structured_result["summary"], list)
return structured_result
```
> Besides using `assert` statements, another popular way to validate schemas is [Pydantic](https://github.com/pydantic/pydantic)
{: .note }
### Why YAML instead of JSON?
Current LLMs struggle with escaping. YAML is easier with strings since they don't always need quotes.
**In JSON**
```json
{
"dialogue": "Alice said: \"Hello Bob.\\nHow are you?\\nI am good.\""
}
```
- Every double quote inside the string must be escaped with `\"`.
- Each newline in the dialogue must be represented as `\n`.
**In YAML**
```yaml
dialogue: |
Alice said: "Hello Bob.
How are you?
I am good."
```
- No need to escape interior quotes—just place the entire text under a block literal (`|`).
- Newlines are naturally preserved without needing `\n`.
================================================
File: docs/design_pattern/workflow.md
================================================
---
layout: default
title: "Workflow"
parent: "Design Pattern"
nav_order: 2
---
# Workflow
Many real-world tasks are too complex for one LLM call. The solution is to **Task Decomposition**: decompose them into a [chain](../core_abstraction/flow.md) of multiple Nodes.
> - You don't want to make each task **too coarse**, because it may be *too complex for one LLM call*.
> - You don't want to make each task **too granular**, because then *the LLM call doesn't have enough context* and results are *not consistent across nodes*.
>
> You usually need multiple *iterations* to find the *sweet spot*. If the task has too many *edge cases*, consider using [Agents](./agent.md).
{: .best-practice }
### Example: Article Writing
```python
class GenerateOutline(Node):
def prep(self, shared): return shared["topic"]
def exec(self, topic): return call_llm(f"Create a detailed outline for an article about {topic}")
def post(self, shared, prep_res, exec_res): shared["outline"] = exec_res
class WriteSection(Node):
def prep(self, shared): return shared["outline"]
def exec(self, outline): return call_llm(f"Write content based on this outline: {outline}")
def post(self, shared, prep_res, exec_res): shared["draft"] = exec_res
class ReviewAndRefine(Node):
def prep(self, shared): return shared["draft"]
def exec(self, draft): return call_llm(f"Review and improve this draft: {draft}")
def post(self, shared, prep_res, exec_res): shared["final_article"] = exec_res
# Connect nodes
outline = GenerateOutline()
write = WriteSection()
review = ReviewAndRefine()
outline >> write >> review
# Create and run flow
writing_flow = Flow(start=outline)
shared = {"topic": "AI Safety"}
writing_flow.run(shared)
```
For *dynamic cases*, consider using [Agents](./agent.md).
================================================
File: docs/utility_function/llm.md
================================================
---
layout: default
title: "LLM Wrapper"
parent: "Utility Function"
nav_order: 1
---
# LLM Wrappers
Check out libraries like [litellm](https://github.com/BerriAI/litellm).
Here, we provide some minimal example implementations:
1. OpenAI
```python
def call_llm(prompt):
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY_HERE")
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
# Example usage
call_llm("How are you?")
```
> Store the API key in an environment variable like OPENAI_API_KEY for security.
{: .best-practice }
2. Claude (Anthropic)
```python
def call_llm(prompt):
from anthropic import Anthropic
client = Anthropic(api_key="YOUR_API_KEY_HERE")
r = client.messages.create(
model="claude-sonnet-4-0",
messages=[
{"role": "user", "content": prompt}
]
)
return r.content[0].text
```
3. Google (Generative AI Studio / PaLM API)
```python
def call_llm(prompt):
from google import genai
client = genai.Client(api_key='GEMINI_API_KEY')
response = client.models.generate_content(
model='gemini-2.5-pro',
contents=prompt
)
return response.text
```
4. Azure (Azure OpenAI)
```python
def call_llm(prompt):
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint="https://.openai.azure.com/",
api_key="YOUR_API_KEY_HERE",
api_version="2023-05-15"
)
r = client.chat.completions.create(
model="",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
```
5. Ollama (Local LLM)
```python
def call_llm(prompt):
from ollama import chat
response = chat(
model="llama2",
messages=[{"role": "user", "content": prompt}]
)
return response.message.content
```
## Improvements
Feel free to enhance your `call_llm` function as needed. Here are examples:
- Handle chat history:
```python
def call_llm(messages):
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY_HERE")
r = client.chat.completions.create(
model="gpt-4o",
messages=messages
)
return r.choices[0].message.content
```
- Add in-memory caching
```python
from functools import lru_cache
@lru_cache(maxsize=1000)
def call_llm(prompt):
# Your implementation here
pass
```
> ⚠️ Caching conflicts with Node retries, as retries yield the same result.
>
> To address this, you could use cached results only if not retried.
{: .warning }
```python
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_call(prompt):
pass
def call_llm(prompt, use_cache):
if use_cache:
return cached_call(prompt)
# Call the underlying function directly
return cached_call.__wrapped__(prompt)
class SummarizeNode(Node):
def exec(self, text):
return call_llm(f"Summarize: {text}", self.cur_retry==0)
```
- Enable logging:
```python
def call_llm(prompt):
import logging
logging.info(f"Prompt: {prompt}")
response = ... # Your implementation here
logging.info(f"Response: {response}")
return response
```
================================================
FILE: .gitignore
================================================
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDE specific files
.idea/
.vscode/
*.swp
*.swo
*~
# Node
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
venv/
ENV/
# Logs and databases
*.log
*.sql
*.sqlite
# Build output
dist/
build/
out/
# Coverage reports
coverage/
.coverage
.coverage.*
htmlcov/
# Misc
*.bak
*.tmp
*.temp
test.ipynb
.pytest_cache/
cookbook/pocketflow-multi-agent/.python-version
# local
uv.lock
.python-version
pyproject.toml
usage.md
cookbook/pocketflow-minimal-example/viz/flow_visualization.html
cookbook/pocketflow-minimal-example/viz/flow_visualization.json
.claude/
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Zachary Huang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
English | [中文](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_CHINESE.md) | [Español](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_SPANISH.md) | [日本語](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_JAPANESE.md) | [Deutsch](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_GERMAN.md) | [Русский](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_RUSSIAN.md) | [Português](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_PORTUGUESE.md) | [Français](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_FRENCH.md) | [한국어](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_KOREAN.md)

[](https://the-pocket.github.io/PocketFlow/)
Pocket Flow is a [100-line](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) minimalist LLM framework
- **Lightweight**: Just 100 lines. Zero bloat, zero dependencies, zero vendor lock-in.
- **Expressive**: Everything you love—([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agents](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), and more.
- **[Agentic Coding](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)**: Let AI Agents (e.g., Cursor AI) build Agents—10x productivity boost!
Get started with Pocket Flow:
- To install, ```pip install pocketflow```or just copy the [source code](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) (only 100 lines).
- To learn more, check out the [video tutorial](https://youtu.be/0Zr3NwcvpA0) and [documentation](https://the-pocket.github.io/PocketFlow/)
- 🎉 Join our [Discord](https://discord.gg/hUHHE9Sa6T) to connect with other developers building with Pocket Flow!
- 🎉 Pocket Flow now has [Typescript](https://github.com/The-Pocket/PocketFlow-Typescript), [Java](https://github.com/The-Pocket/PocketFlow-Java), [C++](https://github.com/The-Pocket/PocketFlow-CPP), [Go](https://github.com/The-Pocket/PocketFlow-Go), [Rust](https://github.com/The-Pocket/PocketFlow-Rust) and [PHP](https://github.com/The-Pocket/PocketFlow-PHP) versions!
## Why Pocket Flow?
Current LLM frameworks are bloated... You only need 100 lines for LLM Framework!
## How does Pocket Flow work?
The [100 lines](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) capture the core abstraction of LLM frameworks: Graph!
From there, it's easy to implement popular design patterns like ([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agents](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), etc.
✨ Below are basic tutorials:
| Name | Difficulty | Description |
| :-------------: | :-------------: | :--------------------- |
| [Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *Dummy* | A basic chat bot with conversation history |
| [Structured Output](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *Dummy* | Extracting structured data from resumes by prompting |
| [Workflow](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *Dummy* | A writing workflow that outlines, writes content, and applies styling |
| [Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *Dummy* | A research agent that can search the web and answer questions |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *Dummy* | A simple Retrieval-augmented Generation process |
| [Batch](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *Dummy* | A batch processor that translates markdown into multiple languages |
| [Streaming](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *Dummy* | A real-time LLM streaming demo with user interrupt capability |
| [Chat Guardrail](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Dummy* | A travel advisor chatbot that only processes travel-related queries |
| [Majority Vote](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ☆☆☆ *Dummy* | Improve reasoning accuracy by aggregating multiple solution attempts |
| [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ☆☆☆ *Dummy* | Batch resume qualification using map-reduce pattern |
| [CLI HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-cli-hitl) | ☆☆☆ *Dummy* | A command-line joke generator with human-in-the-loop feedback |
| [Multi-Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Beginner* | A Taboo word game for async communication between 2 agents |
| [Supervisor](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *Beginner* | Research agent is getting unreliable... Let's build a supervision process|
| [Parallel](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *Beginner* | A parallel execution demo that shows 3x speedup |
| [Parallel Flow](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *Beginner* | A parallel image processing showing 8x speedup |
| [Thinking](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *Beginner* | Solve complex reasoning problems through Chain-of-Thought |
| [Memory](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *Beginner* | A chat bot with short-term and long-term memory |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *Beginner* | Convert natural language to SQL queries with an auto-debug loop |
| [Code Generator](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-code-generator) | ★☆☆ *Beginner* | Generate test cases, implement solutions, and iteratively improve code |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *Beginner* | Agent using Model Context Protocol for numerical operations |
| [Agent Skills](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent-skills) | ★☆☆ *Beginner* | Route requests to reusable markdown skills and apply them in an agent flow |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *Beginner* | Agent wrapped with A2A protocol for inter-agent communication |
| [Streamlit FSM](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-streamlit-fsm) | ★☆☆ *Beginner* | Streamlit app with finite state machine for HITL image generation |
| [FastAPI WebSocket](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-fastapi-websocket) | ★☆☆ *Beginner* | Real-time chat interface with streaming LLM responses via WebSocket |
| [FastAPI Background](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-fastapi-background) | ★☆☆ *Beginner* | FastAPI app with background jobs and real-time progress via SSE |
| [Voice Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-voice-chat) | ★☆☆ *Beginner* | An interactive voice chat application with VAD, STT, LLM, and TTS. |
👀 Want to see other tutorials for dummies? [Create an issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
## How to Use Pocket Flow?
🚀 Through **Agentic Coding**—the fastest LLM App development paradigm-where *humans design* and *agents code*!
✨ Below are examples of more complex LLM Apps:
| App Name | Difficulty | Topics | Human Design | Agent Code |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Website Chatbot](https://github.com/The-Pocket/PocketFlow-Tutorial-Website-Chatbot) Turn your website into a 24/7 customer support genius | ★★☆ *Medium* | [Agent](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) | [Design Doc](https://github.com/The-Pocket/PocketFlow-Tutorial-Website-Chatbot/blob/main/docs/design.md) | [Flow Code](https://github.com/The-Pocket/PocketFlow-Tutorial-Website-Chatbot/blob/main/flow.py)
| [Danganronpa Simulator](https://github.com/The-Pocket/PocketFlow-Tutorial-Danganronpa-Simulator) Forget the Turing test. Danganronpa, the ultimate AI experiment! | ★★★ *Advanced* | [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) [Agent](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [Design Doc](https://github.com/The-Pocket/PocketFlow-Tutorial-Danganronpa-Simulator/blob/main/docs/design.md) | [Flow Code](https://github.com/The-Pocket/PocketFlow-Tutorial-Danganronpa-Simulator/blob/main/flow.py)
| [Codebase Knowledge Builder](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) Life's too short to stare at others' code in confusion | ★★☆ *Medium* | [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [Design Doc](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [Flow Code](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [Build Cursor with Cursor](https://github.com/The-Pocket/Tutorial-Cursor) We'll reach the singularity soon ... | ★★★ *Advanced* | [Agent](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [Design Doc](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [Flow Code](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [Ask AI Paul Graham](https://github.com/The-Pocket/Tutorial-YC-Partner) Ask AI Paul Graham, in case you don't get in | ★★☆ *Medium* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [Design Doc](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [Flow Code](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [Youtube Summarizer](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) Explain YouTube Videos to you like you're 5 | ★☆☆ *Beginner* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [Design Doc](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [Flow Code](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [Cold Opener Generator](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) Instant icebreakers that turn cold leads hot | ★☆☆ *Beginner* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [Web Search](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [Design Doc](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Flow Code](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- Want to learn **Agentic Coding**?
- Check out [my YouTube](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1) for video tutorial on how some apps above are made!
- Want to build your own LLM App? Read this [post](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)! Start with [this template](https://github.com/The-Pocket/PocketFlow-Template-Python)!
================================================
FILE: cookbook/README.md
================================================
# Pocket Flow Cookbook
| Name | Difficulty | Description |
| :-------------: | :-------------: | :--------------------- |
| [Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *Dummy* | A basic chat bot with conversation history |
| [Structured Output](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *Dummy* | Extracting structured data from resumes by prompting |
| [Workflow](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *Dummy* | A writing workflow that outlines, writes content, and applies styling |
| [Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *Dummy* | A research agent that can search the web and answer questions |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *Dummy* | A simple Retrieval-augmented Generation process |
| [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ☆☆☆ *Dummy* | A resume qualification processor using map-reduce pattern for batch evaluation |
| [Streaming](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *Dummy* | A real-time LLM streaming demo with user interrupt capability |
| [Chat Guardrail](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Dummy* | A travel advisor chatbot that only processes travel-related queries |
| [Multi-Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Beginner* | A Taboo word game for asynchronous communication between two agents |
| [Supervisor](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *Beginner* | Research agent is getting unreliable... Let's build a supervision process|
| [Parallel](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *Beginner* | A parallel execution demo that shows 3x speedup |
| [Parallel Flow](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *Beginner* | A parallel image processing demo showing 8x speedup with multiple filters |
| [Thinking](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *Beginner* | Solve complex reasoning problems through Chain-of-Thought |
| [Memory](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *Beginner* | A chat bot with short-term and long-term memory |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *Beginner* | Agent using Model Context Protocol for numerical operations |
| [Tracing](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-tracing) | ★☆☆ *Beginner* | Trace and visualize the execution of your flow |
👀 Want to see other tutorials? [Create an issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
================================================
FILE: cookbook/data/PaulGrahamEssaysLarge/addiction.txt
================================================
July 2010What hard liquor, cigarettes, heroin, and crack have in common is
that they're all more concentrated forms of less addictive predecessors.
Most if not all the things we describe as addictive are. And the
scary thing is, the process that created them is accelerating.We wouldn't want to stop it. It's the same process that cures
diseases: technological progress. Technological progress means
making things do more of what we want. When the thing we want is
something we want to want, we consider technological progress good.
If some new technique makes solar cells x% more efficient, that
seems strictly better. When progress concentrates something we
don't want to want—when it transforms opium into heroin—it seems
bad. But it's the same process at work.
[1]No one doubts this process is accelerating, which means increasing
numbers of things we like will be transformed into things we like
too much.
[2]As far as I know there's no word for something we like too much.
The closest is the colloquial sense of "addictive." That usage has
become increasingly common during my lifetime. And it's clear why:
there are an increasing number of things we need it for. At the
extreme end of the spectrum are crack and meth. Food has been
transformed by a combination of factory farming and innovations in
food processing into something with way more immediate bang for the
buck, and you can see the results in any town in America. Checkers
and solitaire have been replaced by World of Warcraft and FarmVille.
TV has become much more engaging, and even so it can't compete with Facebook.The world is more addictive than it was 40 years ago. And unless
the forms of technological progress that produced these things are
subject to different laws than technological progress in general,
the world will get more addictive in the next 40 years than it did
in the last 40.The next 40 years will bring us some wonderful things. I don't
mean to imply they're all to be avoided. Alcohol is a dangerous
drug, but I'd rather live in a world with wine than one without.
Most people can coexist with alcohol; but you have to be careful.
More things we like will mean more things we have to be careful
about.Most people won't, unfortunately. Which means that as the world
becomes more addictive, the two senses in which one can live a
normal life will be driven ever further apart. One sense of "normal"
is statistically normal: what everyone else does. The other is the
sense we mean when we talk about the normal operating range of a
piece of machinery: what works best.These two senses are already quite far apart. Already someone
trying to live well would seem eccentrically abstemious in most of
the US. That phenomenon is only going to become more pronounced.
You can probably take it as a rule of thumb from now on that if
people don't think you're weird, you're living badly.Societies eventually develop antibodies to addictive new things.
I've seen that happen with cigarettes. When cigarettes first
appeared, they spread the way an infectious disease spreads through
a previously isolated population. Smoking rapidly became a
(statistically) normal thing. There were ashtrays everywhere. We
had ashtrays in our house when I was a kid, even though neither of
my parents smoked. You had to for guests.As knowledge spread about the dangers of smoking, customs changed.
In the last 20 years, smoking has been transformed from something
that seemed totally normal into a rather seedy habit: from something
movie stars did in publicity shots to something small huddles of
addicts do outside the doors of office buildings. A lot of the
change was due to legislation, of course, but the legislation
couldn't have happened if customs hadn't already changed.It took a while though—on the order of 100 years. And unless the
rate at which social antibodies evolve can increase to match the
accelerating rate at which technological progress throws off new
addictions, we'll be increasingly unable to rely on customs to
protect us.
[3]
Unless we want to be canaries in the coal mine
of each new addiction—the people whose sad example becomes a
lesson to future generations—we'll have to figure out for ourselves
what to avoid and how. It will actually become a reasonable strategy
(or a more reasonable strategy) to suspect
everything new.In fact, even that won't be enough. We'll have to worry not just
about new things, but also about existing things becoming more
addictive. That's what bit me. I've avoided most addictions, but
the Internet got me because it became addictive while I was using
it.
[4]Most people I know have problems with Internet addiction. We're
all trying to figure out our own customs for getting free of it.
That's why I don't have an iPhone, for example; the last thing I
want is for the Internet to follow me out into the world.
[5]
My latest trick is taking long hikes. I used to think running was a
better form of exercise than hiking because it took less time. Now
the slowness of hiking seems an advantage, because the longer I
spend on the trail, the longer I have to think without interruption.Sounds pretty eccentric, doesn't it? It always will when you're
trying to solve problems where there are no customs yet to guide
you. Maybe I can't plead Occam's razor; maybe I'm simply eccentric.
But if I'm right about the acceleration of addictiveness, then this
kind of lonely squirming to avoid it will increasingly be the fate
of anyone who wants to get things done. We'll increasingly be
defined by what we say no to.
Notes[1]
Could you restrict technological progress to areas where you
wanted it? Only in a limited way, without becoming a police state.
And even then your restrictions would have undesirable side effects.
"Good" and "bad" technological progress aren't sharply differentiated,
so you'd find you couldn't slow the latter without also slowing the
former. And in any case, as Prohibition and the "war on drugs"
show, bans often do more harm than good.[2]
Technology has always been accelerating. By Paleolithic
standards, technology evolved at a blistering pace in the Neolithic
period.[3]
Unless we mass produce social customs. I suspect the recent
resurgence of evangelical Christianity in the US is partly a reaction
to drugs. In desperation people reach for the sledgehammer; if
their kids won't listen to them, maybe they'll listen to God. But
that solution has broader consequences than just getting kids to
say no to drugs. You end up saying no to
science as well.
I worry we may be heading for a future in which only a few people
plot their own itinerary through no-land, while everyone else books
a package tour. Or worse still, has one booked for them by the
government.[4]
People commonly use the word "procrastination" to describe
what they do on the Internet. It seems to me too mild to describe
what's happening as merely not-doing-work. We don't call it
procrastination when someone gets drunk instead of working.[5]
Several people have told me they like the iPad because it
lets them bring the Internet into situations where a laptop would
be too conspicuous. In other words, it's a hip flask. (This is
true of the iPhone too, of course, but this advantage isn't as
obvious because it reads as a phone, and everyone's used to those.)Thanks to Sam Altman, Patrick Collison, Jessica Livingston, and
Robert Morris for reading drafts of this.
================================================
FILE: cookbook/data/PaulGrahamEssaysLarge/aord.txt
================================================
October 2015When I talk to a startup that's been operating for more than 8 or
9 months, the first thing I want to know is almost always the same.
Assuming their expenses remain constant and their revenue growth
is what it has been over the last several months, do they make it to
profitability on the money they have left? Or to put it more
dramatically, by default do they live or die?The startling thing is how often the founders themselves don't know.
Half the founders I talk to don't know whether they're default alive
or default dead.If you're among that number, Trevor Blackwell has made a handy
calculator you can use to find out.The reason I want to know first whether a startup is default alive
or default dead is that the rest of the conversation depends on the
answer. If the company is default alive, we can talk about ambitious
new things they could do. If it's default dead, we probably need
to talk about how to save it. We know the current trajectory ends
badly. How can they get off that trajectory?Why do so few founders know whether they're default alive or default
dead? Mainly, I think, because they're not used to asking that.
It's not a question that makes sense to ask early on, any more than
it makes sense to ask a 3 year old how he plans to support
himself. But as the company grows older, the question switches from
meaningless to critical. That kind of switch often takes people
by surprise.I propose the following solution: instead of starting to ask too
late whether you're default alive or default dead, start asking too
early. It's hard to say precisely when the question switches
polarity. But it's probably not that dangerous to start worrying
too early that you're default dead, whereas it's very dangerous to
start worrying too late.The reason is a phenomenon I wrote about earlier: the
fatal pinch.
The fatal pinch is default dead + slow growth + not enough
time to fix it. And the way founders end up in it is by not realizing
that's where they're headed.There is another reason founders don't ask themselves whether they're
default alive or default dead: they assume it will be easy to raise
more money. But that assumption is often false, and worse still, the
more you depend on it, the falser it becomes.Maybe it will help to separate facts from hopes. Instead of thinking
of the future with vague optimism, explicitly separate the components.
Say "We're default dead, but we're counting on investors to save
us." Maybe as you say that, it will set off the same alarms in your
head that it does in mine. And if you set off the alarms sufficiently
early, you may be able to avoid the fatal pinch.It would be safe to be default dead if you could count on investors
saving you. As a rule their interest is a function of
growth. If you have steep revenue growth, say over 5x a year, you
can start to count on investors being interested even if you're not
profitable.
[1]
But investors are so fickle that you can never
do more than start to count on them. Sometimes something about your
business will spook investors even if your growth is great. So no
matter how good your growth is, you can never safely treat fundraising
as more than a plan A. You should always have a plan B as well: you
should know (as in write down) precisely what you'll need to do to
survive if you can't raise more money, and precisely when you'll
have to switch to plan B if plan A isn't working.In any case, growing fast versus operating cheaply is far from the
sharp dichotomy many founders assume it to be. In practice there
is surprisingly little connection between how much a startup spends
and how fast it grows. When a startup grows fast, it's usually
because the product hits a nerve, in the sense of hitting some big
need straight on. When a startup spends a lot, it's usually because
the product is expensive to develop or sell, or simply because
they're wasteful.If you're paying attention, you'll be asking at this point not just
how to avoid the fatal pinch, but how to avoid being default dead.
That one is easy: don't hire too fast. Hiring too fast is by far
the biggest killer of startups that raise money.
[2]Founders tell themselves they need to hire in order to grow. But
most err on the side of overestimating this need rather than
underestimating it. Why? Partly because there's so much work to
do. Naive founders think that if they can just hire enough
people, it will all get done. Partly because successful startups have
lots of employees, so it seems like that's what one does in order
to be successful. In fact the large staffs of successful startups
are probably more the effect of growth than the cause. And
partly because when founders have slow growth they don't want to
face what is usually the real reason: the product is not appealing
enough.Plus founders who've just raised money are often encouraged to
overhire by the VCs who funded them. Kill-or-cure strategies are
optimal for VCs because they're protected by the portfolio effect.
VCs want to blow you up, in one sense of the phrase or the other.
But as a founder your incentives are different. You want above all
to survive.
[3]Here's a common way startups die. They make something moderately
appealing and have decent initial growth. They raise their first
round fairly easily, because the founders seem smart and the idea
sounds plausible. But because the product is only moderately
appealing, growth is ok but not great. The founders convince
themselves that hiring a bunch of people is the way to boost growth.
Their investors agree. But (because the product is only moderately
appealing) the growth never comes. Now they're rapidly running out
of runway. They hope further investment will save them. But because
they have high expenses and slow growth, they're now unappealing
to investors. They're unable to raise more, and the company dies.What the company should have done is address the fundamental problem:
that the product is only moderately appealing. Hiring people is
rarely the way to fix that. More often than not it makes it harder.
At this early stage, the product needs to evolve more than to be
"built out," and that's usually easier with fewer people.
[4]Asking whether you're default alive or default dead may save you
from this. Maybe the alarm bells it sets off will counteract the
forces that push you to overhire. Instead you'll be compelled to
seek growth in other ways. For example, by doing
things that don't scale, or by redesigning the product in the
way only founders can.
And for many if not most startups, these paths to growth will be
the ones that actually work.Airbnb waited 4 months after raising money at the end of Y Combinator
before they hired their first employee. In the meantime the founders
were terribly overworked. But they were overworked evolving Airbnb
into the astonishingly successful organism it is now.Notes[1]
Steep usage growth will also interest investors. Revenue
will ultimately be a constant multiple of usage, so x% usage growth
predicts x% revenue growth. But in practice investors discount
merely predicted revenue, so if you're measuring usage you need a
higher growth rate to impress investors.[2]
Startups that don't raise money are saved from hiring too
fast because they can't afford to. But that doesn't mean you should
avoid raising money in order to avoid this problem, any more than
that total abstinence is the only way to avoid becoming an alcoholic.[3]
I would not be surprised if VCs' tendency to push founders
to overhire is not even in their own interest. They don't know how
many of the companies that get killed by overspending might have
done well if they'd survived. My guess is a significant number.[4]
After reading a draft, Sam Altman wrote:"I think you should make the hiring point more strongly. I think
it's roughly correct to say that YC's most successful companies
have never been the fastest to hire, and one of the marks of a great
founder is being able to resist this urge."Paul Buchheit adds:"A related problem that I see a lot is premature scaling—founders
take a small business that isn't really working (bad unit economics,
typically) and then scale it up because they want impressive growth
numbers. This is similar to over-hiring in that it makes the business
much harder to fix once it's big, plus they are bleeding cash really
fast."
Thanks to Sam Altman, Paul Buchheit, Joe Gebbia, Jessica Livingston,
and Geoff Ralston for reading drafts of this.
================================================
FILE: cookbook/data/PaulGrahamEssaysLarge/apple.txt
================================================
Want to start a startup? Get funded by
Y Combinator.
November 2009I don't think Apple realizes how badly the App Store approval process
is broken. Or rather, I don't think they realize how much it matters
that it's broken.The way Apple runs the App Store has harmed their reputation with
programmers more than anything else they've ever done.
Their reputation with programmers used to be great.
It used to be the most common complaint you heard
about Apple was that their fans admired them too uncritically.
The App Store has changed that. Now a lot of programmers
have started to see Apple as evil.How much of the goodwill Apple once had with programmers have they
lost over the App Store? A third? Half? And that's just so far.
The App Store is an ongoing karma leak.* * *How did Apple get into this mess? Their fundamental problem is
that they don't understand software.They treat iPhone apps the way they treat the music they sell through
iTunes. Apple is the channel; they own the user; if you want to
reach users, you do it on their terms. The record labels agreed,
reluctantly. But this model doesn't work for software. It doesn't
work for an intermediary to own the user. The software business
learned that in the early 1980s, when companies like VisiCorp showed
that although the words "software" and "publisher" fit together,
the underlying concepts don't. Software isn't like music or books.
It's too complicated for a third party to act as an intermediary
between developer and user. And yet that's what Apple is trying
to be with the App Store: a software publisher. And a particularly
overreaching one at that, with fussy tastes and a rigidly enforced
house style.If software publishing didn't work in 1980, it works even less now
that software development has evolved from a small number of big
releases to a constant stream of small ones. But Apple doesn't
understand that either. Their model of product development derives
from hardware. They work on something till they think it's finished,
then they release it. You have to do that with hardware, but because
software is so easy to change, its design can benefit from evolution.
The standard way to develop applications now is to launch fast and
iterate. Which means it's a disaster to have long, random delays
each time you release a new version.Apparently Apple's attitude is that developers should be more careful
when they submit a new version to the App Store. They would say
that. But powerful as they are, they're not powerful enough to
turn back the evolution of technology. Programmers don't use
launch-fast-and-iterate out of laziness. They use it because it
yields the best results. By obstructing that process, Apple is
making them do bad work, and programmers hate that as much as Apple
would.How would Apple like it if when they discovered a serious bug in
OS X, instead of releasing a software update immediately, they had
to submit their code to an intermediary who sat on it for a month
and then rejected it because it contained an icon they didn't like?By breaking software development, Apple gets the opposite of what
they intended: the version of an app currently available in the App
Store tends to be an old and buggy one. One developer told me:
As a result of their process, the App Store is full of half-baked
applications. I make a new version almost every day that I release
to beta users. The version on the App Store feels old and crappy.
I'm sure that a lot of developers feel this way: One emotion is
"I'm not really proud about what's in the App Store", and it's
combined with the emotion "Really, it's Apple's fault."
Another wrote:
I believe that they think their approval process helps users by
ensuring quality. In reality, bugs like ours get through all the
time and then it can take 4-8 weeks to get that bug fix approved,
leaving users to think that iPhone apps sometimes just don't work.
Worse for Apple, these apps work just fine on other platforms
that have immediate approval processes.
Actually I suppose Apple has a third misconception: that all the
complaints about App Store approvals are not a serious problem.
They must hear developers complaining. But partners and suppliers
are always complaining. It would be a bad sign if they weren't;
it would mean you were being too easy on them. Meanwhile the iPhone
is selling better than ever. So why do they need to fix anything?They get away with maltreating developers, in the short term, because
they make such great hardware. I just bought a new 27" iMac a
couple days ago. It's fabulous. The screen's too shiny, and the
disk is surprisingly loud, but it's so beautiful that you can't
make yourself care.So I bought it, but I bought it, for the first time, with misgivings.
I felt the way I'd feel buying something made in a country with a
bad human rights record. That was new. In the past when I bought
things from Apple it was an unalloyed pleasure. Oh boy! They make
such great stuff. This time it felt like a Faustian bargain. They
make such great stuff, but they're such assholes. Do I really want
to support this company?* * *Should Apple care what people like me think? What difference does
it make if they alienate a small minority of their users?There are a couple reasons they should care. One is that these
users are the people they want as employees. If your company seems
evil, the best programmers won't work for you. That hurt Microsoft
a lot starting in the 90s. Programmers started to feel sheepish
about working there. It seemed like selling out. When people from
Microsoft were talking to other programmers and they mentioned where
they worked, there were a lot of self-deprecating jokes about having
gone over to the dark side. But the real problem for Microsoft
wasn't the embarrassment of the people they hired. It was the
people they never got. And you know who got them? Google and
Apple. If Microsoft was the Empire, they were the Rebel Alliance.
And it's largely because they got more of the best people that
Google and Apple are doing so much better than Microsoft today.Why are programmers so fussy about their employers' morals? Partly
because they can afford to be. The best programmers can work
wherever they want. They don't have to work for a company they
have qualms about.But the other reason programmers are fussy, I think, is that evil
begets stupidity. An organization that wins by exercising power
starts to lose the ability to win by doing better work. And it's
not fun for a smart person to work in a place where the best ideas
aren't the ones that win. I think the reason Google embraced "Don't
be evil" so eagerly was not so much to impress the outside world
as to inoculate themselves against arrogance.
[1]That has worked for Google so far. They've become more
bureaucratic, but otherwise they seem to have held true to their
original principles. With Apple that seems less the case. When you
look at the famous
1984 ad
now, it's easier to imagine Apple as the
dictator on the screen than the woman with the hammer.
[2]
In fact, if you read the dictator's speech it sounds uncannily like a
prophecy of the App Store.
We have triumphed over the unprincipled dissemination of facts.We have created, for the first time in all history, a garden of
pure ideology, where each worker may bloom secure from the pests
of contradictory and confusing truths.
The other reason Apple should care what programmers think of them
is that when you sell a platform, developers make or break you. If
anyone should know this, Apple should. VisiCalc made the Apple II.And programmers build applications for the platforms they use. Most
applications—most startups, probably—grow out of personal projects.
Apple itself did. Apple made microcomputers because that's what
Steve Wozniak wanted for himself. He couldn't have afforded a
minicomputer.
[3]
Microsoft likewise started out making interpreters
for little microcomputers because
Bill Gates and Paul Allen were interested in using them. It's a
rare startup that doesn't build something the founders use.The main reason there are so many iPhone apps is that so many programmers
have iPhones. They may know, because they read it in an article,
that Blackberry has such and such market share. But in practice
it's as if RIM didn't exist. If they're going to build something,
they want to be able to use it themselves, and that means building
an iPhone app.So programmers continue to develop iPhone apps, even though Apple
continues to maltreat them. They're like someone stuck in an abusive
relationship. They're so attracted to the iPhone that they can't
leave. But they're looking for a way out. One wrote:
While I did enjoy developing for the iPhone, the control they
place on the App Store does not give me the drive to develop
applications as I would like. In fact I don't intend to make any
more iPhone applications unless absolutely necessary.
[4]
Can anything break this cycle? No device I've seen so far could.
Palm and RIM haven't a hope. The only credible contender is Android.
But Android is an orphan; Google doesn't really care about it, not
the way Apple cares about the iPhone. Apple cares about the iPhone
the way Google cares about search.* * *Is the future of handheld devices one locked down by Apple? It's
a worrying prospect. It would be a bummer to have another grim
monoculture like we had in the 1990s. In 1995, writing software
for end users was effectively identical with writing Windows
applications. Our horror at that prospect was the single biggest
thing that drove us to start building web apps.At least we know now what it would take to break Apple's lock.
You'd have to get iPhones out of programmers' hands. If programmers
used some other device for mobile web access, they'd start to develop
apps for that instead.How could you make a device programmers liked better than the iPhone?
It's unlikely you could make something better designed. Apple
leaves no room there. So this alternative device probably couldn't
win on general appeal. It would have to win by virtue of some
appeal it had to programmers specifically.One way to appeal to programmers is with software. If you
could think of an application programmers had to have, but that
would be impossible in the circumscribed world of the iPhone,
you could presumably get them to switch.That would definitely happen if programmers started to use handhelds
as development machines—if handhelds displaced laptops the
way laptops displaced desktops. You need more control of a development
machine than Apple will let you have over an iPhone.Could anyone make a device that you'd carry around in your pocket
like a phone, and yet would also work as a development machine?
It's hard to imagine what it would look like. But I've learned
never to say never about technology. A phone-sized device that
would work as a development machine is no more miraculous by present
standards than the iPhone itself would have seemed by the standards
of 1995.My current development machine is a MacBook Air, which I use with
an external monitor and keyboard in my office, and by itself when
traveling. If there was a version half the size I'd prefer it.
That still wouldn't be small enough to carry around everywhere like
a phone, but we're within a factor of 4 or so. Surely that gap is
bridgeable. In fact, let's make it an
RFS. Wanted:
Woman with hammer.Notes[1]
When Google adopted "Don't be evil," they were still so small
that no one would have expected them to be, yet.
[2]
The dictator in the 1984 ad isn't Microsoft, incidentally;
it's IBM. IBM seemed a lot more frightening in those days, but
they were friendlier to developers than Apple is now.[3]
He couldn't even afford a monitor. That's why the Apple
I used a TV as a monitor.[4]
Several people I talked to mentioned how much they liked the
iPhone SDK. The problem is not Apple's products but their policies.
Fortunately policies are software; Apple can change them instantly
if they want to. Handy that, isn't it?Thanks to Sam Altman, Trevor Blackwell, Ross Boucher,
James Bracy, Gabor Cselle,
Patrick Collison, Jason Freedman, John Gruber, Joe Hewitt, Jessica Livingston,
Robert Morris, Teng Siong Ong, Nikhil Pandit, Savraj Singh, and Jared Tame for reading drafts of this.
================================================
FILE: cookbook/data/PaulGrahamEssaysLarge/avg.txt
================================================
Want to start a startup? Get funded by
Y Combinator.
April 2001, rev. April 2003(This article is derived from a talk given at the 2001 Franz
Developer Symposium.)
In the summer of 1995, my friend Robert Morris and I
started a startup called
Viaweb.
Our plan was to write
software that would let end users build online stores.
What was novel about this software, at the time, was
that it ran on our server, using ordinary Web pages
as the interface.A lot of people could have been having this idea at the
same time, of course, but as far as I know, Viaweb was
the first Web-based application. It seemed such
a novel idea to us that we named the company after it:
Viaweb, because our software worked via the Web,
instead of running on your desktop computer.Another unusual thing about this software was that it
was written primarily in a programming language called
Lisp. It was one of the first big end-user
applications to be written in Lisp, which up till then
had been used mostly in universities and research labs. [1]The Secret WeaponEric Raymond has written an essay called "How to Become a Hacker,"
and in it, among other things, he tells would-be hackers what
languages they should learn. He suggests starting with Python and
Java, because they are easy to learn. The serious hacker will also
want to learn C, in order to hack Unix, and Perl for system
administration and cgi scripts. Finally, the truly serious hacker
should consider learning Lisp:
Lisp is worth learning for the profound enlightenment experience
you will have when you finally get it; that experience will make
you a better programmer for the rest of your days, even if you
never actually use Lisp itself a lot.
This is the same argument you tend to hear for learning Latin. It
won't get you a job, except perhaps as a classics professor, but
it will improve your mind, and make you a better writer in languages
you do want to use, like English.But wait a minute. This metaphor doesn't stretch that far. The
reason Latin won't get you a job is that no one speaks it. If you
write in Latin, no one can understand you. But Lisp is a computer
language, and computers speak whatever language you, the programmer,
tell them to.So if Lisp makes you a better programmer, like he says, why wouldn't
you want to use it? If a painter were offered a brush that would
make him a better painter, it seems to me that he would want to
use it in all his paintings, wouldn't he? I'm not trying to make
fun of Eric Raymond here. On the whole, his advice is good. What
he says about Lisp is pretty much the conventional wisdom. But
there is a contradiction in the conventional wisdom: Lisp will
make you a better programmer, and yet you won't use it.Why not? Programming languages are just tools, after all. If Lisp
really does yield better programs, you should use it. And if it
doesn't, then who needs it?This is not just a theoretical question. Software is a very
competitive business, prone to natural monopolies. A company that
gets software written faster and better will, all other things
being equal, put its competitors out of business. And when you're
starting a startup, you feel this very keenly. Startups tend to
be an all or nothing proposition. You either get rich, or you get
nothing. In a startup, if you bet on the wrong technology, your
competitors will crush you.Robert and I both knew Lisp well, and we couldn't see any reason
not to trust our instincts and go with Lisp. We knew that everyone
else was writing their software in C++ or Perl. But we also knew
that that didn't mean anything. If you chose technology that way,
you'd be running Windows. When you choose technology, you have to
ignore what other people are doing, and consider only what will
work the best.This is especially true in a startup. In a big company, you can
do what all the other big companies are doing. But a startup can't
do what all the other startups do. I don't think a lot of people
realize this, even in startups.The average big company grows at about ten percent a year. So if
you're running a big company and you do everything the way the
average big company does it, you can expect to do as well as the
average big company-- that is, to grow about ten percent a year.The same thing will happen if you're running a startup, of course.
If you do everything the way the average startup does it, you should
expect average performance. The problem here is, average performance
means that you'll go out of business. The survival rate for startups
is way less than fifty percent. So if you're running a startup,
you had better be doing something odd. If not, you're in trouble.Back in 1995, we knew something that I don't think our competitors
understood, and few understand even now: when you're writing
software that only has to run on your own servers, you can use
any language you want. When you're writing desktop software,
there's a strong bias toward writing applications in the same
language as the operating system. Ten years ago, writing applications
meant writing applications in C. But with Web-based software,
especially when you have the source code of both the language and
the operating system, you can use whatever language you want.This new freedom is a double-edged sword, however. Now that you
can use any language, you have to think about which one to use.
Companies that try to pretend nothing has changed risk finding that
their competitors do not.If you can use any language, which do you use? We chose Lisp.
For one thing, it was obvious that rapid development would be
important in this market. We were all starting from scratch, so
a company that could get new features done before its competitors
would have a big advantage. We knew Lisp was a really good language
for writing software quickly, and server-based applications magnify
the effect of rapid development, because you can release software
the minute it's done.If other companies didn't want to use Lisp, so much the better.
It might give us a technological edge, and we needed all the help
we could get. When we started Viaweb, we had no experience in
business. We didn't know anything about marketing, or hiring
people, or raising money, or getting customers. Neither of us had
ever even had what you would call a real job. The only thing we
were good at was writing software. We hoped that would save us.
Any advantage we could get in the software department, we would
take.So you could say that using Lisp was an experiment. Our hypothesis
was that if we wrote our software in Lisp, we'd be able to get
features done faster than our competitors, and also to do things
in our software that they couldn't do. And because Lisp was so
high-level, we wouldn't need a big development team, so our costs
would be lower. If this were so, we could offer a better product
for less money, and still make a profit. We would end up getting
all the users, and our competitors would get none, and eventually
go out of business. That was what we hoped would happen, anyway.What were the results of this experiment? Somewhat surprisingly,
it worked. We eventually had many competitors, on the order of
twenty to thirty of them, but none of their software could compete
with ours. We had a wysiwyg online store builder that ran on the
server and yet felt like a desktop application. Our competitors
had cgi scripts. And we were always far ahead of them in features.
Sometimes, in desperation, competitors would try to introduce
features that we didn't have. But with Lisp our development cycle
was so fast that we could sometimes duplicate a new feature within
a day or two of a competitor announcing it in a press release. By
the time journalists covering the press release got round to calling
us, we would have the new feature too.It must have seemed to our competitors that we had some kind of
secret weapon-- that we were decoding their Enigma traffic or
something. In fact we did have a secret weapon, but it was simpler
than they realized. No one was leaking news of their features to
us. We were just able to develop software faster than anyone
thought possible.When I was about nine I happened to get hold of a copy of The Day
of the Jackal, by Frederick Forsyth. The main character is an
assassin who is hired to kill the president of France. The assassin
has to get past the police to get up to an apartment that overlooks
the president's route. He walks right by them, dressed up as an
old man on crutches, and they never suspect him.Our secret weapon was similar. We wrote our software in a weird
AI language, with a bizarre syntax full of parentheses. For years
it had annoyed me to hear Lisp described that way. But now it
worked to our advantage. In business, there is nothing more valuable
than a technical advantage your competitors don't understand. In
business, as in war, surprise is worth as much as force.And so, I'm a little embarrassed to say, I never said anything
publicly about Lisp while we were working on Viaweb. We never
mentioned it to the press, and if you searched for Lisp on our Web
site, all you'd find were the titles of two books in my bio. This
was no accident. A startup should give its competitors as little
information as possible. If they didn't know what language our
software was written in, or didn't care, I wanted to keep it that
way.[2]The people who understood our technology best were the customers.
They didn't care what language Viaweb was written in either, but
they noticed that it worked really well. It let them build great
looking online stores literally in minutes. And so, by word of
mouth mostly, we got more and more users. By the end of 1996 we
had about 70 stores online. At the end of 1997 we had 500. Six
months later, when Yahoo bought us, we had 1070 users. Today, as
Yahoo Store, this software continues to dominate its market. It's
one of the more profitable pieces of Yahoo, and the stores built
with it are the foundation of Yahoo Shopping. I left Yahoo in
1999, so I don't know exactly how many users they have now, but
the last I heard there were about 20,000.
The Blub ParadoxWhat's so great about Lisp? And if Lisp is so great, why doesn't
everyone use it? These sound like rhetorical questions, but actually
they have straightforward answers. Lisp is so great not because
of some magic quality visible only to devotees, but because it is
simply the most powerful language available. And the reason everyone
doesn't use it is that programming languages are not merely
technologies, but habits of mind as well, and nothing changes
slower. Of course, both these answers need explaining.I'll begin with a shockingly controversial statement: programming
languages vary in power.Few would dispute, at least, that high level languages are more
powerful than machine language. Most programmers today would agree
that you do not, ordinarily, want to program in machine language.
Instead, you should program in a high-level language, and have a
compiler translate it into machine language for you. This idea is
even built into the hardware now: since the 1980s, instruction sets
have been designed for compilers rather than human programmers.Everyone knows it's a mistake to write your whole program by hand
in machine language. What's less often understood is that there
is a more general principle here: that if you have a choice of
several languages, it is, all other things being equal, a mistake
to program in anything but the most powerful one. [3]There are many exceptions to this rule. If you're writing a program
that has to work very closely with a program written in a certain
language, it might be a good idea to write the new program in the
same language. If you're writing a program that only has to do
something very simple, like number crunching or bit manipulation,
you may as well use a less abstract language, especially since it
may be slightly faster. And if you're writing a short, throwaway
program, you may be better off just using whatever language has
the best library functions for the task. But in general, for
application software, you want to be using the most powerful
(reasonably efficient) language you can get, and using anything
else is a mistake, of exactly the same kind, though possibly in a
lesser degree, as programming in machine language.You can see that machine language is very low level. But, at least
as a kind of social convention, high-level languages are often all
treated as equivalent. They're not. Technically the term "high-level
language" doesn't mean anything very definite. There's no dividing
line with machine languages on one side and all the high-level
languages on the other. Languages fall along a continuum [4] of
abstractness, from the most powerful all the way down to machine
languages, which themselves vary in power.Consider Cobol. Cobol is a high-level language, in the sense that
it gets compiled into machine language. Would anyone seriously
argue that Cobol is equivalent in power to, say, Python? It's
probably closer to machine language than Python.Or how about Perl 4? Between Perl 4 and Perl 5, lexical closures
got added to the language. Most Perl hackers would agree that Perl
5 is more powerful than Perl 4. But once you've admitted that,
you've admitted that one high level language can be more powerful
than another. And it follows inexorably that, except in special
cases, you ought to use the most powerful you can get.This idea is rarely followed to its conclusion, though. After a
certain age, programmers rarely switch languages voluntarily.
Whatever language people happen to be used to, they tend to consider
just good enough.Programmers get very attached to their favorite languages, and I
don't want to hurt anyone's feelings, so to explain this point I'm
going to use a hypothetical language called Blub. Blub falls right
in the middle of the abstractness continuum. It is not the most
powerful language, but it is more powerful than Cobol or machine
language.And in fact, our hypothetical Blub programmer wouldn't use either
of them. Of course he wouldn't program in machine language. That's
what compilers are for. And as for Cobol, he doesn't know how
anyone can get anything done with it. It doesn't even have x (Blub
feature of your choice).As long as our hypothetical Blub programmer is looking down the
power continuum, he knows he's looking down. Languages less powerful
than Blub are obviously less powerful, because they're missing some
feature he's used to. But when our hypothetical Blub programmer
looks in the other direction, up the power continuum, he doesn't
realize he's looking up. What he sees are merely weird languages.
He probably considers them about equivalent in power to Blub, but
with all this other hairy stuff thrown in as well. Blub is good
enough for him, because he thinks in Blub.When we switch to the point of view of a programmer using any of
the languages higher up the power continuum, however, we find that
he in turn looks down upon Blub. How can you get anything done in
Blub? It doesn't even have y.By induction, the only programmers in a position to see all the
differences in power between the various languages are those who
understand the most powerful one. (This is probably what Eric
Raymond meant about Lisp making you a better programmer.) You can't
trust the opinions of the others, because of the Blub paradox:
they're satisfied with whatever language they happen to use, because
it dictates the way they think about programs.I know this from my own experience, as a high school kid writing
programs in Basic. That language didn't even support recursion.
It's hard to imagine writing programs without using recursion, but
I didn't miss it at the time. I thought in Basic. And I was a
whiz at it. Master of all I surveyed.The five languages that Eric Raymond recommends to hackers fall at
various points on the power continuum. Where they fall relative
to one another is a sensitive topic. What I will say is that I
think Lisp is at the top. And to support this claim I'll tell you
about one of the things I find missing when I look at the other
four languages. How can you get anything done in them, I think,
without macros? [5]Many languages have something called a macro. But Lisp macros are
unique. And believe it or not, what they do is related to the
parentheses. The designers of Lisp didn't put all those parentheses
in the language just to be different. To the Blub programmer, Lisp
code looks weird. But those parentheses are there for a reason.
They are the outward evidence of a fundamental difference between
Lisp and other languages.Lisp code is made out of Lisp data objects. And not in the trivial
sense that the source files contain characters, and strings are
one of the data types supported by the language. Lisp code, after
it's read by the parser, is made of data structures that you can
traverse.If you understand how compilers work, what's really going on is
not so much that Lisp has a strange syntax as that Lisp has no
syntax. You write programs in the parse trees that get generated
within the compiler when other languages are parsed. But these
parse trees are fully accessible to your programs. You can write
programs that manipulate them. In Lisp, these programs are called
macros. They are programs that write programs.Programs that write programs? When would you ever want to do that?
Not very often, if you think in Cobol. All the time, if you think
in Lisp. It would be convenient here if I could give an example
of a powerful macro, and say there! how about that? But if I did,
it would just look like gibberish to someone who didn't know Lisp;
there isn't room here to explain everything you'd need to know to
understand what it meant. In
Ansi Common Lisp I tried to move
things along as fast as I could, and even so I didn't get to macros
until page 160.But I think I can give a kind of argument that might be convincing.
The source code of the Viaweb editor was probably about 20-25%
macros. Macros are harder to write than ordinary Lisp functions,
and it's considered to be bad style to use them when they're not
necessary. So every macro in that code is there because it has to
be. What that means is that at least 20-25% of the code in this
program is doing things that you can't easily do in any other
language. However skeptical the Blub programmer might be about my
claims for the mysterious powers of Lisp, this ought to make him
curious. We weren't writing this code for our own amusement. We
were a tiny startup, programming as hard as we could in order to
put technical barriers between us and our competitors.A suspicious person might begin to wonder if there was some
correlation here. A big chunk of our code was doing things that
are very hard to do in other languages. The resulting software
did things our competitors' software couldn't do. Maybe there was
some kind of connection. I encourage you to follow that thread.
There may be more to that old man hobbling along on his crutches
than meets the eye.Aikido for StartupsBut I don't expect to convince anyone
(over 25)
to go out and learn
Lisp. The purpose of this article is not to change anyone's mind,
but to reassure people already interested in using Lisp-- people
who know that Lisp is a powerful language, but worry because it
isn't widely used. In a competitive situation, that's an advantage.
Lisp's power is multiplied by the fact that your competitors don't
get it.If you think of using Lisp in a startup, you shouldn't worry that
it isn't widely understood. You should hope that it stays that
way. And it's likely to. It's the nature of programming languages
to make most people satisfied with whatever they currently use.
Computer hardware changes so much faster than personal habits that
programming practice is usually ten to twenty years behind the
processor. At places like MIT they were writing programs in
high-level languages in the early 1960s, but many companies continued
to write code in machine language well into the 1980s. I bet a
lot of people continued to write machine language until the processor,
like a bartender eager to close up and go home, finally kicked them
out by switching to a risc instruction set.Ordinarily technology changes fast. But programming languages are
different: programming languages are not just technology, but what
programmers think in. They're half technology and half religion.[6]
And so the median language, meaning whatever language the median
programmer uses, moves as slow as an iceberg. Garbage collection,
introduced by Lisp in about 1960, is now widely considered to be
a good thing. Runtime typing, ditto, is growing in popularity.
Lexical closures, introduced by Lisp in the early 1970s, are now,
just barely, on the radar screen. Macros, introduced by Lisp in the
mid 1960s, are still terra incognita.Obviously, the median language has enormous momentum. I'm not
proposing that you can fight this powerful force. What I'm proposing
is exactly the opposite: that, like a practitioner of Aikido, you
can use it against your opponents.If you work for a big company, this may not be easy. You will have
a hard time convincing the pointy-haired boss to let you build
things in Lisp, when he has just read in the paper that some other
language is poised, like Ada was twenty years ago, to take over
the world. But if you work for a startup that doesn't have
pointy-haired bosses yet, you can, like we did, turn the Blub
paradox to your advantage: you can use technology that your
competitors, glued immovably to the median language, will never be
able to match.If you ever do find yourself working for a startup, here's a handy
tip for evaluating competitors. Read their job listings. Everything
else on their site may be stock photos or the prose equivalent,
but the job listings have to be specific about what they want, or
they'll get the wrong candidates.During the years we worked on Viaweb I read a lot of job descriptions.
A new competitor seemed to emerge out of the woodwork every month
or so. The first thing I would do, after checking to see if they
had a live online demo, was look at their job listings. After a
couple years of this I could tell which companies to worry about
and which not to. The more of an IT flavor the job descriptions
had, the less dangerous the company was. The safest kind were the
ones that wanted Oracle experience. You never had to worry about
those. You were also safe if they said they wanted C++ or Java
developers. If they wanted Perl or Python programmers, that would
be a bit frightening-- that's starting to sound like a company
where the technical side, at least, is run by real hackers. If I
had ever seen a job posting looking for Lisp hackers, I would have
been really worried.
Notes[1] Viaweb at first had two parts: the editor, written in Lisp,
which people used to build their sites, and the ordering system,
written in C, which handled orders. The first version was mostly
Lisp, because the ordering system was small. Later we added two
more modules, an image generator written in C, and a back-office
manager written mostly in Perl.In January 2003, Yahoo released a new version of the editor
written in C++ and Perl. It's hard to say whether the program is no
longer written in Lisp, though, because to translate this program
into C++ they literally had to write a Lisp interpreter: the source
files of all the page-generating templates are still, as far as I
know, Lisp code. (See Greenspun's Tenth Rule.)[2] Robert Morris says that I didn't need to be secretive, because
even if our competitors had known we were using Lisp, they wouldn't
have understood why: "If they were that smart they'd already be
programming in Lisp."[3] All languages are equally powerful in the sense of being Turing
equivalent, but that's not the sense of the word programmers care
about. (No one wants to program a Turing machine.) The kind of
power programmers care about may not be formally definable, but
one way to explain it would be to say that it refers to features
you could only get in the less powerful language by writing an
interpreter for the more powerful language in it. If language A
has an operator for removing spaces from strings and language B
doesn't, that probably doesn't make A more powerful, because you
can probably write a subroutine to do it in B. But if A supports,
say, recursion, and B doesn't, that's not likely to be something
you can fix by writing library functions.[4] Note to nerds: or possibly a lattice, narrowing toward the top;
it's not the shape that matters here but the idea that there is at
least a partial order.[5] It is a bit misleading to treat macros as a separate feature.
In practice their usefulness is greatly enhanced by other Lisp
features like lexical closures and rest parameters.[6] As a result, comparisons of programming languages either take
the form of religious wars or undergraduate textbooks so determinedly
neutral that they're really works of anthropology. People who
value their peace, or want tenure, avoid the topic. But the question
is only half a religious one; there is something there worth
studying, especially if you want to design new languages.
================================================
FILE: cookbook/data/PaulGrahamEssaysLarge/before.txt
================================================
Want to start a startup? Get funded by
Y Combinator.
October 2014(This essay is derived from a guest lecture in Sam Altman's startup class at
Stanford. It's intended for college students, but much of it is
applicable to potential founders at other ages.)One of the advantages of having kids is that when you have to give
advice, you can ask yourself "what would I tell my own kids?" My
kids are little, but I can imagine what I'd tell them about startups
if they were in college, and that's what I'm going to tell you.Startups are very counterintuitive. I'm not sure why. Maybe it's
just because knowledge about them hasn't permeated our culture yet.
But whatever the reason, starting a startup is a task where you
can't always trust your instincts.It's like skiing in that way. When you first try skiing and you
want to slow down, your instinct is to lean back. But if you lean
back on skis you fly down the hill out of control. So part of
learning to ski is learning to suppress that impulse. Eventually
you get new habits, but at first it takes a conscious effort. At
first there's a list of things you're trying to remember as you
start down the hill.Startups are as unnatural as skiing, so there's a similar list for
startups. Here I'm going to give you the first part of it — the things
to remember if you want to prepare yourself to start a startup.
CounterintuitiveThe first item on it is the fact I already mentioned: that startups
are so weird that if you trust your instincts, you'll make a lot
of mistakes. If you know nothing more than this, you may at least
pause before making them.When I was running Y Combinator I used to joke that our function
was to tell founders things they would ignore. It's really true.
Batch after batch, the YC partners warn founders about mistakes
they're about to make, and the founders ignore them, and then come
back a year later and say "I wish we'd listened."Why do the founders ignore the partners' advice? Well, that's the
thing about counterintuitive ideas: they contradict your intuitions.
They seem wrong. So of course your first impulse is to disregard
them. And in fact my joking description is not merely the curse
of Y Combinator but part of its raison d'etre. If founders' instincts
already gave them the right answers, they wouldn't need us. You
only need other people to give you advice that surprises you. That's
why there are a lot of ski instructors and not many running
instructors.
[1]You can, however, trust your instincts about people. And in fact
one of the most common mistakes young founders make is not to
do that enough. They get involved with people who seem impressive,
but about whom they feel some misgivings personally. Later when
things blow up they say "I knew there was something off about him,
but I ignored it because he seemed so impressive."If you're thinking about getting involved with someone — as a
cofounder, an employee, an investor, or an acquirer — and you
have misgivings about them, trust your gut. If someone seems
slippery, or bogus, or a jerk, don't ignore it.This is one case where it pays to be self-indulgent. Work with
people you genuinely like, and you've known long enough to be sure.
ExpertiseThe second counterintuitive point is that it's not that important
to know a lot about startups. The way to succeed in a startup is
not to be an expert on startups, but to be an expert on your users
and the problem you're solving for them.
Mark Zuckerberg didn't succeed because he was an expert on startups.
He succeeded despite being a complete noob at startups, because he
understood his users really well.If you don't know anything about, say, how to raise an angel round,
don't feel bad on that account. That sort of thing you can learn
when you need to, and forget after you've done it.In fact, I worry it's not merely unnecessary to learn in great
detail about the mechanics of startups, but possibly somewhat
dangerous. If I met an undergrad who knew all about convertible
notes and employee agreements and (God forbid) class FF stock, I
wouldn't think "here is someone who is way ahead of their peers."
It would set off alarms. Because another of the characteristic
mistakes of young founders is to go through the motions of starting
a startup. They make up some plausible-sounding idea, raise money
at a good valuation, rent a cool office, hire a bunch of people.
From the outside that seems like what startups do. But the next
step after rent a cool office and hire a bunch of people is: gradually
realize how completely fucked they are, because while imitating all
the outward forms of a startup they have neglected the one thing
that's actually essential: making something people want.
GameWe saw this happen so often that we made up a name for it: playing
house. Eventually I realized why it was happening. The reason
young founders go through the motions of starting a startup is
because that's what they've been trained to do for their whole lives
up to that point. Think about what you have to do to get into
college, for example. Extracurricular activities, check. Even in
college classes most of the work is as artificial as running laps.I'm not attacking the educational system for being this way. There
will always be a certain amount of fakeness in the work you do when
you're being taught something, and if you measure their performance
it's inevitable that people will exploit the difference to the point
where much of what you're measuring is artifacts of the fakeness.I confess I did it myself in college. I found that in a lot of
classes there might only be 20 or 30 ideas that were the right shape
to make good exam questions. The way I studied for exams in these
classes was not (except incidentally) to master the material taught
in the class, but to make a list of potential exam questions and
work out the answers in advance. When I walked into the final, the
main thing I'd be feeling was curiosity about which of my questions
would turn up on the exam. It was like a game.It's not surprising that after being trained for their whole lives
to play such games, young founders' first impulse on starting a
startup is to try to figure out the tricks for winning at this new
game. Since fundraising appears to be the measure of success for
startups (another classic noob mistake), they always want to know what the
tricks are for convincing investors. We tell them the best way to
convince investors is to make a startup
that's actually doing well, meaning growing fast, and then simply
tell investors so. Then they want to know what the tricks are for
growing fast. And we have to tell them the best way to do that is
simply to make something people want.So many of the conversations YC partners have with young founders
begin with the founder asking "How do we..." and the partner replying
"Just..."Why do the founders always make things so complicated? The reason,
I realized, is that they're looking for the trick.So this is the third counterintuitive thing to remember about
startups: starting a startup is where gaming the system stops
working. Gaming the system may continue to work if you go to work
for a big company. Depending on how broken the company is, you can
succeed by sucking up to the right people, giving the impression
of productivity, and so on.
[2]
But that doesn't work with startups.
There is no boss to trick, only users, and all users care about is
whether your product does what they want. Startups are as impersonal
as physics. You have to make something people want, and you prosper
only to the extent you do.The dangerous thing is, faking does work to some degree on investors.
If you're super good at sounding like you know what you're talking
about, you can fool investors for at least one and perhaps even two
rounds of funding. But it's not in your interest to. The company
is ultimately doomed. All you're doing is wasting your own time
riding it down.So stop looking for the trick. There are tricks in startups, as
there are in any domain, but they are an order of magnitude less
important than solving the real problem. A founder who knows nothing
about fundraising but has made something users love will have an
easier time raising money than one who knows every trick in the
book but has a flat usage graph. And more importantly, the founder
who has made something users love is the one who will go on to
succeed after raising the money.Though in a sense it's bad news in that you're deprived of one of
your most powerful weapons, I think it's exciting that gaming the
system stops working when you start a startup. It's exciting that
there even exist parts of the world where you win by doing good
work. Imagine how depressing the world would be if it were all
like school and big companies, where you either have to spend a lot
of time on bullshit things or lose to people who do.
[3]
I would
have been delighted if I'd realized in college that there were parts
of the real world where gaming the system mattered less than others,
and a few where it hardly mattered at all. But there are, and this
variation is one of the most important things to consider when
you're thinking about your future. How do you win in each type of
work, and what would you like to win by doing?
[4]
All-ConsumingThat brings us to our fourth counterintuitive point: startups are
all-consuming. If you start a startup, it will take over your life
to a degree you cannot imagine. And if your startup succeeds, it
will take over your life for a long time: for several years at the
very least, maybe for a decade, maybe for the rest of your working
life. So there is a real opportunity cost here.Larry Page may seem to have an enviable life, but there are aspects
of it that are unenviable. Basically at 25 he started running as
fast as he could and it must seem to him that he hasn't stopped to
catch his breath since. Every day new shit happens in the Google
empire that only the CEO can deal with, and he, as CEO, has to deal
with it. If he goes on vacation for even a week, a whole week's
backlog of shit accumulates. And he has to bear this uncomplainingly,
partly because as the company's daddy he can never show fear or
weakness, and partly because billionaires get less than zero sympathy
if they talk about having difficult lives. Which has the strange
side effect that the difficulty of being a successful startup founder
is concealed from almost everyone except those who've done it.Y Combinator has now funded several companies that can be called
big successes, and in every single case the founders say the same
thing. It never gets any easier. The nature of the problems change.
You're worrying about construction delays at your London office
instead of the broken air conditioner in your studio apartment.
But the total volume of worry never decreases; if anything it
increases.Starting a successful startup is similar to having kids in that
it's like a button you push that changes your life irrevocably.
And while it's truly wonderful having kids, there are a lot of
things that are easier to do before you have them than after. Many
of which will make you a better parent when you do have kids. And
since you can delay pushing the button for a while, most people in
rich countries do.Yet when it comes to startups, a lot of people seem to think they're
supposed to start them while they're still in college. Are you
crazy? And what are the universities thinking? They go out of
their way to ensure their students are well supplied with contraceptives,
and yet they're setting up entrepreneurship programs and startup
incubators left and right.To be fair, the universities have their hand forced here. A lot
of incoming students are interested in startups. Universities are,
at least de facto, expected to prepare them for their careers. So
students who want to start startups hope universities can teach
them about startups. And whether universities can do this or not,
there's some pressure to claim they can, lest they lose applicants
to other universities that do.Can universities teach students about startups? Yes and no. They
can teach students about startups, but as I explained before, this
is not what you need to know. What you need to learn about are the
needs of your own users, and you can't do that until you actually
start the company.
[5]
So starting a startup is intrinsically
something you can only really learn by doing it. And it's impossible
to do that in college, for the reason I just explained: startups
take over your life. You can't start a startup for real as a
student, because if you start a startup for real you're not a student
anymore. You may be nominally a student for a bit, but you won't even
be that for long.
[6]Given this dichotomy, which of the two paths should you take? Be
a real student and not start a startup, or start a real startup and
not be a student? I can answer that one for you. Do not start a
startup in college. How to start a startup is just a subset of a
bigger problem you're trying to solve: how to have a good life.
And though starting a startup can be part of a good life for a lot
of ambitious people, age 20 is not the optimal time to do it.
Starting a startup is like a brutally fast depth-first search. Most
people should still be searching breadth-first at 20.You can do things in your early 20s that you can't do as well before
or after, like plunge deeply into projects on a whim and travel
super cheaply with no sense of a deadline. For unambitious people,
this sort of thing is the dreaded "failure to launch," but for the
ambitious ones it can be an incomparably valuable sort of exploration.
If you start a startup at 20 and you're sufficiently successful,
you'll never get to do it.
[7]Mark Zuckerberg will never get to bum around a foreign country. He
can do other things most people can't, like charter jets to fly him
to foreign countries. But success has taken a lot of the serendipity
out of his life. Facebook is running him as much as he's running
Facebook. And while it can be very cool to be in the grip of a
project you consider your life's work, there are advantages to
serendipity too, especially early in life. Among other things it
gives you more options to choose your life's work from.There's not even a tradeoff here. You're not sacrificing anything
if you forgo starting a startup at 20, because you're more likely
to succeed if you wait. In the unlikely case that you're 20 and
one of your side projects takes off like Facebook did, you'll face
a choice of running with it or not, and it may be reasonable to run
with it. But the usual way startups take off is for the founders
to make them take off, and it's gratuitously
stupid to do that at 20.
TryShould you do it at any age? I realize I've made startups sound
pretty hard. If I haven't, let me try again: starting a startup
is really hard. What if it's too hard? How can you tell if you're
up to this challenge?The answer is the fifth counterintuitive point: you can't tell. Your
life so far may have given you some idea what your prospects might
be if you tried to become a mathematician, or a professional football
player. But unless you've had a very strange life you haven't done
much that was like being a startup founder.
Starting a startup will change you a lot. So what you're trying
to estimate is not just what you are, but what you could grow into,
and who can do that?For the past 9 years it was my job to predict whether people would
have what it took to start successful startups. It was easy to
tell how smart they were, and most people reading this will be over
that threshold. The hard part was predicting how tough and ambitious they would become. There
may be no one who has more experience at trying to predict that,
so I can tell you how much an expert can know about it, and the
answer is: not much. I learned to keep a completely open mind about
which of the startups in each batch would turn out to be the stars.The founders sometimes think they know. Some arrive feeling sure
they will ace Y Combinator just as they've aced every one of the (few,
artificial, easy) tests they've faced in life so far. Others arrive
wondering how they got in, and hoping YC doesn't discover whatever
mistake caused it to accept them. But there is little correlation
between founders' initial attitudes and how well their companies
do.I've read that the same is true in the military — that the
swaggering recruits are no more likely to turn out to be really
tough than the quiet ones. And probably for the same reason: that
the tests involved are so different from the ones in their previous
lives.If you're absolutely terrified of starting a startup, you probably
shouldn't do it. But if you're merely unsure whether you're up to
it, the only way to find out is to try. Just not now.
IdeasSo if you want to start a startup one day, what should you do in
college? There are only two things you need initially: an idea and
cofounders. And the m.o. for getting both is the same. Which leads
to our sixth and last counterintuitive point: that the way to get
startup ideas is not to try to think of startup ideas.I've written a whole essay on this,
so I won't repeat it all here. But the short version is that if
you make a conscious effort to think of startup ideas, the ideas
you come up with will not merely be bad, but bad and plausible-sounding,
meaning you'll waste a lot of time on them before realizing they're
bad.The way to come up with good startup ideas is to take a step back.
Instead of making a conscious effort to think of startup ideas,
turn your mind into the type that startup ideas form in without any
conscious effort. In fact, so unconsciously that you don't even
realize at first that they're startup ideas.This is not only possible, it's how Apple, Yahoo, Google, and
Facebook all got started. None of these companies were even meant
to be companies at first. They were all just side projects. The
best startups almost have to start as side projects, because great
ideas tend to be such outliers that your conscious mind would reject
them as ideas for companies.Ok, so how do you turn your mind into the type that startup ideas
form in unconsciously? (1) Learn a lot about things that matter,
then (2) work on problems that interest you (3) with people you
like and respect. The third part, incidentally, is how you get
cofounders at the same time as the idea.The first time I wrote that paragraph, instead of "learn a lot about
things that matter," I wrote "become good at some technology." But
that prescription, though sufficient, is too narrow. What was
special about Brian Chesky and Joe Gebbia was not that they were
experts in technology. They were good at design, and perhaps even
more importantly, they were good at organizing groups and making
projects happen. So you don't have to work on technology per se,
so long as you work on problems demanding enough to stretch you.What kind of problems are those? That is very hard to answer in
the general case. History is full of examples of young people who
were working on important problems that no
one else at the time thought were important, and in particular
that their parents didn't think were important. On the other hand,
history is even fuller of examples of parents who thought their
kids were wasting their time and who were right. So how do you
know when you're working on real stuff?
[8]I know how I know. Real problems are interesting, and I am
self-indulgent in the sense that I always want to work on interesting
things, even if no one else cares about them (in fact, especially
if no one else cares about them), and find it very hard to make
myself work on boring things, even if they're supposed to be
important.My life is full of case after case where I worked on something just
because it seemed interesting, and it turned out later to be useful
in some worldly way. Y
Combinator itself was something I only did because it seemed
interesting. So I seem to have some sort of internal compass that
helps me out. But I don't know what other people have in their
heads. Maybe if I think more about this I can come up with heuristics
for recognizing genuinely interesting problems, but for the moment
the best I can offer is the hopelessly question-begging advice that
if you have a taste for genuinely interesting problems, indulging
it energetically is the best way to prepare yourself for a startup.
And indeed, probably also the best way to live.
[9]But although I can't explain in the general case what counts as an
interesting problem, I can tell you about a large subset of them.
If you think of technology as something that's spreading like a
sort of fractal stain, every moving point on the edge represents
an interesting problem. So one guaranteed way to turn your mind
into the type that has good startup ideas is to get yourself to the
leading edge of some technology — to cause yourself, as Paul
Buchheit put it, to "live in the future." When you reach that point,
ideas that will seem to other people uncannily prescient will seem
obvious to you. You may not realize they're startup ideas, but
you'll know they're something that ought to exist.For example, back at Harvard in the mid 90s a fellow grad student
of my friends Robert and Trevor wrote his own voice over IP software.
He didn't mean it to be a startup, and he never tried to turn it
into one. He just wanted to talk to his girlfriend in Taiwan without
paying for long distance calls, and since he was an expert on
networks it seemed obvious to him that the way to do it was turn
the sound into packets and ship it over the Internet. He never did
any more with his software than talk to his girlfriend, but this
is exactly the way the best startups get started.So strangely enough the optimal thing to do in college if you want
to be a successful startup founder is not some sort of new, vocational
version of college focused on "entrepreneurship." It's the classic
version of college as education for its own sake. If you want to
start a startup after college, what you should do in college is
learn powerful things. And if you have genuine intellectual
curiosity, that's what you'll naturally tend to do if you just
follow your own inclinations.
[10]The component of entrepreneurship that really matters is domain
expertise. The way to become Larry Page was to become an expert
on search. And the way to become an expert on search was to be
driven by genuine curiosity, not some ulterior motive.At its best, starting a startup is merely an ulterior motive for
curiosity. And you'll do it best if you introduce the ulterior
motive toward the end of the process.So here is the ultimate advice for young would-be startup founders,
boiled down to two words: just learn.
Notes[1]
Some founders listen more than others, and this tends to be a
predictor of success. One of the things I
remember about the Airbnbs during YC is how intently they listened.[2]
In fact, this is one of the reasons startups are possible. If
big companies weren't plagued by internal inefficiencies, they'd
be proportionately more effective, leaving less room for startups.[3]
In a startup you have to spend a lot of time on schleps, but this sort of work is merely
unglamorous, not bogus.[4]
What should you do if your true calling is gaming the system?
Management consulting.[5]
The company may not be incorporated, but if you start to get
significant numbers of users, you've started it, whether you realize
it yet or not.[6]
It shouldn't be that surprising that colleges can't teach
students how to be good startup founders, because they can't teach
them how to be good employees either.The way universities "teach" students how to be employees is to
hand off the task to companies via internship programs. But you
couldn't do the equivalent thing for startups, because by definition
if the students did well they would never come back.[7]
Charles Darwin was 22 when he received an invitation to travel
aboard the HMS Beagle as a naturalist. It was only because he was
otherwise unoccupied, to a degree that alarmed his family, that he
could accept it. And yet if he hadn't we probably would not know
his name.[8]
Parents can sometimes be especially conservative in this
department. There are some whose definition of important problems
includes only those on the critical path to med school.[9]
I did manage to think of a heuristic for detecting whether you
have a taste for interesting ideas: whether you find known boring
ideas intolerable. Could you endure studying literary theory, or
working in middle management at a large company?[10]
In fact, if your goal is to start a startup, you can stick
even more closely to the ideal of a liberal education than past
generations have. Back when students focused mainly on getting a
job after college, they thought at least a little about how the
courses they took might look to an employer. And perhaps even
worse, they might shy away from taking a difficult class lest they
get a low grade, which would harm their all-important GPA. Good
news: users don't care what your GPA
was. And I've never heard of investors caring either. Y Combinator
certainly never asks what classes you took in college or what grades
you got in them.
Thanks to Sam Altman, Paul Buchheit, John Collison, Patrick
Collison, Jessica Livingston, Robert Morris, Geoff Ralston, and
Fred Wilson for reading drafts of this.
================================================
FILE: cookbook/pocketflow-a2a/README.md
================================================
# PocketFlow Agent with A2A Protocol
This project demonstrates how to take an existing agent built with the PocketFlow library and make it accessible to other agents using the **Agent-to-Agent (A2A) communication protocol**.
This implementation is based on the tutorial: [A2A Protocol Simply Explained: Here are 3 key differences to MCP!](https://zacharyhuang.substack.com/p/a2a-protocol-simply-explained-here)
## How it Works: A2A Integration
This project combines two main parts:
1. **PocketFlow Agent Logic:** The original agent code ([`nodes.py`](nodes.py), [`utils.py`](utils.py), [`flow.py`](flow.py)) defines the internal workflow (Decide -> Search -> Answer). This code is taken directly from the [PocketFlow Agent Tutorial](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent).
2. **A2A Server Wrapper:** Code from the [google/A2A samples repository](https://github.com/google/A2A/tree/main/samples/python) (`common/` directory) provides the necessary infrastructure to host the agent as an A2A-compliant server. *Note: Minor modifications were made to the common server/client code to add detailed logging for educational purposes.*
3. **The Bridge ([`task_manager.py`](task_manager.py)):** A custom `PocketFlowTaskManager` class acts as the bridge. It receives A2A requests (like `tasks/send`), extracts the user query, runs the PocketFlow `agent_flow`, takes the final result from the flow's shared state, and packages it back into an A2A `Task` object with the answer as an `Artifact`.
This demonstrates how a non-A2A agent framework can be exposed over the A2A protocol by implementing a specific `TaskManager`.
## Simplified Interaction Sequence
```mermaid
sequenceDiagram
participant Client as "Client ([minimal_a2a_client.py](a2a_client.py))"
participant Server as "Server (localhost:10003)"
Note over Client: User enters question
Client->>+Server: POST / (JSON-RPC Request: tasks/send)
Note over Server: Processes request internally (runs PocketFlow)
Server-->>-Client: HTTP 200 OK (JSON-RPC Response: result=Task)
Note over Client: Displays final answer
```
## Getting Started
### Prerequisites
* Python 3.10+ (due to type hinting used in the A2A `common` code)
* An OpenAI API Key
### Installation
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Set your OpenAI API key as an environment variable:
```bash
export OPENAI_API_KEY="your-api-key-here"
```
Let's do a quick check to make sure your API key is working properly:
```bash
python utils.py
```
3. Run the server from this directory:
```bash
python a2a_server.py --port 10003
```
You should see logs indicating the server has started on `http://localhost:10003`.
4. Run the Client in a *separate terminal*
```bash
python a2a_client.py --agent-url http://localhost:10003
```
5. Follow the instructions in the client terminal to ask questions. Type `:q` or `quit` to exit the client.
## Example Interaction Logs
**(Server Log - showing internal PocketFlow steps)**
```
2025-04-12 17:20:40,893 - __main__ - INFO - Starting PocketFlow A2A server on http://localhost:10003
INFO: Started server process [677223]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://localhost:10003 (Press CTRL+C to quit)
2025-04-12 17:20:57,647 - A2AServer - INFO - <- Received Request (ID: d3f3fb93350d47d9a94ca12bb62b656b):
{
"jsonrpc": "2.0",
"id": "d3f3fb93350d47d9a94ca12bb62b656b",
"method": "tasks/send",
"params": {
"id": "46c3ce7b941a4fff9b8e3b644d6db5f4",
"sessionId": "f3e12b8424c44241be881cd4bb8a269f",
"message": {
"role": "user",
"parts": [
{
"type": "text",
"text": "Who won the Nobel Prize in Physics 2024?"
}
]
},
"acceptedOutputModes": [
"text",
"text/plain"
]
}
}
2025-04-12 17:20:57,647 - task_manager - INFO - Received task send request: 46c3ce7b941a4fff9b8e3b644d6db5f4
2025-04-12 17:20:57,647 - common.server.task_manager - INFO - Upserting task 46c3ce7b941a4fff9b8e3b644d6db5f4
2025-04-12 17:20:57,647 - task_manager - INFO - Running PocketFlow for task 46c3ce7b941a4fff9b8e3b644d6db5f4...
🤔 Agent deciding what to do next...
2025-04-12 17:20:59,213 - httpx - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
🔍 Agent decided to search for: 2024 Nobel Prize in Physics winner
🌐 Searching the web for: 2024 Nobel Prize in Physics winner
2025-04-12 17:20:59,974 - primp - INFO - response: https://lite.duckduckgo.com/lite/ 200
📚 Found information, analyzing results...
🤔 Agent deciding what to do next...
2025-04-12 17:21:01,619 - httpx - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
💡 Agent decided to answer the question
✍️ Crafting final answer...
2025-04-12 17:21:03,833 - httpx - INFO - HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
✅ Answer generated successfully
2025-04-12 17:21:03,834 - task_manager - INFO - PocketFlow completed for task 46c3ce7b941a4fff9b8e3b644d6db5f4
2025-04-12 17:21:03,834 - A2AServer - INFO - -> Response (ID: d3f3fb93350d47d9a94ca12bb62b656b):
{
"jsonrpc": "2.0",
"id": "d3f3fb93350d47d9a94ca12bb62b656b",
"result": {
"id": "46c3ce7b941a4fff9b8e3b644d6db5f4",
"sessionId": "f3e12b8424c44241be881cd4bb8a269f",
"status": {
"state": "completed",
"timestamp": "2025-04-12T17:21:03.834542"
},
"artifacts": [
{
"parts": [
{
"type": "text",
"text": "The 2024 Nobel Prize in Physics was awarded to John J. Hopfield and Geoffrey Hinton for their foundational discoveries and inventions that have significantly advanced the field of machine learning through the use of artificial neural networks. Their pioneering work has been crucial in the development and implementation of algorithms that enable machines to learn and process information in a manner that mimics human cognitive functions. This advancement in artificial intelligence technology has had a profound impact on numerous industries, facilitating innovations across various applications, from image and speech recognition to self-driving cars."
}
],
"index": 0
}
],
"history": []
}
}
```
**(Client Log - showing request/response)**
```
Connecting to agent at: http://localhost:10003
Using Session ID: f3e12b8424c44241be881cd4bb8a269f
Enter your question (:q or quit to exit) > Who won the Nobel Prize in Physics 2024?
Sending task 46c3ce7b941a4fff9b8e3b644d6db5f4...
2025-04-12 17:20:57,643 - A2AClient - INFO - -> Sending Request (ID: d3f3fb93350d47d9a94ca12bb62b656b, Method: tasks/send):
{
"jsonrpc": "2.0",
"id": "d3f3fb93350d47d9a94ca12bb62b656b",
"method": "tasks/send",
"params": {
"id": "46c3ce7b941a4fff9b8e3b644d6db5f4",
"sessionId": "f3e12b8424c44241be881cd4bb8a269f",
"message": {
"role": "user",
"parts": [
{
"type": "text",
"text": "Who won the Nobel Prize in Physics 2024?"
}
]
},
"acceptedOutputModes": [
"text",
"text/plain"
]
}
}
2025-04-12 17:21:03,835 - httpx - INFO - HTTP Request: POST http://localhost:10003 "HTTP/1.1 200 OK"
2025-04-12 17:21:03,836 - A2AClient - INFO - <- Received HTTP Status 200 for Request (ID: d3f3fb93350d47d9a94ca12bb62b656b)
2025-04-12 17:21:03,836 - A2AClient - INFO - <- Received Success Response (ID: d3f3fb93350d47d9a94ca12bb62b656b):
{
"jsonrpc": "2.0",
"id": "d3f3fb93350d47d9a94ca12bb62b656b",
"result": {
"id": "46c3ce7b941a4fff9b8e3b644d6db5f4",
"sessionId": "f3e12b8424c44241be881cd4bb8a269f",
"status": {
"state": "completed",
"timestamp": "2025-04-12T17:21:03.834542"
},
"artifacts": [
{
"parts": [
{
"type": "text",
"text": "The 2024 Nobel Prize in Physics was awarded to John J. Hopfield and Geoffrey Hinton for their foundational discoveries and inventions that have significantly advanced the field of machine learning through the use of artificial neural networks. Their pioneering work has been crucial in the development and implementation of algorithms that enable machines to learn and process information in a manner that mimics human cognitive functions. This advancement in artificial intelligence technology has had a profound impact on numerous industries, facilitating innovations across various applications, from image and speech recognition to self-driving cars."
}
],
"index": 0
}
],
"history": []
}
}
Task 46c3ce7b941a4fff9b8e3b644d6db5f4 finished with state: completed
Agent Response:
The 2024 Nobel Prize in Physics was awarded to John J. Hopfield and Geoffrey Hinton for their foundational discoveries and inventions that have significantly advanced the field of machine learning through the use of artificial neural networks. Their pioneering work has been crucial in the development and implementation of algorithms that enable machines to learn and process information in a manner that mimics human cognitive functions. This advancement in artificial intelligence technology has had a profound impact on numerous industries, facilitating innovations across various applications, from image and speech recognition to self-driving cars.
```
## Key A2A Integration Points
To make the PocketFlow agent A2A-compatible, the following were essential:
1. **A2A Server ([`common/server/server.py`](common/server/server.py)):** An ASGI application (using Starlette/Uvicorn) that listens for HTTP POST requests, parses JSON-RPC, and routes requests based on the `method` field.
2. **A2A Data Types ([`common/types.py`](common/types.py)):** Pydantic models defining the structure of A2A messages, tasks, artifacts, errors, and the agent card, ensuring compliance with the `a2a.json` specification.
3. **Task Manager ([`task_manager.py`](task_manager.py)):** A custom class (`PocketFlowTaskManager`) inheriting from the common `InMemoryTaskManager`. Its primary role is implementing the `on_send_task` method (and potentially others like `on_send_task_subscribe` if streaming were supported). This method:
* Receives the validated A2A `SendTaskRequest`.
* Extracts the user's query (`TextPart`) from the request's `message`.
* Initializes the PocketFlow `shared_data` dictionary.
* Creates and runs the PocketFlow `agent_flow`.
* Retrieves the final answer from the `shared_data` dictionary *after* the flow completes.
* Updates the task's state (e.g., to `COMPLETED` or `FAILED`) in the `InMemoryTaskManager`'s store.
* Packages the final answer into an A2A `Artifact` containing a `TextPart`.
* Constructs the final A2A `Task` object for the response.
4. **Agent Card ([`a2a_server.py`](a2a_server.py)):** A Pydantic model (`AgentCard`) defining the agent's metadata (name, description, URL, capabilities, skills) served at `/.well-known/agent.json`.
5. **Server Entry Point ([`a2a_server.py`](a2a_server.py)):** A script that initializes the `AgentCard`, the `PocketFlowTaskManager`, and the `A2AServer`, then starts the Uvicorn server process.
================================================
FILE: cookbook/pocketflow-a2a/a2a_client.py
================================================
import asyncio
import asyncclick as click # Using asyncclick for async main
from uuid import uuid4
import json # For potentially inspecting raw errors
import anyio
import functools
import logging
# Import from the common directory placed alongside this script
from common.client import A2AClient
from common.types import (
TaskState,
A2AClientError,
TextPart, # Used to construct the message
JSONRPCResponse # Potentially useful for error checking
)
# --- Configure logging ---
# Set level to INFO to see client requests and responses
# Set level to DEBUG to see raw response bodies and SSE data lines
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Optionally silence overly verbose libraries
# logging.getLogger("httpx").setLevel(logging.WARNING)
# logging.getLogger("httpcore").setLevel(logging.WARNING)
# --- ANSI Colors (Optional but helpful) ---
C_RED = "\x1b[31m"
C_GREEN = "\x1b[32m"
C_YELLOW = "\x1b[33m"
C_BLUE = "\x1b[34m"
C_MAGENTA = "\x1b[35m"
C_CYAN = "\x1b[36m"
C_WHITE = "\x1b[37m"
C_GRAY = "\x1b[90m"
C_BRIGHT_MAGENTA = "\x1b[95m"
C_RESET = "\x1b[0m"
C_BOLD = "\x1b[1m"
def colorize(color, text):
return f"{color}{text}{C_RESET}"
@click.command()
@click.option(
"--agent-url",
default="http://localhost:10003", # Default to the port used in server __main__
help="URL of the PocketFlow A2A agent server.",
)
async def cli(agent_url: str):
"""Minimal CLI client to interact with an A2A agent."""
print(colorize(C_BRIGHT_MAGENTA, f"Connecting to agent at: {agent_url}"))
# Instantiate the client - only URL is needed if not fetching card first
# Note: The PocketFlow wrapper doesn't expose much via the AgentCard,
# so we skip fetching it for this minimal client.
client = A2AClient(url=agent_url)
sessionId = uuid4().hex # Generate a new session ID for this run
print(colorize(C_GRAY, f"Using Session ID: {sessionId}"))
while True:
taskId = uuid4().hex # Generate a new task ID for each interaction
try:
# Use functools.partial to prepare the prompt function call
prompt_func = functools.partial(
click.prompt,
colorize(C_CYAN, "\nEnter your question (:q or quit to exit)"),
prompt_suffix=" > ",
type=str
)
# Run the synchronous prompt function in a worker thread
prompt = await anyio.to_thread.run_sync(prompt_func)
except (EOFError, RuntimeError, KeyboardInterrupt):
# Catch potential errors during input or if stdin closes
print(colorize(C_RED, "\nInput closed or interrupted. Exiting."))
break
if prompt.lower() in [":q", "quit"]:
print(colorize(C_YELLOW, "Exiting client."))
break
# --- Construct A2A Request Payload ---
payload = {
"id": taskId,
"sessionId": sessionId,
"message": {
"role": "user",
"parts": [
{
"type": "text", # Explicitly match TextPart structure
"text": prompt,
}
],
},
"acceptedOutputModes": ["text", "text/plain"], # What the client wants back
# historyLength could be added if needed
}
print(colorize(C_GRAY, f"Sending task {taskId}..."))
try:
# --- Send Task (Non-Streaming) ---
response = await client.send_task(payload)
# --- Process Response ---
if response.error:
print(colorize(C_RED, f"Error from agent (Code: {response.error.code}): {response.error.message}"))
if response.error.data:
print(colorize(C_GRAY, f"Error Data: {response.error.data}"))
elif response.result:
task_result = response.result
print(colorize(C_GREEN, f"Task {task_result.id} finished with state: {task_result.status.state}"))
final_answer = "Agent did not provide a final artifact."
# Extract answer from artifacts (as implemented in PocketFlowTaskManager)
if task_result.artifacts:
try:
# Find the first text part in the first artifact
first_artifact = task_result.artifacts[0]
first_text_part = next(
(p for p in first_artifact.parts if isinstance(p, TextPart)),
None
)
if first_text_part:
final_answer = first_text_part.text
else:
final_answer = f"(Non-text artifact received: {first_artifact.parts})"
except (IndexError, AttributeError, TypeError) as e:
final_answer = f"(Error parsing artifact: {e})"
elif task_result.status.message and task_result.status.message.parts:
# Fallback to status message if no artifact
try:
first_text_part = next(
(p for p in task_result.status.message.parts if isinstance(p, TextPart)),
None
)
if first_text_part:
final_answer = f"(Final status message: {first_text_part.text})"
except (AttributeError, TypeError) as e:
final_answer = f"(Error parsing status message: {e})"
print(colorize(C_BOLD + C_WHITE, f"\nAgent Response:\n{final_answer}"))
else:
# Should not happen if error is None
print(colorize(C_YELLOW, "Received response with no result and no error."))
except A2AClientError as e:
print(colorize(C_RED, f"\nClient Error: {e}"))
except Exception as e:
print(colorize(C_RED, f"\nAn unexpected error occurred: {e}"))
if __name__ == "__main__":
asyncio.run(cli())
================================================
FILE: cookbook/pocketflow-a2a/a2a_server.py
================================================
import click
import logging
import os
# Import from the common code you copied
from common.server import A2AServer
from common.types import AgentCard, AgentCapabilities, AgentSkill, MissingAPIKeyError
# Import your custom TaskManager (which now imports from your original files)
from task_manager import PocketFlowTaskManager
# --- Configure logging ---
# Set level to INFO to see server start, requests, responses
# Set level to DEBUG to see raw response bodies from client
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# Optionally silence overly verbose libraries
# logging.getLogger("httpx").setLevel(logging.WARNING)
# logging.getLogger("httpcore").setLevel(logging.WARNING)
# logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
logger = logging.getLogger(__name__)
@click.command()
@click.option("--host", "host", default="localhost")
@click.option("--port", "port", default=10003) # Use a different port from other agents
def main(host, port):
"""Starts the PocketFlow A2A Agent server."""
try:
# Check for necessary API keys (add others if needed)
if not os.getenv("OPENAI_API_KEY"):
raise MissingAPIKeyError("OPENAI_API_KEY environment variable not set.")
# --- Define the Agent Card ---
capabilities = AgentCapabilities(
streaming=False, # This simple implementation is synchronous
pushNotifications=False,
stateTransitionHistory=False # PocketFlow state isn't exposed via A2A history
)
skill = AgentSkill(
id="web_research_qa",
name="Web Research and Answering",
description="Answers questions using web search results when necessary.",
tags=["research", "qa", "web search"],
examples=[
"Who won the Nobel Prize in Physics 2024?",
"What is quantum computing?",
"Summarize the latest news about AI.",
],
# Input/Output modes defined in the TaskManager
inputModes=PocketFlowTaskManager.SUPPORTED_CONTENT_TYPES,
outputModes=PocketFlowTaskManager.SUPPORTED_CONTENT_TYPES,
)
agent_card = AgentCard(
name="PocketFlow Research Agent (A2A Wrapped)",
description="A simple research agent based on PocketFlow, made accessible via A2A.",
url=f"http://{host}:{port}/", # The endpoint A2A clients will use
version="0.1.0-a2a",
capabilities=capabilities,
skills=[skill],
# Assuming no specific provider or auth for this example
provider=None,
authentication=None,
defaultInputModes=PocketFlowTaskManager.SUPPORTED_CONTENT_TYPES,
defaultOutputModes=PocketFlowTaskManager.SUPPORTED_CONTENT_TYPES,
)
# --- Initialize and Start Server ---
task_manager = PocketFlowTaskManager() # Instantiate your custom manager
server = A2AServer(
agent_card=agent_card,
task_manager=task_manager,
host=host,
port=port,
)
logger.info(f"Starting PocketFlow A2A server on http://{host}:{port}")
server.start()
except MissingAPIKeyError as e:
logger.error(f"Configuration Error: {e}")
exit(1)
except Exception as e:
logger.error(f"An error occurred during server startup: {e}", exc_info=True)
exit(1)
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-a2a/common/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-a2a/common/client/__init__.py
================================================
from .client import A2AClient
from .card_resolver import A2ACardResolver
__all__ = ["A2AClient", "A2ACardResolver"]
================================================
FILE: cookbook/pocketflow-a2a/common/client/card_resolver.py
================================================
import httpx
from common.types import (
AgentCard,
A2AClientJSONError,
)
import json
class A2ACardResolver:
def __init__(self, base_url, agent_card_path="/.well-known/agent.json"):
self.base_url = base_url.rstrip("/")
self.agent_card_path = agent_card_path.lstrip("/")
def get_agent_card(self) -> AgentCard:
with httpx.Client() as client:
response = client.get(self.base_url + "/" + self.agent_card_path)
response.raise_for_status()
try:
return AgentCard(**response.json())
except json.JSONDecodeError as e:
raise A2AClientJSONError(str(e)) from e
================================================
FILE: cookbook/pocketflow-a2a/common/client/client.py
================================================
import httpx
from httpx_sse import connect_sse
from typing import Any, AsyncIterable
from common.types import (
AgentCard,
GetTaskRequest,
SendTaskRequest,
SendTaskResponse,
JSONRPCRequest,
JSONRPCResponse,
JSONRPCError,
GetTaskResponse,
CancelTaskResponse,
CancelTaskRequest,
SetTaskPushNotificationRequest,
SetTaskPushNotificationResponse,
GetTaskPushNotificationRequest,
GetTaskPushNotificationResponse,
A2AClientHTTPError,
A2AClientJSONError,
SendTaskStreamingRequest,
SendTaskStreamingResponse,
Task,
TaskPushNotificationConfig,
TaskStatusUpdateEvent,
TaskArtifactUpdateEvent,
)
import json
import logging
# Configure a logger specific to the client
logger = logging.getLogger("A2AClient")
class A2AClientError(Exception):
"""Base class for A2A client errors"""
def __init__(self, message):
super().__init__(message)
class RpcError(Exception):
code: int
data: Any = None
def __init__(self, code: int, message: str, data: Any = None):
super().__init__(message)
self.name = "RpcError"
self.code = code
self.data = data
class A2AClient:
def __init__(self, agent_card: AgentCard = None, url: str = None):
if agent_card:
self.url = agent_card.url.rstrip("/")
elif url:
self.url = url.rstrip("/")
else:
raise ValueError("Must provide either agent_card or url")
self.fetchImpl = httpx.AsyncClient(timeout=None)
def _generateRequestId(self):
import time
return int(time.time() * 1000)
async def _send_request(self, request: JSONRPCRequest) -> dict[str, Any]:
req_id = request.id
req_method = request.method
req_dump = request.model_dump(exclude_none=True)
logger.info(f"-> Sending Request (ID: {req_id}, Method: {req_method}):\n{json.dumps(req_dump, indent=2)}")
try:
response = await self.fetchImpl.post(
self.url, json=req_dump, timeout=60.0
)
logger.info(f"<- Received HTTP Status {response.status_code} for Request (ID: {req_id})")
response_text = await response.aread()
logger.debug(f"Raw Response Body (ID: {req_id}):\n{response_text.decode('utf-8', errors='replace')}")
response.raise_for_status()
try:
json_response = json.loads(response_text)
except json.JSONDecodeError as e:
logger.error(f"Failed to decode JSON response (ID: {req_id}): {e}")
raise A2AClientJSONError(f"Failed to decode JSON: {e}") from e
if "error" in json_response and json_response["error"] is not None:
rpc_error = json_response["error"]
logger.warning(f"<- Received JSON-RPC Error (ID: {req_id}): Code={rpc_error.get('code')}, Msg='{rpc_error.get('message')}'")
raise RpcError(rpc_error.get("code", -32000), rpc_error.get("message", "Unknown RPC Error"), rpc_error.get("data"))
logger.info(f"<- Received Success Response (ID: {req_id}):\n{json.dumps(json_response, indent=2)}")
return json_response
except httpx.HTTPStatusError as e:
logger.error(f"HTTP Error for Request (ID: {req_id}): {e.response.status_code} - {e.request.url}")
error_body = await e.response.aread()
raise A2AClientHTTPError(e.response.status_code, f"{e}. Body: {error_body.decode('utf-8', errors='replace')}") from e
except httpx.RequestError as e:
logger.error(f"Request Error for (ID: {req_id}): {e}")
raise A2AClientError(f"Network or request error: {e}") from e
except RpcError:
raise
except Exception as e:
logger.error(f"Unexpected error during request (ID: {req_id}): {e}", exc_info=True)
raise A2AClientError(f"Unexpected error: {e}") from e
async def send_task(self, payload: dict[str, Any]) -> SendTaskResponse:
request = SendTaskRequest(params=payload)
response_dict = await self._send_request(request)
return SendTaskResponse(**response_dict)
async def send_task_streaming(
self, payload: dict[str, Any]
) -> AsyncIterable[SendTaskStreamingResponse]:
request = SendTaskStreamingRequest(params=payload)
req_id = request.id
req_dump = request.model_dump(exclude_none=True)
logger.info(f"-> Sending Streaming Request (ID: {req_id}, Method: {request.method}):\n{json.dumps(req_dump, indent=2)}")
try:
async with self.fetchImpl.stream("POST", self.url, json=req_dump, timeout=None) as response:
logger.info(f"<- Received HTTP Status {response.status_code} for Streaming Request (ID: {req_id})")
response.raise_for_status()
buffer = ""
async for line in response.aiter_lines():
if not line:
if buffer.startswith("data:"):
data_str = buffer[len("data:"):].strip()
logger.debug(f"Received SSE Data Line (ID: {req_id}): {data_str}")
try:
sse_data_dict = json.loads(data_str)
yield SendTaskStreamingResponse(**sse_data_dict)
except json.JSONDecodeError as e:
logger.error(f"Failed to decode SSE JSON (ID: {req_id}): {e}. Data: '{data_str}'")
except Exception as e:
logger.error(f"Error processing SSE data (ID: {req_id}): {e}. Data: '{data_str}'", exc_info=True)
elif buffer:
logger.debug(f"Received non-data SSE line (ID: {req_id}): {buffer}")
buffer = ""
else:
buffer += line + "\n"
if buffer:
logger.warning(f"SSE stream ended with partial data in buffer (ID: {req_id}): {buffer}")
logger.info(f"SSE Stream ended for request ID: {req_id}")
except httpx.HTTPStatusError as e:
logger.error(f"HTTP Error during streaming connection (ID: {req_id}): {e.response.status_code} - {e.request.url}")
error_body = await e.response.aread()
raise A2AClientHTTPError(e.response.status_code, f"{e}. Body: {error_body.decode('utf-8', errors='replace')}") from e
except httpx.RequestError as e:
logger.error(f"Request Error during streaming (ID: {req_id}): {e}")
raise A2AClientError(f"Network or request error during streaming: {e}") from e
except Exception as e:
logger.error(f"Unexpected error during streaming (ID: {req_id}): {e}", exc_info=True)
raise A2AClientError(f"Unexpected streaming error: {e}") from e
async def get_task(self, payload: dict[str, Any]) -> GetTaskResponse:
request = GetTaskRequest(params=payload)
response_dict = await self._send_request(request)
return GetTaskResponse(**response_dict)
async def cancel_task(self, payload: dict[str, Any]) -> CancelTaskResponse:
request = CancelTaskRequest(params=payload)
response_dict = await self._send_request(request)
return CancelTaskResponse(**response_dict)
async def set_task_callback(
self, payload: dict[str, Any]
) -> SetTaskPushNotificationResponse:
request = SetTaskPushNotificationRequest(params=payload)
response_dict = await self._send_request(request)
return SetTaskPushNotificationResponse(**response_dict)
async def get_task_callback(
self, payload: dict[str, Any]
) -> GetTaskPushNotificationResponse:
request = GetTaskPushNotificationRequest(params=payload)
response_dict = await self._send_request(request)
return GetTaskPushNotificationResponse(**response_dict)
================================================
FILE: cookbook/pocketflow-a2a/common/server/__init__.py
================================================
from .server import A2AServer
from .task_manager import TaskManager, InMemoryTaskManager
__all__ = ["A2AServer", "TaskManager", "InMemoryTaskManager"]
================================================
FILE: cookbook/pocketflow-a2a/common/server/server.py
================================================
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from sse_starlette.sse import EventSourceResponse
from starlette.requests import Request
from common.types import (
A2ARequest,
JSONRPCResponse,
InvalidRequestError,
JSONParseError,
GetTaskRequest,
CancelTaskRequest,
SendTaskRequest,
SetTaskPushNotificationRequest,
GetTaskPushNotificationRequest,
InternalError,
AgentCard,
TaskResubscriptionRequest,
SendTaskStreamingRequest,
Message,
)
from pydantic import ValidationError
import json
from typing import AsyncIterable, Any
from common.server.task_manager import TaskManager
import logging
# Configure a logger specific to the server
logger = logging.getLogger("A2AServer")
class A2AServer:
def __init__(
self,
host="0.0.0.0",
port=5000,
endpoint="/",
agent_card: AgentCard = None,
task_manager: TaskManager = None,
):
self.host = host
self.port = port
self.endpoint = endpoint
self.task_manager = task_manager
self.agent_card = agent_card
self.app = Starlette()
self.app.add_route(self.endpoint, self._process_request, methods=["POST"])
self.app.add_route(
"/.well-known/agent.json", self._get_agent_card, methods=["GET"]
)
def start(self):
if self.agent_card is None:
raise ValueError("agent_card is not defined")
if self.task_manager is None:
raise ValueError("request_handler is not defined")
import uvicorn
# Basic logging config moved to __main__.py for application-level control
uvicorn.run(self.app, host=self.host, port=self.port)
def _get_agent_card(self, request: Request) -> JSONResponse:
logger.info("Serving Agent Card request")
return JSONResponse(self.agent_card.model_dump(exclude_none=True))
async def _process_request(self, request: Request):
request_id_for_log = "N/A" # Default if parsing fails early
raw_body = b""
try:
# Log raw body first
raw_body = await request.body()
body = json.loads(raw_body) # Attempt parsing
request_id_for_log = body.get("id", "N/A") # Get ID if possible
logger.info(f"<- Received Request (ID: {request_id_for_log}):\n{json.dumps(body, indent=2)}")
json_rpc_request = A2ARequest.validate_python(body)
# Route based on method (same as before)
if isinstance(json_rpc_request, GetTaskRequest):
result = await self.task_manager.on_get_task(json_rpc_request)
elif isinstance(json_rpc_request, SendTaskRequest):
result = await self.task_manager.on_send_task(json_rpc_request)
elif isinstance(json_rpc_request, SendTaskStreamingRequest):
result = await self.task_manager.on_send_task_subscribe(
json_rpc_request
)
elif isinstance(json_rpc_request, CancelTaskRequest):
result = await self.task_manager.on_cancel_task(json_rpc_request)
elif isinstance(json_rpc_request, SetTaskPushNotificationRequest):
result = await self.task_manager.on_set_task_push_notification(json_rpc_request)
elif isinstance(json_rpc_request, GetTaskPushNotificationRequest):
result = await self.task_manager.on_get_task_push_notification(json_rpc_request)
elif isinstance(json_rpc_request, TaskResubscriptionRequest):
result = await self.task_manager.on_resubscribe_to_task(
json_rpc_request
)
else:
logger.warning(f"Unexpected request type: {type(json_rpc_request)}")
raise ValueError(f"Unexpected request type: {type(request)}")
return self._create_response(result) # Pass result to response creation
except json.decoder.JSONDecodeError as e:
logger.error(f"JSON Parse Error for Request body: <<<{raw_body.decode('utf-8', errors='replace')}>>>\nError: {e}")
return self._handle_exception(e, request_id_for_log) # Pass ID if known
except ValidationError as e:
logger.error(f"Request Validation Error (ID: {request_id_for_log}): {e.json()}")
return self._handle_exception(e, request_id_for_log)
except Exception as e:
logger.error(f"Unhandled Exception processing request (ID: {request_id_for_log}): {e}", exc_info=True)
return self._handle_exception(e, request_id_for_log) # Pass ID if known
def _handle_exception(self, e: Exception, req_id=None) -> JSONResponse: # Accept req_id
if isinstance(e, json.decoder.JSONDecodeError):
json_rpc_error = JSONParseError()
elif isinstance(e, ValidationError):
json_rpc_error = InvalidRequestError(data=json.loads(e.json()))
else:
# Log the full exception details
logger.error(f"Internal Server Error (ReqID: {req_id}): {e}", exc_info=True)
json_rpc_error = InternalError(message=f"Internal Server Error: {type(e).__name__}")
response = JSONRPCResponse(id=req_id, error=json_rpc_error)
response_dump = response.model_dump(exclude_none=True)
logger.info(f"-> Sending Error Response (ReqID: {req_id}):\n{json.dumps(response_dump, indent=2)}")
# A2A errors are still sent with HTTP 200
return JSONResponse(response_dump, status_code=200)
def _create_response(self, result: Any) -> JSONResponse | EventSourceResponse:
if isinstance(result, AsyncIterable):
# Streaming response
async def event_generator(result_stream) -> AsyncIterable[dict[str, str]]:
stream_request_id = None # Capture ID from the first event if possible
try:
async for item in result_stream:
# Log each streamed item
response_json = item.model_dump_json(exclude_none=True)
stream_request_id = item.id # Update ID
logger.info(f"-> Sending SSE Event (ID: {stream_request_id}):\n{json.dumps(json.loads(response_json), indent=2)}")
yield {"data": response_json}
logger.info(f"SSE Stream ended for request ID: {stream_request_id}")
except Exception as e:
logger.error(f"Error during SSE generation (ReqID: {stream_request_id}): {e}", exc_info=True)
# Optionally yield an error event if the protocol allows/requires it
# error_payload = JSONRPCResponse(id=stream_request_id, error=InternalError(message=f"SSE Error: {e}"))
# yield {"data": error_payload.model_dump_json(exclude_none=True)}
logger.info("Starting SSE stream...") # Log stream start
return EventSourceResponse(event_generator(result))
elif isinstance(result, JSONRPCResponse):
# Standard JSON response
response_dump = result.model_dump(exclude_none=True)
log_id = result.id if result.id is not None else "N/A (Notification?)"
log_prefix = "->"
log_type = "Response"
if result.error:
log_prefix = "-> Sending Error"
log_type = "Error Response"
logger.info(f"{log_prefix} {log_type} (ID: {log_id}):\n{json.dumps(response_dump, indent=2)}")
return JSONResponse(response_dump)
else:
# This should ideally not happen if task manager returns correctly
logger.error(f"Task manager returned unexpected type: {type(result)}")
err_resp = JSONRPCResponse(id=None, error=InternalError(message="Invalid internal response type"))
return JSONResponse(err_resp.model_dump(exclude_none=True), status_code=500)
================================================
FILE: cookbook/pocketflow-a2a/common/server/task_manager.py
================================================
from abc import ABC, abstractmethod
from typing import Union, AsyncIterable, List
from common.types import Task
from common.types import (
JSONRPCResponse,
TaskIdParams,
TaskQueryParams,
GetTaskRequest,
TaskNotFoundError,
SendTaskRequest,
CancelTaskRequest,
TaskNotCancelableError,
SetTaskPushNotificationRequest,
GetTaskPushNotificationRequest,
GetTaskResponse,
CancelTaskResponse,
SendTaskResponse,
SetTaskPushNotificationResponse,
GetTaskPushNotificationResponse,
PushNotificationNotSupportedError,
TaskSendParams,
TaskStatus,
TaskState,
TaskResubscriptionRequest,
SendTaskStreamingRequest,
SendTaskStreamingResponse,
Artifact,
PushNotificationConfig,
TaskStatusUpdateEvent,
JSONRPCError,
TaskPushNotificationConfig,
InternalError,
)
from common.server.utils import new_not_implemented_error
import asyncio
import logging
logger = logging.getLogger(__name__)
class TaskManager(ABC):
@abstractmethod
async def on_get_task(self, request: GetTaskRequest) -> GetTaskResponse:
pass
@abstractmethod
async def on_cancel_task(self, request: CancelTaskRequest) -> CancelTaskResponse:
pass
@abstractmethod
async def on_send_task(self, request: SendTaskRequest) -> SendTaskResponse:
pass
@abstractmethod
async def on_send_task_subscribe(
self, request: SendTaskStreamingRequest
) -> Union[AsyncIterable[SendTaskStreamingResponse], JSONRPCResponse]:
pass
@abstractmethod
async def on_set_task_push_notification(
self, request: SetTaskPushNotificationRequest
) -> SetTaskPushNotificationResponse:
pass
@abstractmethod
async def on_get_task_push_notification(
self, request: GetTaskPushNotificationRequest
) -> GetTaskPushNotificationResponse:
pass
@abstractmethod
async def on_resubscribe_to_task(
self, request: TaskResubscriptionRequest
) -> Union[AsyncIterable[SendTaskResponse], JSONRPCResponse]:
pass
class InMemoryTaskManager(TaskManager):
def __init__(self):
self.tasks: dict[str, Task] = {}
self.push_notification_infos: dict[str, PushNotificationConfig] = {}
self.lock = asyncio.Lock()
self.task_sse_subscribers: dict[str, List[asyncio.Queue]] = {}
self.subscriber_lock = asyncio.Lock()
async def on_get_task(self, request: GetTaskRequest) -> GetTaskResponse:
logger.info(f"Getting task {request.params.id}")
task_query_params: TaskQueryParams = request.params
async with self.lock:
task = self.tasks.get(task_query_params.id)
if task is None:
return GetTaskResponse(id=request.id, error=TaskNotFoundError())
task_result = self.append_task_history(
task, task_query_params.historyLength
)
return GetTaskResponse(id=request.id, result=task_result)
async def on_cancel_task(self, request: CancelTaskRequest) -> CancelTaskResponse:
logger.info(f"Cancelling task {request.params.id}")
task_id_params: TaskIdParams = request.params
async with self.lock:
task = self.tasks.get(task_id_params.id)
if task is None:
return CancelTaskResponse(id=request.id, error=TaskNotFoundError())
return CancelTaskResponse(id=request.id, error=TaskNotCancelableError())
@abstractmethod
async def on_send_task(self, request: SendTaskRequest) -> SendTaskResponse:
pass
@abstractmethod
async def on_send_task_subscribe(
self, request: SendTaskStreamingRequest
) -> Union[AsyncIterable[SendTaskStreamingResponse], JSONRPCResponse]:
pass
async def set_push_notification_info(self, task_id: str, notification_config: PushNotificationConfig):
async with self.lock:
task = self.tasks.get(task_id)
if task is None:
raise ValueError(f"Task not found for {task_id}")
self.push_notification_infos[task_id] = notification_config
return
async def get_push_notification_info(self, task_id: str) -> PushNotificationConfig:
async with self.lock:
task = self.tasks.get(task_id)
if task is None:
raise ValueError(f"Task not found for {task_id}")
return self.push_notification_infos[task_id]
return
async def has_push_notification_info(self, task_id: str) -> bool:
async with self.lock:
return task_id in self.push_notification_infos
async def on_set_task_push_notification(
self, request: SetTaskPushNotificationRequest
) -> SetTaskPushNotificationResponse:
logger.info(f"Setting task push notification {request.params.id}")
task_notification_params: TaskPushNotificationConfig = request.params
try:
await self.set_push_notification_info(task_notification_params.id, task_notification_params.pushNotificationConfig)
except Exception as e:
logger.error(f"Error while setting push notification info: {e}")
return JSONRPCResponse(
id=request.id,
error=InternalError(
message="An error occurred while setting push notification info"
),
)
return SetTaskPushNotificationResponse(id=request.id, result=task_notification_params)
async def on_get_task_push_notification(
self, request: GetTaskPushNotificationRequest
) -> GetTaskPushNotificationResponse:
logger.info(f"Getting task push notification {request.params.id}")
task_params: TaskIdParams = request.params
try:
notification_info = await self.get_push_notification_info(task_params.id)
except Exception as e:
logger.error(f"Error while getting push notification info: {e}")
return GetTaskPushNotificationResponse(
id=request.id,
error=InternalError(
message="An error occurred while getting push notification info"
),
)
return GetTaskPushNotificationResponse(id=request.id, result=TaskPushNotificationConfig(id=task_params.id, pushNotificationConfig=notification_info))
async def upsert_task(self, task_send_params: TaskSendParams) -> Task:
logger.info(f"Upserting task {task_send_params.id}")
async with self.lock:
task = self.tasks.get(task_send_params.id)
if task is None:
task = Task(
id=task_send_params.id,
sessionId = task_send_params.sessionId,
messages=[task_send_params.message],
status=TaskStatus(state=TaskState.SUBMITTED),
history=[task_send_params.message],
)
self.tasks[task_send_params.id] = task
else:
task.history.append(task_send_params.message)
return task
async def on_resubscribe_to_task(
self, request: TaskResubscriptionRequest
) -> Union[AsyncIterable[SendTaskStreamingResponse], JSONRPCResponse]:
return new_not_implemented_error(request.id)
async def update_store(
self, task_id: str, status: TaskStatus, artifacts: list[Artifact]
) -> Task:
async with self.lock:
try:
task = self.tasks[task_id]
except KeyError:
logger.error(f"Task {task_id} not found for updating the task")
raise ValueError(f"Task {task_id} not found")
task.status = status
if status.message is not None:
task.history.append(status.message)
if artifacts is not None:
if task.artifacts is None:
task.artifacts = []
task.artifacts.extend(artifacts)
return task
def append_task_history(self, task: Task, historyLength: int | None):
new_task = task.model_copy()
if historyLength is not None and historyLength > 0:
new_task.history = new_task.history[-historyLength:]
else:
new_task.history = []
return new_task
async def setup_sse_consumer(self, task_id: str, is_resubscribe: bool = False):
async with self.subscriber_lock:
if task_id not in self.task_sse_subscribers:
if is_resubscribe:
raise ValueError("Task not found for resubscription")
else:
self.task_sse_subscribers[task_id] = []
sse_event_queue = asyncio.Queue(maxsize=0) # <=0 is unlimited
self.task_sse_subscribers[task_id].append(sse_event_queue)
return sse_event_queue
async def enqueue_events_for_sse(self, task_id, task_update_event):
async with self.subscriber_lock:
if task_id not in self.task_sse_subscribers:
return
current_subscribers = self.task_sse_subscribers[task_id]
for subscriber in current_subscribers:
await subscriber.put(task_update_event)
async def dequeue_events_for_sse(
self, request_id, task_id, sse_event_queue: asyncio.Queue
) -> AsyncIterable[SendTaskStreamingResponse] | JSONRPCResponse:
try:
while True:
event = await sse_event_queue.get()
if isinstance(event, JSONRPCError):
yield SendTaskStreamingResponse(id=request_id, error=event)
break
yield SendTaskStreamingResponse(id=request_id, result=event)
if isinstance(event, TaskStatusUpdateEvent) and event.final:
break
finally:
async with self.subscriber_lock:
if task_id in self.task_sse_subscribers:
self.task_sse_subscribers[task_id].remove(sse_event_queue)
================================================
FILE: cookbook/pocketflow-a2a/common/server/utils.py
================================================
from common.types import (
JSONRPCResponse,
ContentTypeNotSupportedError,
UnsupportedOperationError,
)
from typing import List
def are_modalities_compatible(
server_output_modes: List[str], client_output_modes: List[str]
):
"""Modalities are compatible if they are both non-empty
and there is at least one common element."""
if client_output_modes is None or len(client_output_modes) == 0:
return True
if server_output_modes is None or len(server_output_modes) == 0:
return True
return any(x in server_output_modes for x in client_output_modes)
def new_incompatible_types_error(request_id):
return JSONRPCResponse(id=request_id, error=ContentTypeNotSupportedError())
def new_not_implemented_error(request_id):
return JSONRPCResponse(id=request_id, error=UnsupportedOperationError())
================================================
FILE: cookbook/pocketflow-a2a/common/types.py
================================================
from typing import Union, Any
from pydantic import BaseModel, Field, TypeAdapter
from typing import Literal, List, Annotated, Optional
from datetime import datetime
from pydantic import model_validator, ConfigDict, field_serializer
from uuid import uuid4
from enum import Enum
from typing_extensions import Self
class TaskState(str, Enum):
SUBMITTED = "submitted"
WORKING = "working"
INPUT_REQUIRED = "input-required"
COMPLETED = "completed"
CANCELED = "canceled"
FAILED = "failed"
UNKNOWN = "unknown"
class TextPart(BaseModel):
type: Literal["text"] = "text"
text: str
metadata: dict[str, Any] | None = None
class FileContent(BaseModel):
name: str | None = None
mimeType: str | None = None
bytes: str | None = None
uri: str | None = None
@model_validator(mode="after")
def check_content(self) -> Self:
if not (self.bytes or self.uri):
raise ValueError("Either 'bytes' or 'uri' must be present in the file data")
if self.bytes and self.uri:
raise ValueError(
"Only one of 'bytes' or 'uri' can be present in the file data"
)
return self
class FilePart(BaseModel):
type: Literal["file"] = "file"
file: FileContent
metadata: dict[str, Any] | None = None
class DataPart(BaseModel):
type: Literal["data"] = "data"
data: dict[str, Any]
metadata: dict[str, Any] | None = None
Part = Annotated[Union[TextPart, FilePart, DataPart], Field(discriminator="type")]
class Message(BaseModel):
role: Literal["user", "agent"]
parts: List[Part]
metadata: dict[str, Any] | None = None
class TaskStatus(BaseModel):
state: TaskState
message: Message | None = None
timestamp: datetime = Field(default_factory=datetime.now)
@field_serializer("timestamp")
def serialize_dt(self, dt: datetime, _info):
return dt.isoformat()
class Artifact(BaseModel):
name: str | None = None
description: str | None = None
parts: List[Part]
metadata: dict[str, Any] | None = None
index: int = 0
append: bool | None = None
lastChunk: bool | None = None
class Task(BaseModel):
id: str
sessionId: str | None = None
status: TaskStatus
artifacts: List[Artifact] | None = None
history: List[Message] | None = None
metadata: dict[str, Any] | None = None
class TaskStatusUpdateEvent(BaseModel):
id: str
status: TaskStatus
final: bool = False
metadata: dict[str, Any] | None = None
class TaskArtifactUpdateEvent(BaseModel):
id: str
artifact: Artifact
metadata: dict[str, Any] | None = None
class AuthenticationInfo(BaseModel):
model_config = ConfigDict(extra="allow")
schemes: List[str]
credentials: str | None = None
class PushNotificationConfig(BaseModel):
url: str
token: str | None = None
authentication: AuthenticationInfo | None = None
class TaskIdParams(BaseModel):
id: str
metadata: dict[str, Any] | None = None
class TaskQueryParams(TaskIdParams):
historyLength: int | None = None
class TaskSendParams(BaseModel):
id: str
sessionId: str = Field(default_factory=lambda: uuid4().hex)
message: Message
acceptedOutputModes: Optional[List[str]] = None
pushNotification: PushNotificationConfig | None = None
historyLength: int | None = None
metadata: dict[str, Any] | None = None
class TaskPushNotificationConfig(BaseModel):
id: str
pushNotificationConfig: PushNotificationConfig
## RPC Messages
class JSONRPCMessage(BaseModel):
jsonrpc: Literal["2.0"] = "2.0"
id: int | str | None = Field(default_factory=lambda: uuid4().hex)
class JSONRPCRequest(JSONRPCMessage):
method: str
params: dict[str, Any] | None = None
class JSONRPCError(BaseModel):
code: int
message: str
data: Any | None = None
class JSONRPCResponse(JSONRPCMessage):
result: Any | None = None
error: JSONRPCError | None = None
class SendTaskRequest(JSONRPCRequest):
method: Literal["tasks/send"] = "tasks/send"
params: TaskSendParams
class SendTaskResponse(JSONRPCResponse):
result: Task | None = None
class SendTaskStreamingRequest(JSONRPCRequest):
method: Literal["tasks/sendSubscribe"] = "tasks/sendSubscribe"
params: TaskSendParams
class SendTaskStreamingResponse(JSONRPCResponse):
result: TaskStatusUpdateEvent | TaskArtifactUpdateEvent | None = None
class GetTaskRequest(JSONRPCRequest):
method: Literal["tasks/get"] = "tasks/get"
params: TaskQueryParams
class GetTaskResponse(JSONRPCResponse):
result: Task | None = None
class CancelTaskRequest(JSONRPCRequest):
method: Literal["tasks/cancel",] = "tasks/cancel"
params: TaskIdParams
class CancelTaskResponse(JSONRPCResponse):
result: Task | None = None
class SetTaskPushNotificationRequest(JSONRPCRequest):
method: Literal["tasks/pushNotification/set",] = "tasks/pushNotification/set"
params: TaskPushNotificationConfig
class SetTaskPushNotificationResponse(JSONRPCResponse):
result: TaskPushNotificationConfig | None = None
class GetTaskPushNotificationRequest(JSONRPCRequest):
method: Literal["tasks/pushNotification/get",] = "tasks/pushNotification/get"
params: TaskIdParams
class GetTaskPushNotificationResponse(JSONRPCResponse):
result: TaskPushNotificationConfig | None = None
class TaskResubscriptionRequest(JSONRPCRequest):
method: Literal["tasks/resubscribe",] = "tasks/resubscribe"
params: TaskIdParams
A2ARequest = TypeAdapter(
Annotated[
Union[
SendTaskRequest,
GetTaskRequest,
CancelTaskRequest,
SetTaskPushNotificationRequest,
GetTaskPushNotificationRequest,
TaskResubscriptionRequest,
SendTaskStreamingRequest,
],
Field(discriminator="method"),
]
)
## Error types
class JSONParseError(JSONRPCError):
code: int = -32700
message: str = "Invalid JSON payload"
data: Any | None = None
class InvalidRequestError(JSONRPCError):
code: int = -32600
message: str = "Request payload validation error"
data: Any | None = None
class MethodNotFoundError(JSONRPCError):
code: int = -32601
message: str = "Method not found"
data: None = None
class InvalidParamsError(JSONRPCError):
code: int = -32602
message: str = "Invalid parameters"
data: Any | None = None
class InternalError(JSONRPCError):
code: int = -32603
message: str = "Internal error"
data: Any | None = None
class TaskNotFoundError(JSONRPCError):
code: int = -32001
message: str = "Task not found"
data: None = None
class TaskNotCancelableError(JSONRPCError):
code: int = -32002
message: str = "Task cannot be canceled"
data: None = None
class PushNotificationNotSupportedError(JSONRPCError):
code: int = -32003
message: str = "Push Notification is not supported"
data: None = None
class UnsupportedOperationError(JSONRPCError):
code: int = -32004
message: str = "This operation is not supported"
data: None = None
class ContentTypeNotSupportedError(JSONRPCError):
code: int = -32005
message: str = "Incompatible content types"
data: None = None
class AgentProvider(BaseModel):
organization: str
url: str | None = None
class AgentCapabilities(BaseModel):
streaming: bool = False
pushNotifications: bool = False
stateTransitionHistory: bool = False
class AgentAuthentication(BaseModel):
schemes: List[str]
credentials: str | None = None
class AgentSkill(BaseModel):
id: str
name: str
description: str | None = None
tags: List[str] | None = None
examples: List[str] | None = None
inputModes: List[str] | None = None
outputModes: List[str] | None = None
class AgentCard(BaseModel):
name: str
description: str | None = None
url: str
provider: AgentProvider | None = None
version: str
documentationUrl: str | None = None
capabilities: AgentCapabilities
authentication: AgentAuthentication | None = None
defaultInputModes: List[str] = ["text"]
defaultOutputModes: List[str] = ["text"]
skills: List[AgentSkill]
class A2AClientError(Exception):
pass
class A2AClientHTTPError(A2AClientError):
def __init__(self, status_code: int, message: str):
self.status_code = status_code
self.message = message
super().__init__(f"HTTP Error {status_code}: {message}")
class A2AClientJSONError(A2AClientError):
def __init__(self, message: str):
self.message = message
super().__init__(f"JSON Error: {message}")
class MissingAPIKeyError(Exception):
"""Exception for missing API key."""
pass
================================================
FILE: cookbook/pocketflow-a2a/common/utils/in_memory_cache.py
================================================
"""In Memory Cache utility."""
import threading
import time
from typing import Any, Dict, Optional
class InMemoryCache:
"""A thread-safe Singleton class to manage cache data.
Ensures only one instance of the cache exists across the application.
"""
_instance: Optional["InMemoryCache"] = None
_lock: threading.Lock = threading.Lock()
_initialized: bool = False
def __new__(cls):
"""Override __new__ to control instance creation (Singleton pattern).
Uses a lock to ensure thread safety during the first instantiation.
Returns:
The singleton instance of InMemoryCache.
"""
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
"""Initialize the cache storage.
Uses a flag (_initialized) to ensure this logic runs only on the very first
creation of the singleton instance.
"""
if not self._initialized:
with self._lock:
if not self._initialized:
# print("Initializing SessionCache storage")
self._cache_data: Dict[str, Dict[str, Any]] = {}
self._ttl: Dict[str, float] = {}
self._data_lock: threading.Lock = threading.Lock()
self._initialized = True
def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
"""Set a key-value pair.
Args:
key: The key for the data.
value: The data to store.
ttl: Time to live in seconds. If None, data will not expire.
"""
with self._data_lock:
self._cache_data[key] = value
if ttl is not None:
self._ttl[key] = time.time() + ttl
else:
if key in self._ttl:
del self._ttl[key]
def get(self, key: str, default: Any = None) -> Any:
"""Get the value associated with a key.
Args:
key: The key for the data within the session.
default: The value to return if the session or key is not found.
Returns:
The cached value, or the default value if not found.
"""
with self._data_lock:
if key in self._ttl and time.time() > self._ttl[key]:
del self._cache_data[key]
del self._ttl[key]
return default
return self._cache_data.get(key, default)
def delete(self, key: str) -> None:
"""Delete a specific key-value pair from a cache.
Args:
key: The key to delete.
Returns:
True if the key was found and deleted, False otherwise.
"""
with self._data_lock:
if key in self._cache_data:
del self._cache_data[key]
if key in self._ttl:
del self._ttl[key]
return True
return False
def clear(self) -> bool:
"""Remove all data.
Returns:
True if the data was cleared, False otherwise.
"""
with self._data_lock:
self._cache_data.clear()
self._ttl.clear()
return True
return False
================================================
FILE: cookbook/pocketflow-a2a/common/utils/push_notification_auth.py
================================================
from jwcrypto import jwk
import uuid
from starlette.responses import JSONResponse
from starlette.requests import Request
from typing import Any
import jwt
import time
import json
import hashlib
import httpx
import logging
from jwt import PyJWK, PyJWKClient
logger = logging.getLogger(__name__)
AUTH_HEADER_PREFIX = 'Bearer '
class PushNotificationAuth:
def _calculate_request_body_sha256(self, data: dict[str, Any]):
"""Calculates the SHA256 hash of a request body.
This logic needs to be same for both the agent who signs the payload and the client verifier.
"""
body_str = json.dumps(
data,
ensure_ascii=False,
allow_nan=False,
indent=None,
separators=(",", ":"),
)
return hashlib.sha256(body_str.encode()).hexdigest()
class PushNotificationSenderAuth(PushNotificationAuth):
def __init__(self):
self.public_keys = []
self.private_key_jwk: PyJWK = None
@staticmethod
async def verify_push_notification_url(url: str) -> bool:
async with httpx.AsyncClient(timeout=10) as client:
try:
validation_token = str(uuid.uuid4())
response = await client.get(
url,
params={"validationToken": validation_token}
)
response.raise_for_status()
is_verified = response.text == validation_token
logger.info(f"Verified push-notification URL: {url} => {is_verified}")
return is_verified
except Exception as e:
logger.warning(f"Error during sending push-notification for URL {url}: {e}")
return False
def generate_jwk(self):
key = jwk.JWK.generate(kty='RSA', size=2048, kid=str(uuid.uuid4()), use="sig")
self.public_keys.append(key.export_public(as_dict=True))
self.private_key_jwk = PyJWK.from_json(key.export_private())
def handle_jwks_endpoint(self, _request: Request):
"""Allow clients to fetch public keys.
"""
return JSONResponse({
"keys": self.public_keys
})
def _generate_jwt(self, data: dict[str, Any]):
"""JWT is generated by signing both the request payload SHA digest and time of token generation.
Payload is signed with private key and it ensures the integrity of payload for client.
Including iat prevents from replay attack.
"""
iat = int(time.time())
return jwt.encode(
{"iat": iat, "request_body_sha256": self._calculate_request_body_sha256(data)},
key=self.private_key_jwk,
headers={"kid": self.private_key_jwk.key_id},
algorithm="RS256"
)
async def send_push_notification(self, url: str, data: dict[str, Any]):
jwt_token = self._generate_jwt(data)
headers = {'Authorization': f"Bearer {jwt_token}"}
async with httpx.AsyncClient(timeout=10) as client:
try:
response = await client.post(
url,
json=data,
headers=headers
)
response.raise_for_status()
logger.info(f"Push-notification sent for URL: {url}")
except Exception as e:
logger.warning(f"Error during sending push-notification for URL {url}: {e}")
class PushNotificationReceiverAuth(PushNotificationAuth):
def __init__(self):
self.public_keys_jwks = []
self.jwks_client = None
async def load_jwks(self, jwks_url: str):
self.jwks_client = PyJWKClient(jwks_url)
async def verify_push_notification(self, request: Request) -> bool:
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith(AUTH_HEADER_PREFIX):
print("Invalid authorization header")
return False
token = auth_header[len(AUTH_HEADER_PREFIX):]
signing_key = self.jwks_client.get_signing_key_from_jwt(token)
decode_token = jwt.decode(
token,
signing_key,
options={"require": ["iat", "request_body_sha256"]},
algorithms=["RS256"],
)
actual_body_sha256 = self._calculate_request_body_sha256(await request.json())
if actual_body_sha256 != decode_token["request_body_sha256"]:
# Payload signature does not match the digest in signed token.
raise ValueError("Invalid request body")
if time.time() - decode_token["iat"] > 60 * 5:
# Do not allow push-notifications older than 5 minutes.
# This is to prevent replay attack.
raise ValueError("Token is expired")
return True
================================================
FILE: cookbook/pocketflow-a2a/flow.py
================================================
from pocketflow import Flow
from nodes import DecideAction, SearchWeb, AnswerQuestion
def create_agent_flow():
"""
Create and connect the nodes to form a complete agent flow.
The flow works like this:
1. DecideAction node decides whether to search or answer
2. If search, go to SearchWeb node
3. If answer, go to AnswerQuestion node
4. After SearchWeb completes, go back to DecideAction
Returns:
Flow: A complete research agent flow
"""
# Create instances of each node
decide = DecideAction()
search = SearchWeb()
answer = AnswerQuestion()
# Connect the nodes
# If DecideAction returns "search", go to SearchWeb
decide - "search" >> search
# If DecideAction returns "answer", go to AnswerQuestion
decide - "answer" >> answer
# After SearchWeb completes and returns "decide", go back to DecideAction
search - "decide" >> decide
# Create and return the flow, starting with the DecideAction node
return Flow(start=decide)
================================================
FILE: cookbook/pocketflow-a2a/main.py
================================================
import sys
from flow import create_agent_flow
def main():
"""Simple function to process a question."""
# Default question
default_question = "Who won the Nobel Prize in Physics 2024?"
# Get question from command line if provided with --
question = default_question
for arg in sys.argv[1:]:
if arg.startswith("--"):
question = arg[2:]
break
# Create the agent flow
agent_flow = create_agent_flow()
# Process the question
shared = {"question": question}
print(f"🤔 Processing question: {question}")
agent_flow.run(shared)
print("\n🎯 Final Answer:")
print(shared.get("answer", "No answer found"))
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-a2a/nodes.py
================================================
from pocketflow import Node
from utils import call_llm, search_web
import yaml
class DecideAction(Node):
def prep(self, shared):
"""Prepare the context and question for the decision-making process."""
# Get the current context (default to "No previous search" if none exists)
context = shared.get("context", "No previous search")
# Get the question from the shared store
question = shared["question"]
# Return both for the exec step
return question, context
def exec(self, inputs):
"""Call the LLM to decide whether to search or answer."""
question, context = inputs
print(f"🤔 Agent deciding what to do next...")
# Create a prompt to help the LLM decide what to do next with proper yaml formatting
prompt = f"""
### CONTEXT
You are a research assistant that can search the web.
Question: {question}
Previous Research: {context}
### ACTION SPACE
[1] search
Description: Look up more information on the web
Parameters:
- query (str): What to search for
[2] answer
Description: Answer the question with current knowledge
Parameters:
- answer (str): Final answer to the question
## NEXT ACTION
Decide the next action based on the context and available actions.
Return your response in this format:
```yaml
thinking: |
action: search OR answer
reason:
answer:
search_query:
```
IMPORTANT: Make sure to:
1. Use proper indentation (4 spaces) for all multi-line fields
2. Use the | character for multi-line text fields
3. Keep single-line fields without the | character
"""
# Call the LLM to make a decision
response = call_llm(prompt)
# Parse the response to get the decision
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
decision = yaml.safe_load(yaml_str)
return decision
def post(self, shared, prep_res, exec_res):
"""Save the decision and determine the next step in the flow."""
# If LLM decided to search, save the search query
if exec_res["action"] == "search":
shared["search_query"] = exec_res["search_query"]
print(f"🔍 Agent decided to search for: {exec_res['search_query']}")
else:
shared["context"] = exec_res["answer"] #save the context if LLM gives the answer without searching.
print(f"💡 Agent decided to answer the question")
# Return the action to determine the next node in the flow
return exec_res["action"]
class SearchWeb(Node):
def prep(self, shared):
"""Get the search query from the shared store."""
return shared["search_query"]
def exec(self, search_query):
"""Search the web for the given query."""
# Call the search utility function
print(f"🌐 Searching the web for: {search_query}")
results = search_web(search_query)
return results
def post(self, shared, prep_res, exec_res):
"""Save the search results and go back to the decision node."""
# Add the search results to the context in the shared store
previous = shared.get("context", "")
shared["context"] = previous + "\n\nSEARCH: " + shared["search_query"] + "\nRESULTS: " + exec_res
print(f"📚 Found information, analyzing results...")
# Always go back to the decision node after searching
return "decide"
class AnswerQuestion(Node):
def prep(self, shared):
"""Get the question and context for answering."""
return shared["question"], shared.get("context", "")
def exec(self, inputs):
"""Call the LLM to generate a final answer."""
question, context = inputs
print(f"✍️ Crafting final answer...")
# Create a prompt for the LLM to answer the question
prompt = f"""
### CONTEXT
Based on the following information, answer the question.
Question: {question}
Research: {context}
## YOUR ANSWER:
Provide a comprehensive answer using the research results.
"""
# Call the LLM to generate an answer
answer = call_llm(prompt)
return answer
def post(self, shared, prep_res, exec_res):
"""Save the final answer and complete the flow."""
# Save the answer in the shared store
shared["answer"] = exec_res
print(f"✅ Answer generated successfully")
# We're done - no need to continue the flow
return "done"
================================================
FILE: cookbook/pocketflow-a2a/requirements.txt
================================================
# For PocketFlow Agent Logic
pocketflow>=0.0.1
openai>=1.0.0
duckduckgo-search>=7.5.2
pyyaml>=5.1
# For A2A Server Infrastructure (from common)
starlette>=0.37.2,<0.38.0
uvicorn[standard]>=0.29.0,<0.30.0
sse-starlette>=1.8.2,<2.0.0
pydantic>=2.0.0,<3.0.0
httpx>=0.27.0,<0.28.0
anyio>=3.0.0,<5.0.0 # Dependency of starlette/httpx
# For running __main__.py
click>=8.0.0,<9.0.0
# For A2A Client
httpx>=0.27.0,<0.28.0
httpx-sse>=0.4.0
asyncclick>=8.1.8 # Or just 'click' if you prefer asyncio.run
pydantic>=2.0.0,<3.0.0 # For common.types
================================================
FILE: cookbook/pocketflow-a2a/task_manager.py
================================================
# FILE: pocketflow_a2a_agent/task_manager.py
import logging
from typing import AsyncIterable, Union
import asyncio
# Import from the common code you copied
from common.server.task_manager import InMemoryTaskManager
from common.types import (
JSONRPCResponse, SendTaskRequest, SendTaskResponse,
SendTaskStreamingRequest, SendTaskStreamingResponse, Task, TaskSendParams,
TaskState, TaskStatus, TextPart, Artifact, UnsupportedOperationError,
InternalError, InvalidParamsError,
Message
)
import common.server.utils as server_utils
# Import directly from your original PocketFlow files
from flow import create_agent_flow
logger = logging.getLogger(__name__)
class PocketFlowTaskManager(InMemoryTaskManager):
""" TaskManager implementation that runs the PocketFlow agent. """
SUPPORTED_CONTENT_TYPES = ["text", "text/plain"] # Define what the agent accepts/outputs
async def on_send_task(self, request: SendTaskRequest) -> SendTaskResponse:
"""Handles non-streaming task requests."""
logger.info(f"Received task send request: {request.params.id}")
# Validate output modes
if not server_utils.are_modalities_compatible(
request.params.acceptedOutputModes, self.SUPPORTED_CONTENT_TYPES
):
logger.warning(
"Unsupported output mode. Received %s, Support %s",
request.params.acceptedOutputModes, self.SUPPORTED_CONTENT_TYPES
)
return SendTaskResponse(id=request.id, error=server_utils.new_incompatible_types_error(request.id).error)
# Upsert the task in the store (initial state: submitted)
# We create the task first so its state can be tracked, even if the sync execution fails
await self.upsert_task(request.params)
# Update state to working before running
await self.update_store(request.params.id, TaskStatus(state=TaskState.WORKING), [])
# --- Run the PocketFlow logic ---
task_params: TaskSendParams = request.params
query = self._get_user_query(task_params)
if query is None:
fail_status = TaskStatus(state=TaskState.FAILED, message=Message(role="agent", parts=[TextPart(text="No text query found")]))
await self.update_store(task_params.id, fail_status, [])
return SendTaskResponse(id=request.id, error=InvalidParamsError(message="No text query found in message parts"))
shared_data = {"question": query}
agent_flow = create_agent_flow() # Create the flow instance
try:
# Run the synchronous PocketFlow
# In a real async server, you might run this in a separate thread/process
# executor to avoid blocking the event loop. For simplicity here, we run it directly.
# Consider adding a timeout if flows can hang.
logger.info(f"Running PocketFlow for task {task_params.id}...")
agent_flow.run(shared_data) # Run the flow, modifying shared_data in place
logger.info(f"PocketFlow completed for task {task_params.id}")
# Access the original shared_data dictionary, which was modified by the flow
answer_text = shared_data.get("answer", "Agent did not produce a final answer text.")
# --- Package result into A2A Task ---
final_task_status = TaskStatus(state=TaskState.COMPLETED)
# Package the answer as an artifact
final_artifact = Artifact(parts=[TextPart(text=answer_text)])
# Update the task in the store with final status and artifact
final_task = await self.update_store(
task_params.id, final_task_status, [final_artifact]
)
# Prepare and return the A2A response
task_result = self.append_task_history(final_task, task_params.historyLength)
return SendTaskResponse(id=request.id, result=task_result)
except Exception as e:
logger.error(f"Error executing PocketFlow for task {task_params.id}: {e}", exc_info=True)
# Update task state to FAILED
fail_status = TaskStatus(
state=TaskState.FAILED,
message=Message(role="agent", parts=[TextPart(text=f"Agent execution failed: {e}")])
)
await self.update_store(task_params.id, fail_status, [])
return SendTaskResponse(id=request.id, error=InternalError(message=f"Agent error: {e}"))
async def on_send_task_subscribe(
self, request: SendTaskStreamingRequest
) -> Union[AsyncIterable[SendTaskStreamingResponse], JSONRPCResponse]:
"""Handles streaming requests - Not implemented for this synchronous agent."""
logger.warning(f"Streaming requested for task {request.params.id}, but not supported by this PocketFlow agent implementation.")
# Return an error indicating streaming is not supported
return JSONRPCResponse(id=request.id, error=UnsupportedOperationError(message="Streaming not supported by this agent"))
def _get_user_query(self, task_send_params: TaskSendParams) -> str | None:
"""Extracts the first text part from the user message."""
if not task_send_params.message or not task_send_params.message.parts:
logger.warning(f"No message parts found for task {task_send_params.id}")
return None
for part in task_send_params.message.parts:
# Ensure part is treated as a dictionary if it came from JSON
part_dict = part if isinstance(part, dict) else part.model_dump()
if part_dict.get("type") == "text" and "text" in part_dict:
return part_dict["text"]
logger.warning(f"No text part found in message for task {task_send_params.id}")
return None # No text part found
================================================
FILE: cookbook/pocketflow-a2a/utils.py
================================================
from openai import OpenAI
import os
from duckduckgo_search import DDGS
def call_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
def search_web(query):
results = DDGS().text(query, max_results=5)
# Convert results to a string
results_str = "\n\n".join([f"Title: {r['title']}\nURL: {r['href']}\nSnippet: {r['body']}" for r in results])
return results_str
if __name__ == "__main__":
print("## Testing call_llm")
prompt = "In a few words, what is the meaning of life?"
print(f"## Prompt: {prompt}")
response = call_llm(prompt)
print(f"## Response: {response}")
print("## Testing search_web")
query = "Who won the Nobel Prize in Physics 2024?"
print(f"## Query: {query}")
results = search_web(query)
print(f"## Results: {results}")
================================================
FILE: cookbook/pocketflow-agent/README.md
================================================
# Research Agent
This project demonstrates a simple yet powerful LLM-powered research agent. This implementation is based directly on the tutorial: [LLM Agents are simply Graph — Tutorial For Dummies](https://zacharyhuang.substack.com/p/llm-agent-internal-as-a-graph-tutorial).
👉 Run the tutorial in your browser: [Try Google Colab Notebook](
https://colab.research.google.com/github/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-agent/demo.ipynb)
## Features
- Performs web searches to gather information
- Makes decisions about when to search vs. when to answer
- Generates comprehensive answers based on research findings
## Getting Started
1. Install the packages you need with this simple command:
```bash
pip install -r requirements.txt
```
2. Let's get your OpenAI API key ready:
```bash
export OPENAI_API_KEY="your-api-key-here"
```
3. Let's do a quick check to make sure your API key is working properly:
```bash
python utils.py
```
This will test both the LLM call and web search features. If you see responses, you're good to go!
4. Try out the agent with the default question (about Nobel Prize winners):
```bash
python main.py
```
5. Got a burning question? Ask anything you want by using the `--` prefix:
```bash
python main.py --"What is quantum computing?"
```
## How It Works?
The magic happens through a simple but powerful graph structure with three main parts:
```mermaid
graph TD
A[DecideAction] -->|"search"| B[SearchWeb]
A -->|"answer"| C[AnswerQuestion]
B -->|"decide"| A
```
Here's what each part does:
1. **DecideAction**: The brain that figures out whether to search or answer
2. **SearchWeb**: The researcher that goes out and finds information
3. **AnswerQuestion**: The writer that crafts the final answer
Here's what's in each file:
- [`main.py`](./main.py): The starting point - runs the whole show!
- [`flow.py`](./flow.py): Connects everything together into a smart agent
- [`nodes.py`](./nodes.py): The building blocks that make decisions and take actions
- [`utils.py`](./utils.py): Helper functions for talking to the LLM and searching the web
================================================
FILE: cookbook/pocketflow-agent/demo.ipynb
================================================
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"vscode": {
"languageId": "plaintext"
},
"id": "8MeqVASIxKBH"
},
"outputs": [],
"source": [
"! pip install pocketflow>=0.0.1\n",
"! pip install aiohttp>=3.8.0\n",
"! pip install openai>=1.0.0\n",
"! pip install duckduckgo-search>=7.5.2"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"vscode": {
"languageId": "plaintext"
},
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "wUp_sNU1xKBI",
"outputId": "a647f919-b253-48c8-c132-5eef582e29c7"
},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"## Testing call_llm\n",
"## Prompt: In a few words, what is the meaning of life?\n",
"## Response: The meaning of life is a deeply personal and philosophical question. For many, it involves seeking happiness, forming relationships, pursuing knowledge, or finding purpose and fulfillment. It's a journey that varies for each individual.\n",
"## Testing search_web\n",
"## Query: Who won the Nobel Prize in Physics 2024?\n",
"## Results: Title: Press release: The Nobel Prize in Physics 2024 - NobelPrize.org\n",
"URL: https://www.nobelprize.org/prizes/physics/2024/press-release/\n",
"Snippet: The Nobel Prize in Physics 2024 was awarded jointly to John J. Hopfield and Geoffrey Hinton \"for foundational discoveries and inventions that enable machine learning with artificial neural networks\"\n",
"\n",
"Title: Pioneers in artificial intelligence win the Nobel Prize in physics\n",
"URL: https://apnews.com/article/nobel-prize-physics-fc0567de3f2ca45f81a7359a017cd542\n",
"Snippet: Two pioneers of artificial intelligence have won the Nobel Prize in physics. John Hopfield and Geoffrey Hinton were awarded the prize Tuesday for discoveries and inventions that formed the building blocks of machine learning.\n",
"\n",
"Title: Nobel Prize 2024: All the Winners | TIME\n",
"URL: https://time.com/7065011/nobel-prize-2024-winners/\n",
"Snippet: The 2024 Nobel Prize announcements began on Oct. 7, recognizing groundbreaking contributions to humanity. The first prize, in the category of physiology or medicine, went to a pair of American ...\n",
"\n",
"Title: Nobel physics prize 2024 won by AI pioneers John Hopfield and Geoffrey ...\n",
"URL: https://www.reuters.com/science/hopfield-hinton-win-2024-nobel-prize-physics-2024-10-08/\n",
"Snippet: John Hopfield and Geoffrey Hinton won for discoveries that paved the way for the AI boom.\n",
"\n",
"Title: Nobel Prize in physics 2024 awarded for work on artificial intelligence ...\n",
"URL: https://www.cnn.com/2024/10/08/science/nobel-prize-physics-hopfield-hinton-machine-learning-intl/index.html\n",
"Snippet: The 2024 Nobel Prize in physics has been awarded to John Hopfield and Geoffrey Hinton for their fundamental discoveries in machine learning, which paved the way for how artificial intelligence is ...\n"
]
}
],
"source": [
"# utils.py\n",
"from openai import OpenAI\n",
"import os\n",
"from duckduckgo_search import DDGS\n",
"\n",
"def call_llm(prompt):\n",
" client = OpenAI(api_key=\"your-api-key\")\n",
" r = client.chat.completions.create(\n",
" model=\"gpt-4o\",\n",
" messages=[{\"role\": \"user\", \"content\": prompt}]\n",
" )\n",
" return r.choices[0].message.content\n",
"\n",
"def search_web(query):\n",
" results = DDGS().text(query, max_results=5)\n",
" # Convert results to a string\n",
" results_str = \"\\n\\n\".join([f\"Title: {r['title']}\\nURL: {r['href']}\\nSnippet: {r['body']}\" for r in results])\n",
" return results_str\n",
"\n",
"print(\"## Testing call_llm\")\n",
"prompt = \"In a few words, what is the meaning of life?\"\n",
"print(f\"## Prompt: {prompt}\")\n",
"response = call_llm(prompt)\n",
"print(f\"## Response: {response}\")\n",
"\n",
"print(\"## Testing search_web\")\n",
"query = \"Who won the Nobel Prize in Physics 2024?\"\n",
"print(f\"## Query: {query}\")\n",
"results = search_web(query)\n",
"print(f\"## Results: {results}\")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"vscode": {
"languageId": "plaintext"
},
"id": "T0ETd4C2xKBI"
},
"outputs": [],
"source": [
"# nodes.py\n",
"from pocketflow import Node\n",
"import yaml\n",
"\n",
"class DecideAction(Node):\n",
" def prep(self, shared):\n",
" \"\"\"Prepare the context and question for the decision-making process.\"\"\"\n",
" # Get the current context (default to \"No previous search\" if none exists)\n",
" context = shared.get(\"context\", \"No previous search\")\n",
" # Get the question from the shared store\n",
" question = shared[\"question\"]\n",
" # Return both for the exec step\n",
" return question, context\n",
"\n",
" def exec(self, inputs):\n",
" \"\"\"Call the LLM to decide whether to search or answer.\"\"\"\n",
" question, context = inputs\n",
"\n",
" print(f\"🤔 Agent deciding what to do next...\")\n",
"\n",
" # Create a prompt to help the LLM decide what to do next with proper yaml formatting\n",
" prompt = f\"\"\"\n",
"### CONTEXT\n",
"You are a research assistant that can search the web.\n",
"Question: {question}\n",
"Previous Research: {context}\n",
"\n",
"### ACTION SPACE\n",
"[1] search\n",
" Description: Look up more information on the web\n",
" Parameters:\n",
" - query (str): What to search for\n",
"\n",
"[2] answer\n",
" Description: Answer the question with current knowledge\n",
" Parameters:\n",
" - answer (str): Final answer to the question\n",
"\n",
"## NEXT ACTION\n",
"Decide the next action based on the context and available actions.\n",
"Return your response in this format:\n",
"\n",
"```yaml\n",
"thinking: |\n",
" \n",
"action: search OR answer\n",
"reason: \n",
"answer: \n",
"search_query: \n",
"```\n",
"IMPORTANT: Make sure to:\n",
"1. Use proper indentation (4 spaces) for all multi-line fields\n",
"2. Use the | character for multi-line text fields\n",
"3. Keep single-line fields without the | character\n",
"\"\"\"\n",
"\n",
" # Call the LLM to make a decision\n",
" response = call_llm(prompt)\n",
"\n",
" # Parse the response to get the decision\n",
" yaml_str = response.split(\"```yaml\")[1].split(\"```\")[0].strip()\n",
" decision = yaml.safe_load(yaml_str)\n",
"\n",
" return decision\n",
"\n",
" def post(self, shared, prep_res, exec_res):\n",
" \"\"\"Save the decision and determine the next step in the flow.\"\"\"\n",
" # If LLM decided to search, save the search query\n",
" if exec_res[\"action\"] == \"search\":\n",
" shared[\"search_query\"] = exec_res[\"search_query\"]\n",
" print(f\"🔍 Agent decided to search for: {exec_res['search_query']}\")\n",
" else:\n",
" shared[\"context\"] = exec_res[\"answer\"] #save the context if LLM gives the answer without searching.\n",
" print(f\"💡 Agent decided to answer the question\")\n",
"\n",
" # Return the action to determine the next node in the flow\n",
" return exec_res[\"action\"]\n",
"\n",
"class SearchWeb(Node):\n",
" def prep(self, shared):\n",
" \"\"\"Get the search query from the shared store.\"\"\"\n",
" return shared[\"search_query\"]\n",
"\n",
" def exec(self, search_query):\n",
" \"\"\"Search the web for the given query.\"\"\"\n",
" # Call the search utility function\n",
" print(f\"🌐 Searching the web for: {search_query}\")\n",
" results = search_web(search_query)\n",
" return results\n",
"\n",
" def post(self, shared, prep_res, exec_res):\n",
" \"\"\"Save the search results and go back to the decision node.\"\"\"\n",
" # Add the search results to the context in the shared store\n",
" previous = shared.get(\"context\", \"\")\n",
" shared[\"context\"] = previous + \"\\n\\nSEARCH: \" + shared[\"search_query\"] + \"\\nRESULTS: \" + exec_res\n",
"\n",
" print(f\"📚 Found information, analyzing results...\")\n",
"\n",
" # Always go back to the decision node after searching\n",
" return \"decide\"\n",
"\n",
"class AnswerQuestion(Node):\n",
" def prep(self, shared):\n",
" \"\"\"Get the question and context for answering.\"\"\"\n",
" return shared[\"question\"], shared.get(\"context\", \"\")\n",
"\n",
" def exec(self, inputs):\n",
" \"\"\"Call the LLM to generate a final answer.\"\"\"\n",
" question, context = inputs\n",
"\n",
" print(f\"✍️ Crafting final answer...\")\n",
"\n",
" # Create a prompt for the LLM to answer the question\n",
" prompt = f\"\"\"\n",
"### CONTEXT\n",
"Based on the following information, answer the question.\n",
"Question: {question}\n",
"Research: {context}\n",
"\n",
"## YOUR ANSWER:\n",
"Provide a comprehensive answer using the research results.\n",
"\"\"\"\n",
" # Call the LLM to generate an answer\n",
" answer = call_llm(prompt)\n",
" return answer\n",
"\n",
" def post(self, shared, prep_res, exec_res):\n",
" \"\"\"Save the final answer and complete the flow.\"\"\"\n",
" # Save the answer in the shared store\n",
" shared[\"answer\"] = exec_res\n",
"\n",
" print(f\"✅ Answer generated successfully\")\n",
"\n",
" # We're done - no need to continue the flow\n",
" return \"done\""
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"vscode": {
"languageId": "plaintext"
},
"id": "0B4jCAmXxKBI"
},
"outputs": [],
"source": [
"# flow.py\n",
"from pocketflow import Flow\n",
"\n",
"def create_agent_flow():\n",
" \"\"\"\n",
" Create and connect the nodes to form a complete agent flow.\n",
"\n",
" The flow works like this:\n",
" 1. DecideAction node decides whether to search or answer\n",
" 2. If search, go to SearchWeb node\n",
" 3. If answer, go to AnswerQuestion node\n",
" 4. After SearchWeb completes, go back to DecideAction\n",
"\n",
" Returns:\n",
" Flow: A complete research agent flow\n",
" \"\"\"\n",
" # Create instances of each node\n",
" decide = DecideAction()\n",
" search = SearchWeb()\n",
" answer = AnswerQuestion()\n",
"\n",
" # Connect the nodes\n",
" # If DecideAction returns \"search\", go to SearchWeb\n",
" decide - \"search\" >> search\n",
"\n",
" # If DecideAction returns \"answer\", go to AnswerQuestion\n",
" decide - \"answer\" >> answer\n",
"\n",
" # After SearchWeb completes and returns \"decide\", go back to DecideAction\n",
" search - \"decide\" >> decide\n",
"\n",
" # Create and return the flow, starting with the DecideAction node\n",
" return Flow(start=decide)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"vscode": {
"languageId": "plaintext"
},
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "bIwsNEDCxKBI",
"outputId": "e6c02020-6fae-4377-8f0a-01d2580dd659"
},
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"🤔 Processing question: Who won the Nobel Prize in Physics 2024?\n",
"🤔 Agent deciding what to do next...\n",
"🔍 Agent decided to search for: 2024 Nobel Prize in Physics winner\n",
"🌐 Searching the web for: 2024 Nobel Prize in Physics winner\n",
"📚 Found information, analyzing results...\n",
"🤔 Agent deciding what to do next...\n",
"💡 Agent decided to answer the question\n",
"✍️ Crafting final answer...\n",
"✅ Answer generated successfully\n",
"\n",
"🎯 Final Answer:\n",
"John J. Hopfield and Geoffrey Hinton won the 2024 Nobel Prize in Physics. They were awarded this prestigious recognition for their foundational discoveries and inventions that have significantly advanced the field of machine learning by enabling the use of artificial neural networks. These contributions have had a profound impact on the development and application of machine learning technologies.\n"
]
}
],
"source": [
"# main.py\n",
"import sys\n",
"\n",
"def main():\n",
" \"\"\"Simple function to process a question.\"\"\"\n",
" # Default question\n",
" default_question = \"Who won the Nobel Prize in Physics 2024?\"\n",
"\n",
" # Get question from command line if provided with --\n",
" question = default_question\n",
" for arg in sys.argv[1:]:\n",
" if arg.startswith(\"--\"):\n",
" question = arg[2:]\n",
" break\n",
"\n",
" # Create the agent flow\n",
" agent_flow = create_agent_flow()\n",
"\n",
" # Process the question\n",
" shared = {\"question\": question}\n",
" print(f\"🤔 Processing question: {question}\")\n",
" agent_flow.run(shared)\n",
" print(\"\\n🎯 Final Answer:\")\n",
" print(shared.get(\"answer\", \"No answer found\"))\n",
"\n",
"main()"
]
}
],
"metadata": {
"language_info": {
"name": "python"
},
"colab": {
"provenance": []
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
================================================
FILE: cookbook/pocketflow-agent/flow.py
================================================
from pocketflow import Flow
from nodes import DecideAction, SearchWeb, AnswerQuestion
def create_agent_flow():
"""
Create and connect the nodes to form a complete agent flow.
The flow works like this:
1. DecideAction node decides whether to search or answer
2. If search, go to SearchWeb node
3. If answer, go to AnswerQuestion node
4. After SearchWeb completes, go back to DecideAction
Returns:
Flow: A complete research agent flow
"""
# Create instances of each node
decide = DecideAction()
search = SearchWeb()
answer = AnswerQuestion()
# Connect the nodes
# If DecideAction returns "search", go to SearchWeb
decide - "search" >> search
# If DecideAction returns "answer", go to AnswerQuestion
decide - "answer" >> answer
# After SearchWeb completes and returns "decide", go back to DecideAction
search - "decide" >> decide
# Create and return the flow, starting with the DecideAction node
return Flow(start=decide)
================================================
FILE: cookbook/pocketflow-agent/main.py
================================================
import sys
from flow import create_agent_flow
def main():
"""Simple function to process a question."""
# Default question
default_question = "Who won the Nobel Prize in Physics 2024?"
# Get question from command line if provided with --
question = default_question
for arg in sys.argv[1:]:
if arg.startswith("--"):
question = arg[2:]
break
# Create the agent flow
agent_flow = create_agent_flow()
# Process the question
shared = {"question": question}
print(f"🤔 Processing question: {question}")
agent_flow.run(shared)
print("\n🎯 Final Answer:")
print(shared.get("answer", "No answer found"))
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-agent/nodes.py
================================================
from pocketflow import Node
from utils import call_llm, search_web_duckduckgo
import yaml
import re
class DecideAction(Node):
def prep(self, shared):
"""Prepare the context and question for the decision-making process."""
# Get the current context (default to "No previous search" if none exists)
context = shared.get("context", "No previous search")
# Get the question from the shared store
question = shared["question"]
# Return both for the exec step
return question, context
def exec(self, inputs):
"""Call the LLM to decide whether to search or answer."""
question, context = inputs
print(f"🤔 Agent deciding what to do next...")
# Create a prompt to help the LLM decide what to do next with proper yaml formatting
prompt = f"""
### CONTEXT
You are a research assistant that can search the web.
Question: {question}
Previous Research: {context}
### ACTION SPACE
[1] search
Description: Look up more information on the web
Parameters:
- query (str): What to search for
[2] answer
Description: Answer the question with current knowledge
Parameters:
- answer (str): Final answer to the question
## NEXT ACTION
Decide the next action based on the context and available actions.
Return your response in this format:
```yaml
thinking: |
action: search OR answer
reason: |
answer: |
search_query:
```
IMPORTANT: Make sure to:
1. ALWAYS use the | block scalar for thinking, reason and answer so colons or quotes inside the text do not break YAML.
2. Use proper indentation (4 spaces) for all multi-line fields under |.
3. Keep search_query as a single line string without the | character.
"""
# Call the LLM to make a decision
response = call_llm(prompt)
# Parse the response to get the decision
def extract_yaml_block(text):
"""Extract YAML from a fenced code block, or fall back to the whole text."""
match = re.search(r"```yaml(.*?)```", text, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
return text.strip()
def parse_yaml_safely(block):
"""Parse YAML, retrying with block scalars if colon characters caused issues."""
try:
return yaml.safe_load(block)
except yaml.YAMLError:
fixed_lines = []
for line in block.splitlines():
if re.match(r"^(thinking|reason|answer|search_query):", line) and "|" not in line:
key, _, val = line.partition(":")
fixed_lines.append(f"{key}: |")
val = val.strip()
if val:
fixed_lines.append(f" {val}")
else:
fixed_lines.append(line)
fixed_block = "\n".join(fixed_lines)
try:
return yaml.safe_load(fixed_block)
except yaml.YAMLError as exc:
raise ValueError(f"Unable to parse LLM YAML response:\n{block}") from exc
yaml_str = extract_yaml_block(response)
decision = parse_yaml_safely(yaml_str)
return decision
def post(self, shared, prep_res, exec_res):
"""Save the decision and determine the next step in the flow."""
# If LLM decided to search, save the search query
if exec_res["action"] == "search":
shared["search_query"] = exec_res["search_query"]
print(f"🔍 Agent decided to search for: {exec_res['search_query']}")
else:
shared["context"] = exec_res["answer"] #save the context if LLM gives the answer without searching.
print(f"💡 Agent decided to answer the question")
# Return the action to determine the next node in the flow
return exec_res["action"]
class SearchWeb(Node):
def prep(self, shared):
"""Get the search query from the shared store."""
return shared["search_query"]
def exec(self, search_query):
"""Search the web for the given query."""
# Call the search utility function
print(f"🌐 Searching the web for: {search_query}")
results = search_web_duckduckgo(search_query)
return results
def post(self, shared, prep_res, exec_res):
"""Save the search results and go back to the decision node."""
# Add the search results to the context in the shared store
previous = shared.get("context", "")
shared["context"] = previous + "\n\nSEARCH: " + shared["search_query"] + "\nRESULTS: " + exec_res
print(f"📚 Found information, analyzing results...")
# Always go back to the decision node after searching
return "decide"
class AnswerQuestion(Node):
def prep(self, shared):
"""Get the question and context for answering."""
return shared["question"], shared.get("context", "")
def exec(self, inputs):
"""Call the LLM to generate a final answer."""
question, context = inputs
print(f"✍️ Crafting final answer...")
# Create a prompt for the LLM to answer the question
prompt = f"""
### CONTEXT
Based on the following information, answer the question.
Question: {question}
Research: {context}
## YOUR ANSWER:
Provide a comprehensive answer using the research results.
"""
# Call the LLM to generate an answer
answer = call_llm(prompt)
return answer
def post(self, shared, prep_res, exec_res):
"""Save the final answer and complete the flow."""
# Save the answer in the shared store
shared["answer"] = exec_res
print(f"✅ Answer generated successfully")
# We're done - no need to continue the flow
return "done"
================================================
FILE: cookbook/pocketflow-agent/requirements.txt
================================================
pocketflow>=0.0.1
ddgs>=7.5.2 # For web search
aiohttp>=3.8.0 # For HTTP requests
openai>=1.0.0 # For LLM calls
requests>=2.25.1 # For HTTP requests
PyYAML>=6.0.2 # For YAML parsing
================================================
FILE: cookbook/pocketflow-agent/utils.py
================================================
from openai import OpenAI
import os
from ddgs import DDGS
import requests
def call_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
def search_web_duckduckgo(query):
results = DDGS().text(query, max_results=5)
# Convert results to a string
results_str = "\n\n".join([f"Title: {r['title']}\nURL: {r['href']}\nSnippet: {r['body']}" for r in results])
return results_str
def search_web_brave(query):
url = f"https://api.search.brave.com/res/v1/web/search?q={query}"
api_key = "your brave search api key"
headers = {
"accept": "application/json",
"Accept-Encoding": "gzip",
"x-subscription-token": api_key
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
results = data['web']['results']
results_str = "\n\n".join([f"Title: {r['title']}\nURL: {r['url']}\nDescription: {r['description']}" for r in results])
else:
print(f"Request failed with status code: {response.status_code}")
return results_str
if __name__ == "__main__":
print("## Testing call_llm")
prompt = "In a few words, what is the meaning of life?"
print(f"## Prompt: {prompt}")
response = call_llm(prompt)
print(f"## Response: {response}")
print("## Testing search_web")
query = "Who won the Nobel Prize in Physics 2024?"
print(f"## Query: {query}")
results = search_web_duckduckgo(query)
print(f"## Results: {results}")
================================================
FILE: cookbook/pocketflow-agent-skills/README.md
================================================
# Agent Skills with PocketFlow
This cookbook shows a lightweight pattern for using **Agent Skills** inside a PocketFlow graph.
Agent Skills are just reusable instruction files (Markdown) that you can route to at runtime.
## What this demo does
- keeps skills as local markdown files (`./skills/*.md`)
- chooses a skill based on the user request
- injects the chosen skill into the final LLM prompt
## Flow
```mermaid
graph TD
A[SelectSkill] --> B[ApplySkill]
```
1. **SelectSkill** picks a skill file (e.g. executive brief vs checklist writer)
2. **ApplySkill** reads that skill and executes the task with the LLM
## Run
```bash
pip install -r requirements.txt
export OPENAI_API_KEY="your-key"
python main.py --"Summarize this launch plan for a VP audience"
```
Try another task:
```bash
python main.py --"Turn this into an implementation checklist"
```
## Files
- `main.py` — CLI entry
- `flow.py` — graph wiring
- `nodes.py` — skill selection + execution nodes
- `utils.py` — load skills + LLM helper
- `skills/*.md` — reusable Agent Skills
================================================
FILE: cookbook/pocketflow-agent-skills/flow.py
================================================
from pocketflow import Flow
from nodes import SelectSkill, ApplySkill
def create_flow():
select_skill = SelectSkill()
apply_skill = ApplySkill()
select_skill >> apply_skill
return Flow(start=select_skill)
================================================
FILE: cookbook/pocketflow-agent-skills/main.py
================================================
import sys
from flow import create_flow
def parse_task(default_task: str) -> str:
for arg in sys.argv[1:]:
if arg.startswith("--"):
return arg[2:]
return default_task
def main():
task = parse_task("Summarize this launch plan for a VP audience")
shared = {
"task": task,
"skills_dir": "skills",
}
flow = create_flow()
print(f"🧩 Task: {task}")
flow.run(shared)
print("\n=== Skill Used ===")
print(shared.get("selected_skill", "(none)"))
print("\n=== Output ===")
print(shared.get("result", "(no result)"))
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-agent-skills/nodes.py
================================================
from pocketflow import Node
from utils import call_llm, load_skills
class SelectSkill(Node):
def prep(self, shared):
return {
"task": shared["task"],
"skills": load_skills(shared["skills_dir"]),
}
def exec(self, prep_res):
task = prep_res["task"].lower()
skills = prep_res["skills"]
# Tiny deterministic router for demo purposes.
if "checklist" in task or "steps" in task:
preferred = "checklist_writer"
else:
preferred = "executive_brief"
if preferred in skills:
return preferred, skills[preferred]
# fallback: first available skill
name, content = next(iter(skills.items()))
return name, content
def post(self, shared, prep_res, exec_res):
skill_name, skill_content = exec_res
shared["selected_skill"] = skill_name
shared["selected_skill_content"] = skill_content
return "default"
class ApplySkill(Node):
def prep(self, shared):
return {
"task": shared["task"],
"skill_name": shared["selected_skill"],
"skill_content": shared["selected_skill_content"],
}
def exec(self, prep_res):
prompt = f"""
You are running an Agent Skill.
Skill name: {prep_res['skill_name']}
Skill instructions:
---
{prep_res['skill_content']}
---
User task:
{prep_res['task']}
Follow the skill instructions exactly and return the final result only.
""".strip()
return call_llm(prompt)
def post(self, shared, prep_res, exec_res):
shared["result"] = exec_res
return "default"
================================================
FILE: cookbook/pocketflow-agent-skills/requirements.txt
================================================
pocketflow>=0.0.1
openai>=1.0.0
================================================
FILE: cookbook/pocketflow-agent-skills/skills/checklist_writer.md
================================================
# Checklist Writer Skill
Convert requests into clear, actionable checklists.
## Rules
- Use numbered steps.
- Keep each step short and verifiable.
- Highlight dependencies and blockers.
- End with a "Definition of Done" section.
================================================
FILE: cookbook/pocketflow-agent-skills/skills/executive_brief.md
================================================
# Executive Brief Skill
You are writing for senior leaders.
## Rules
- Keep it concise and decision-oriented.
- Start with 3 bullet point summary.
- Include risks and recommended next action.
- Avoid implementation-level details unless critical.
================================================
FILE: cookbook/pocketflow-agent-skills/utils.py
================================================
from pathlib import Path
import os
from openai import OpenAI
def load_skills(skills_dir: str) -> dict[str, str]:
skills = {}
for md_file in sorted(Path(skills_dir).glob("*.md")):
skills[md_file.stem] = md_file.read_text(encoding="utf-8")
if not skills:
raise ValueError(f"No skill files found in {skills_dir}")
return skills
def call_llm(prompt: str) -> str:
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
)
return response.choices[0].message.content
================================================
FILE: cookbook/pocketflow-async-basic/README.md
================================================
# PocketFlow Async Basic Example
This example demonstrates async operations using a simple Recipe Finder that:
1. Fetches recipes from an API (async HTTP)
2. Processes them with an LLM (async LLM)
3. Waits for user confirmation (async input)
## What this Example Does
When you run the example:
1. You enter an ingredient (e.g., "chicken")
2. It searches for recipes (async API call)
3. It suggests a recipe (async LLM call)
4. You approve or reject the suggestion
5. If rejected, it tries again with a different recipe
## How it Works
1. **FetchRecipes (AsyncNode)**
```python
async def prep_async(self, shared):
ingredient = input("Enter ingredient: ")
return ingredient
async def exec_async(self, ingredient):
# Async API call
recipes = await fetch_recipes(ingredient)
return recipes
```
2. **SuggestRecipe (AsyncNode)**
```python
async def exec_async(self, recipes):
# Async LLM call
suggestion = await call_llm_async(
f"Choose best recipe from: {recipes}"
)
return suggestion
```
3. **GetApproval (AsyncNode)**
```python
async def post_async(self, shared, prep_res, suggestion):
# Async user input
answer = await get_user_input(
f"Accept {suggestion}? (y/n): "
)
return "accept" if answer == "y" else "retry"
```
## Running the Example
```bash
pip install -r requirements.txt
python main.py
```
## Sample Interaction
```
Enter ingredient: chicken
Fetching recipes...
Found 3 recipes.
Suggesting best recipe...
How about: Grilled Chicken with Herbs
Accept this recipe? (y/n): n
Suggesting another recipe...
How about: Chicken Stir Fry
Accept this recipe? (y/n): y
Great choice! Here's your recipe...
```
## Key Concepts
1. **Async Operations**: Using `async/await` for:
- API calls (non-blocking I/O)
- LLM calls (potentially slow)
- User input (waiting for response)
2. **AsyncNode Methods**:
- `prep_async`: Setup and data gathering
- `exec_async`: Main async processing
- `post_async`: Post-processing and decisions
3. **Flow Control**:
- Actions ("accept"/"retry") control flow
- Retry loop for rejected suggestions
================================================
FILE: cookbook/pocketflow-async-basic/flow.py
================================================
"""AsyncFlow implementation for recipe finder."""
from pocketflow import AsyncFlow, Node
from nodes import FetchRecipes, SuggestRecipe, GetApproval
class NoOp(Node):
"""Node that does nothing, used to properly end the flow."""
pass
def create_flow():
"""Create and connect nodes into a flow."""
# Create nodes
fetch = FetchRecipes()
suggest = SuggestRecipe()
approve = GetApproval()
end = NoOp()
# Connect nodes
fetch - "suggest" >> suggest
suggest - "approve" >> approve
approve - "retry" >> suggest # Loop back for another suggestion
approve - "accept" >> end # Properly end the flow
# Create flow starting with fetch
flow = AsyncFlow(start=fetch)
return flow
================================================
FILE: cookbook/pocketflow-async-basic/main.py
================================================
import asyncio
from flow import create_flow
async def main():
"""Run the recipe finder flow."""
# Create flow
flow = create_flow()
# Create shared store
shared = {}
# Run flow
print("\nWelcome to Recipe Finder!")
print("------------------------")
await flow.run_async(shared)
print("\nThanks for using Recipe Finder!")
if __name__ == "__main__":
# Run the async main function
asyncio.run(main())
================================================
FILE: cookbook/pocketflow-async-basic/nodes.py
================================================
from pocketflow import AsyncNode
from utils import fetch_recipes, call_llm_async, get_user_input
class FetchRecipes(AsyncNode):
"""AsyncNode that fetches recipes."""
async def prep_async(self, shared):
"""Get ingredient from user."""
ingredient = await get_user_input("Enter ingredient: ")
return ingredient
async def exec_async(self, ingredient):
"""Fetch recipes asynchronously."""
recipes = await fetch_recipes(ingredient)
return recipes
async def post_async(self, shared, prep_res, recipes):
"""Store recipes and continue."""
shared["recipes"] = recipes
shared["ingredient"] = prep_res
return "suggest"
class SuggestRecipe(AsyncNode):
"""AsyncNode that suggests a recipe using LLM."""
async def prep_async(self, shared):
"""Get recipes from shared store."""
return shared["recipes"]
async def exec_async(self, recipes):
"""Get suggestion from LLM."""
suggestion = await call_llm_async(
f"Choose best recipe from: {', '.join(recipes)}"
)
return suggestion
async def post_async(self, shared, prep_res, suggestion):
"""Store suggestion and continue."""
shared["suggestion"] = suggestion
return "approve"
class GetApproval(AsyncNode):
"""AsyncNode that gets user approval."""
async def prep_async(self, shared):
"""Get current suggestion."""
return shared["suggestion"]
async def exec_async(self, suggestion):
"""Ask for user approval."""
answer = await get_user_input(f"\nAccept this recipe? (y/n): ")
return answer
async def post_async(self, shared, prep_res, answer):
"""Handle user's decision."""
if answer == "y":
print("\nGreat choice! Here's your recipe...")
print(f"Recipe: {shared['suggestion']}")
print(f"Ingredient: {shared['ingredient']}")
return "accept"
else:
print("\nLet's try another recipe...")
return "retry"
================================================
FILE: cookbook/pocketflow-async-basic/requirements.txt
================================================
pocketflow
aiohttp>=3.8.0 # For async HTTP requests
openai>=1.0.0 # For async LLM calls
================================================
FILE: cookbook/pocketflow-async-basic/utils.py
================================================
import asyncio
import aiohttp
from openai import AsyncOpenAI
async def fetch_recipes(ingredient):
"""Fetch recipes from an API asynchronously."""
print(f"Fetching recipes for {ingredient}...")
# Simulate API call with delay
await asyncio.sleep(1)
# Mock recipes (in real app, would fetch from API)
recipes = [
f"{ingredient} Stir Fry",
f"Grilled {ingredient} with Herbs",
f"Baked {ingredient} with Vegetables"
]
print(f"Found {len(recipes)} recipes.")
return recipes
async def call_llm_async(prompt):
"""Make async LLM call."""
print("\nSuggesting best recipe...")
# Simulate LLM call with delay
await asyncio.sleep(1)
# Mock LLM response (in real app, would call OpenAI)
recipes = prompt.split(": ")[1].split(", ")
suggestion = recipes[1] # Always suggest second recipe
print(f"How about: {suggestion}")
return suggestion
async def get_user_input(prompt):
"""Get user input asynchronously."""
# Create event loop to handle async input
loop = asyncio.get_event_loop()
# Get input in a non-blocking way
answer = await loop.run_in_executor(None, input, prompt)
return answer.lower()
================================================
FILE: cookbook/pocketflow-batch/README.md
================================================
# Batch Translation Process
This project demonstrates a batch processing implementation that enables LLMs to translate documents into multiple languages simultaneously. It's designed to efficiently handle the translation of markdown files while preserving formatting.
## Features
- Translates markdown content into multiple languages in parallel
- Saves translated files to specified output directory
## Getting Started
1. Install the required packages:
```bash
pip install -r requirements.txt
```
2. Set up your API key:
```bash
export ANTHROPIC_API_KEY="your-api-key-here"
```
3. Run the translation process:
```bash
python main.py
```
## How It Works
The implementation uses a `TranslateTextNode` that processes batches of translation requests:
```mermaid
flowchart LR
batch[TranslateTextNode]
```
The `TranslateTextNode`:
1. Prepares batches for multiple language translations
2. Executes translations in parallel using the model
3. Saves the translated content to individual files
4. Maintains the original markdown structure
This approach demonstrates how PocketFlow can efficiently process multiple related tasks in parallel.
## Example Output
When you run the translation process, you'll see output similar to this:
```
Translated Chinese text
Translated Spanish text
Translated Japanese text
Translated German text
Translated Russian text
Translated Portuguese text
Translated French text
Translated Korean text
Saved translation to translations/README_CHINESE.md
Saved translation to translations/README_SPANISH.md
Saved translation to translations/README_JAPANESE.md
Saved translation to translations/README_GERMAN.md
Saved translation to translations/README_RUSSIAN.md
Saved translation to translations/README_PORTUGUESE.md
Saved translation to translations/README_FRENCH.md
Saved translation to translations/README_KOREAN.md
=== Translation Complete ===
Translations saved to: translations
============================
```
## Files
- [`main.py`](./main.py): Implementation of the batch translation node
- [`utils.py`](./utils.py): Simple wrapper for calling the Anthropic model
- [`requirements.txt`](./requirements.txt): Project dependencies
The translations are saved to the `translations` directory, with each file named according to the target language.
================================================
FILE: cookbook/pocketflow-batch/main.py
================================================
import os
import time
from pocketflow import BatchNode, Flow
from utils import call_llm
class TranslateTextNode(BatchNode):
def prep(self, shared):
text = shared.get("text", "(No text provided)")
languages = shared.get("languages", ["Chinese", "Spanish", "Japanese", "German",
"Russian", "Portuguese", "French", "Korean"])
# Create batches for each language translation
return [(text, lang) for lang in languages]
def exec(self, data_tuple):
text, language = data_tuple
prompt = f"""
Please translate the following markdown file into {language}.
But keep the original markdown format, links and code blocks.
Directly return the translated text, without any other text or comments.
Original:
{text}
Translated:"""
result = call_llm(prompt)
print(f"Translated {language} text")
return {"language": language, "translation": result}
def post(self, shared, prep_res, exec_res_list):
# Create output directory if it doesn't exist
output_dir = shared.get("output_dir", "translations")
os.makedirs(output_dir, exist_ok=True)
# Write each translation to a file
for result in exec_res_list:
language, translation = result["language"], result["translation"]
# Write to file
filename = os.path.join(output_dir, f"README_{language.upper()}.md")
with open(filename, "w", encoding="utf-8") as f:
f.write(translation)
print(f"Saved translation to {filename}")
if __name__ == "__main__":
# read the text from ../../README.md
with open("../../README.md", "r") as f:
text = f.read()
# Default settings
shared = {
"text": text,
"languages": ["Chinese", "Spanish", "Japanese", "German", "Russian", "Portuguese", "French", "Korean"],
"output_dir": "translations"
}
# --- Time Measurement Start ---
print(f"Starting sequential translation into {len(shared['languages'])} languages...")
start_time = time.perf_counter()
# Run the translation flow
translate_node = TranslateTextNode(max_retries=3)
flow = Flow(start=translate_node)
flow.run(shared)
# --- Time Measurement End ---
end_time = time.perf_counter()
duration = end_time - start_time
print(f"\nTotal sequential translation time: {duration:.4f} seconds") # Print duration
print("\n=== Translation Complete ===")
print(f"Translations saved to: {shared['output_dir']}")
print("============================")
================================================
FILE: cookbook/pocketflow-batch/requirements.txt
================================================
pocketflow>=0.0.1
anthropic>=0.15.0
pyyaml>=6.0
================================================
FILE: cookbook/pocketflow-batch/translations/README_CHINESE.md
================================================
[English](https://github.com/The-Pocket/PocketFlow/blob/main/README.md) | [中文](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_CHINESE.md) | [Español](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_SPANISH.md) | [日本語](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_JAPANESE.md) | [Deutsch](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_GERMAN.md) | [Русский](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_RUSSIAN.md) | [Português](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_PORTUGUESE.md) | Français | [한국어](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_KOREAN.md)

[](https://the-pocket.github.io/PocketFlow/)
Pocket Flow est un framework LLM minimaliste en [100 lignes](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)
- **Léger** : Seulement 100 lignes. Zéro superflu, zéro dépendance, zéro verrouillage fournisseur.
- **Expressif** : Tout ce que vous aimez — ([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agents](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), et plus encore.
- **[Programmation Agentique](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)** : Laissez les Agents IA (par exemple, Cursor AI) créer des Agents — augmentez votre productivité par 10 !
Commencer avec Pocket Flow :
- Pour installer, ```pip install pocketflow``` ou copiez simplement le [code source](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) (seulement 100 lignes).
- Pour en savoir plus, consultez la [documentation](https://the-pocket.github.io/PocketFlow/). Pour comprendre la motivation, lisez l'[histoire](https://zacharyhuang.substack.com/p/i-built-an-llm-framework-in-just).
- Des questions ? Consultez cet [Assistant IA](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant), ou [créez une issue !](https://github.com/The-Pocket/PocketFlow/issues/new)
- 🎉 Rejoignez notre [Discord](https://discord.gg/hUHHE9Sa6T) pour vous connecter avec d'autres développeurs utilisant Pocket Flow !
- 🎉 Pocket Flow est initialement en Python, mais nous avons maintenant des versions en [Typescript](https://github.com/The-Pocket/PocketFlow-Typescript), [Java](https://github.com/The-Pocket/PocketFlow-Java), [C++](https://github.com/The-Pocket/PocketFlow-CPP) et [Go](https://github.com/The-Pocket/PocketFlow-Go) !
## Pourquoi Pocket Flow ?
Les frameworks LLM actuels sont surchargés... Vous n'avez besoin que de 100 lignes pour un framework LLM !
## Comment fonctionne Pocket Flow ?
Les [100 lignes](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) capturent l'abstraction fondamentale des frameworks LLM : le Graph !
De là, il est facile d'implémenter des modèles de conception populaires comme ([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agents](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), etc.
✨ Voici des tutoriels de base :
| Nom | Difficulté | Description |
| :-------------: | :-------------: | :--------------------- |
| [Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *Débutant* | Un chatbot basique avec historique de conversation |
| [Sortie Structurée](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *Débutant* | Extraction de données structurées à partir de CV par prompt |
| [Workflow](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *Débutant* | Un workflow d'écriture qui planifie, rédige du contenu et applique un style |
| [Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *Débutant* | Un agent de recherche qui peut chercher sur le web et répondre aux questions |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *Débutant* | Un processus simple de génération augmentée par récupération |
| [Batch](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *Débutant* | Un processeur par lots qui traduit du contenu markdown en plusieurs langues |
| [Streaming](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *Débutant* | Une démo de streaming LLM en temps réel avec capacité d'interruption utilisateur |
| [Garde-fou de Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Débutant* | Un chatbot conseiller de voyage qui ne traite que les requêtes liées au voyage |
| [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ★☆☆ *Intermédiaire* | Un processeur de qualification de CV utilisant le modèle map-reduce pour l'évaluation par lots |
| [Multi-Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Intermédiaire* | Un jeu de Tabou pour la communication asynchrone entre deux agents |
| [Superviseur](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *Intermédiaire* | L'agent de recherche devient peu fiable... Construisons un processus de supervision |
| [Parallèle](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *Intermédiaire* | Une démo d'exécution parallèle montrant une accélération de 3x |
| [Flux Parallèle](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *Intermédiaire* | Une démo de traitement d'image parallèle montrant une accélération de 8x avec plusieurs filtres |
| [Vote Majoritaire](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ★☆☆ *Intermédiaire* | Améliorer la précision du raisonnement en agrégeant plusieurs tentatives de solution |
| [Réflexion](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *Intermédiaire* | Résoudre des problèmes de raisonnement complexes grâce à la Chaîne de Pensée |
| [Mémoire](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *Intermédiaire* | Un chatbot avec mémoire à court et long terme |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *Intermédiaire* | Convertir le langage naturel en requêtes SQL avec une boucle d'auto-débogage |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *Intermédiaire* | Agent utilisant le Protocole de Contexte de Modèle pour les opérations numériques |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *Intermédiaire* | Agent encapsulé avec le protocole Agent-to-Agent pour la communication inter-agent |
| [Web HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-web-hitl) | ★☆☆ *Intermédiaire* | Un service web minimal pour une boucle de révision humaine avec mises à jour SSE |
👀 Vous voulez voir d'autres tutoriels pour débutants ? [Créez une issue !](https://github.com/The-Pocket/PocketFlow/issues/new)
## Comment utiliser Pocket Flow ?
🚀 Par la **Programmation Agentique** — le paradigme de développement d'applications LLM le plus rapide — où *les humains conçoivent* et *les agents programment* !
✨ Voici des exemples d'applications LLM plus complexes :
| Nom de l'application | Difficulté | Sujets | Conception Humaine | Code Agent |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Construire Cursor avec Cursor](https://github.com/The-Pocket/Tutorial-Cursor) Nous atteindrons bientôt la singularité ... | ★★★ *Avancé* | [Agent](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [Document de conception](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [Code Flow](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [Constructeur de Connaissances de Base de Code](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) La vie est trop courte pour rester perplexe devant le code des autres | ★★☆ *Moyen* | [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [Document de conception](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [Code Flow](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [Interroger l'IA Paul Graham](https://github.com/The-Pocket/Tutorial-YC-Partner) Interrogez l'IA Paul Graham, au cas où vous ne seriez pas accepté | ★★☆ *Moyen* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [Document de conception](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [Code Flow](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [Résumeur Youtube](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) Vous explique les vidéos YouTube comme si vous aviez 5 ans | ★☆☆ *Intermédiaire* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [Document de conception](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [Code Flow](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [Générateur d'Accroche pour Email](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) Des brise-glaces instantanés qui transforment les prospects froids en prospects chauds | ★☆☆ *Intermédiaire* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [Recherche Web](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [Document de conception](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Code Flow](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- Vous voulez apprendre la **Programmation Agentique** ?
- Consultez [ma chaîne YouTube](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1) pour des tutoriels vidéo sur la façon dont certaines applications ci-dessus sont créées !
- Vous voulez créer votre propre application LLM ? Lisez cet [article](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to) ! Commencez avec [ce modèle](https://github.com/The-Pocket/PocketFlow-Template-Python) !
================================================
FILE: cookbook/pocketflow-batch/translations/README_GERMAN.md
================================================
[English](https://github.com/The-Pocket/PocketFlow/blob/main/README.md) | [中文](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_CHINESE.md) | [Español](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_SPANISH.md) | [日本語](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_JAPANESE.md) | Deutsch | [Русский](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_RUSSIAN.md) | [Português](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_PORTUGUESE.md) | [Français](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_FRENCH.md) | [한국어](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_KOREAN.md)

[](https://the-pocket.github.io/PocketFlow/)
Pocket Flow ist ein [100-zeiliges](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) minimalistisches LLM-Framework
- **Leichtgewichtig**: Nur 100 Zeilen. Kein Ballast, keine Abhängigkeiten, keine Anbieterbindung.
- **Ausdrucksstark**: Alles, was Sie lieben—([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agenten](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), und mehr.
- **[Agenten-basiertes Programmieren](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)**: Lassen Sie KI-Agenten (z.B. Cursor AI) Agenten bauen—10-fache Produktivitätssteigerung!
Erste Schritte mit Pocket Flow:
- Zur Installation, ```pip install pocketflow```oder kopieren Sie einfach den [Quellcode](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) (nur 100 Zeilen).
- Um mehr zu erfahren, schauen Sie in die [Dokumentation](https://the-pocket.github.io/PocketFlow/). Um die Motivation zu verstehen, lesen Sie die [Geschichte](https://zacharyhuang.substack.com/p/i-built-an-llm-framework-in-just).
- Haben Sie Fragen? Schauen Sie sich diesen [KI-Assistenten](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant) an, oder [erstellen Sie ein Issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
- 🎉 Treten Sie unserem [Discord](https://discord.gg/hUHHE9Sa6T) bei, um sich mit anderen Entwicklern zu vernetzen, die mit Pocket Flow arbeiten!
- 🎉 Pocket Flow ist ursprünglich in Python geschrieben, aber wir haben jetzt auch Versionen für [Typescript](https://github.com/The-Pocket/PocketFlow-Typescript), [Java](https://github.com/The-Pocket/PocketFlow-Java), [C++](https://github.com/The-Pocket/PocketFlow-CPP) und [Go](https://github.com/The-Pocket/PocketFlow-Go)!
## Warum Pocket Flow?
Aktuelle LLM-Frameworks sind aufgebläht... Sie brauchen nur 100 Zeilen für ein LLM-Framework!
## Wie funktioniert Pocket Flow?
Die [100 Zeilen](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) erfassen die Kernabstraktion von LLM-Frameworks: Graph!
Von dort aus ist es einfach, beliebte Designmuster wie ([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agenten](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), etc. zu implementieren.
✨ Hier sind grundlegende Tutorials:
| Name | Schwierigkeit | Beschreibung |
| :-------------: | :-------------: | :--------------------- |
| [Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *Anfänger* | Ein einfacher Chatbot mit Gesprächsverlauf |
| [Strukturierte Ausgabe](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *Anfänger* | Extraktion strukturierter Daten aus Lebensläufen durch Prompting |
| [Workflow](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *Anfänger* | Ein Schreib-Workflow, der gliedert, Inhalte schreibt und Formatierungen anwendet |
| [Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *Anfänger* | Ein Recherche-Agent, der im Web suchen und Fragen beantworten kann |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *Anfänger* | Ein einfacher Abrufsaugmentierter Generierungsprozess |
| [Batch](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *Anfänger* | Ein Batch-Prozessor, der Markdown-Inhalte in mehrere Sprachen übersetzt |
| [Streaming](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *Anfänger* | Eine Echtzeit-LLM-Streaming-Demo mit Benutzer-Unterbrechungsfunktion |
| [Chat-Leitplanke](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Anfänger* | Ein Reiseberater-Chatbot, der nur reisebezogene Anfragen verarbeitet |
| [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ★☆☆ *Einsteiger* | Ein Lebenslauf-Qualifikationsprozessor, der das Map-Reduce-Muster für Batch-Auswertungen verwendet |
| [Multi-Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Einsteiger* | Ein Tabu-Wortspiel für asynchrone Kommunikation zwischen zwei Agenten |
| [Supervisor](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *Einsteiger* | Forschungsagent wird unzuverlässig... Bauen wir einen Überwachungsprozess auf|
| [Parallel](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *Einsteiger* | Eine parallele Ausführungsdemo, die 3-fache Beschleunigung zeigt |
| [Paralleler Flow](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *Einsteiger* | Eine parallele Bildverarbeitungsdemo, die 8-fache Beschleunigung mit mehreren Filtern zeigt |
| [Mehrheitswahl](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ★☆☆ *Einsteiger* | Verbesserte Schlussfolgerungsgenauigkeit durch Aggregation mehrerer Lösungsversuche |
| [Denken](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *Einsteiger* | Lösen komplexer Schlussfolgerungsprobleme durch Chain-of-Thought |
| [Gedächtnis](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *Einsteiger* | Ein Chatbot mit Kurz- und Langzeitgedächtnis |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *Einsteiger* | Konvertierung natürlicher Sprache in SQL-Abfragen mit Auto-Debug-Schleife |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *Einsteiger* | Agent mit Model Context Protocol für numerische Operationen |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *Einsteiger* | Agent mit Agent-to-Agent-Protokoll für Inter-Agenten-Kommunikation |
| [Web HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-web-hitl) | ★☆☆ *Einsteiger* | Ein minimaler Webdienst für eine menschliche Überprüfungsschleife mit SSE-Updates |
👀 Möchten Sie andere Tutorials für Anfänger sehen? [Erstellen Sie ein Issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
## Wie verwendet man Pocket Flow?
🚀 Durch **Agenten-basiertes Programmieren**—das schnellste LLM-App-Entwicklungsparadigma, bei dem *Menschen entwerfen* und *Agenten programmieren*!
✨ Hier sind Beispiele für komplexere LLM-Apps:
| App-Name | Schwierigkeit | Themen | Menschlicher Entwurf | Agent-Code |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Cursor mit Cursor bauen](https://github.com/The-Pocket/Tutorial-Cursor) Wir werden bald die Singularität erreichen ... | ★★★ *Fortgeschritten* | [Agent](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [Design-Dokument](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [Flow-Code](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [Codebase-Wissensgenerator](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) Das Leben ist zu kurz, um ratlos fremden Code anzustarren | ★★☆ *Mittel* | [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [Design-Dokument](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [Flow-Code](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [Frage KI Paul Graham](https://github.com/The-Pocket/Tutorial-YC-Partner) Frage KI Paul Graham, falls du nicht reinkommst | ★★☆ *Mittel* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [Design-Dokument](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [Flow-Code](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [Youtube-Zusammenfasser](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) Erklärt YouTube-Videos so, als wärst du 5 | ★☆☆ *Einsteiger* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [Design-Dokument](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [Flow-Code](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [Cold-Opener-Generator](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) Sofortige Eisbrecher, die kalte Leads heiß machen | ★☆☆ *Einsteiger* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [Web-Suche](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [Design-Dokument](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Flow-Code](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- Möchten Sie **Agenten-basiertes Programmieren** lernen?
- Schauen Sie sich [meinen YouTube-Kanal](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1) für Video-Tutorials an, wie einige der oben genannten Apps erstellt wurden!
- Möchten Sie Ihre eigene LLM-App erstellen? Lesen Sie diesen [Beitrag](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)! Beginnen Sie mit [dieser Vorlage](https://github.com/The-Pocket/PocketFlow-Template-Python)!
================================================
FILE: cookbook/pocketflow-batch/translations/README_JAPANESE.md
================================================
## Pocket Flow는 어떻게 작동하나요?
[100줄](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)의 코드는 LLM 프레임워크의 핵심 추상화인 그래프를 구현합니다!
이를 기반으로 ([멀티-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[에이전트](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [워크플로우](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) 등의 인기 있는 디자인 패턴을 쉽게 구현할 수 있습니다.
✨ 아래는 기본 튜토리얼입니다:
| 이름 | 난이도 | 설명 |
| :-------------: | :-------------: | :--------------------- |
| [채팅](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *초보* | 대화 기록을 가진 기본 채팅봇 |
| [구조화된 출력](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *초보* | 프롬프트를 통해 이력서에서 구조화된 데이터 추출 |
| [워크플로우](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *초보* | 개요 작성, 내용 작성, 스타일 적용이 포함된 작성 워크플로우 |
| [에이전트](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *초보* | 웹을 검색하고 질문에 답할 수 있는 연구 에이전트 |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *초보* | 간단한 검색 증강 생성 프로세스 |
| [배치](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *초보* | 마크다운 콘텐츠를 여러 언어로 번역하는 배치 프로세서 |
| [스트리밍](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *초보* | 사용자 중단 기능이 있는 실시간 LLM 스트리밍 데모 |
| [채팅 가드레일](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *초보* | 여행 관련 쿼리만 처리하는 여행 상담 채팅봇 |
| [맵-리듀스](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ★☆☆ *초급* | 배치 평가를 위한 맵-리듀스 패턴을 사용하는 이력서 자격 처리기 |
| [멀티-에이전트](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *초급* | 두 에이전트 간의 비동기 통신을 위한 금지어 게임 |
| [감독자](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *초급* | 연구 에이전트가 불안정할 때... 감독 프로세스를 구축해 봅시다 |
| [병렬](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *초급* | 3배 속도 향상을 보여주는 병렬 실행 데모 |
| [병렬 플로우](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *초급* | 여러 필터를 사용한 8배 속도 향상을 보여주는 병렬 이미지 처리 데모 |
| [다수결 투표](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ★☆☆ *초급* | 여러 솔루션 시도를 집계하여 추론 정확도 향상 |
| [사고](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *초급* | Chain-of-Thought를 통한 복잡한 추론 문제 해결 |
| [메모리](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *초급* | 단기 및 장기 메모리가 있는 채팅봇 |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *초급* | 자동 디버그 루프가 있는 자연어에서 SQL 쿼리로 변환 |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *초급* | 수치 연산을 위한 모델 컨텍스트 프로토콜을 사용하는 에이전트 |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *초급* | 에이전트 간 통신을 위한 Agent-to-Agent 프로토콜로 래핑된 에이전트 |
| [웹 HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-web-hitl) | ★☆☆ *초급* | SSE 업데이트가 있는 인간 검토 루프를 위한 최소한의 웹 서비스 |
👀 더 많은 초보자용 튜토리얼을 보고 싶으신가요? [이슈를 생성하세요!](https://github.com/The-Pocket/PocketFlow/issues/new)
## Pocket Flow를 어떻게 사용하나요?
🚀 **에이전트 코딩**을 통해—가장 빠른 LLM 앱 개발 패러다임으로, *인간이 설계*하고 *에이전트가 코딩*합니다!
✨ 아래는 더 복잡한 LLM 앱의 예시입니다:
| 앱 이름 | 난이도 | 주제 | 인간 설계 | 에이전트 코드 |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Cursor로 Cursor 만들기](https://github.com/The-Pocket/Tutorial-Cursor) 곧 기술적 특이점에 도달할 것입니다... | ★★★ *고급* | [에이전트](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [설계 문서](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [플로우 코드](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [코드베이스 지식 빌더](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) 인생은 다른 사람의 코드를 혼란스럽게 바라볼 만큼 길지 않습니다 | ★★☆ *중급* | [워크플로우](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [설계 문서](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [플로우 코드](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [AI Paul Graham에게 물어보기](https://github.com/The-Pocket/Tutorial-YC-Partner) 합격하지 못한 경우를 대비해 AI Paul Graham에게 물어보세요 | ★★☆ *중급* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [맵 리듀스](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [설계 문서](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [플로우 코드](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [유튜브 요약기](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) 5살 아이에게 설명하듯 YouTube 동영상 설명 | ★☆☆ *초급* | [맵 리듀스](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [설계 문서](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [플로우 코드](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [콜드 오프너 생성기](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) 차가운 잠재 고객을 뜨겁게 만드는 즉각적인 아이스브레이커 | ★☆☆ *초급* | [맵 리듀스](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [웹 검색](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [설계 문서](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [플로우 코드](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- **에이전트 코딩**을 배우고 싶으신가요?
- 위에 소개된 앱들이 어떻게 만들어졌는지 비디오 튜토리얼을 보려면 [제 YouTube](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1)를 확인하세요!
- 자신만의 LLM 앱을 만들고 싶으신가요? 이 [포스트](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)를 읽어보세요! [이 템플릿](https://github.com/The-Pocket/PocketFlow-Template-Python)으로 시작하세요!
================================================
FILE: cookbook/pocketflow-batch/translations/README_PORTUGUESE.md
================================================
[English](https://github.com/The-Pocket/PocketFlow/blob/main/README.md) | [中文](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_CHINESE.md) | [Español](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_SPANISH.md) | [日本語](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_JAPANESE.md) | [Deutsch](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_GERMAN.md) | [Русский](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_RUSSIAN.md) | Português | [Français](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_FRENCH.md) | [한국어](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_KOREAN.md)

[](https://the-pocket.github.io/PocketFlow/)
Pocket Flow é um framework minimalista para LLM com [apenas 100 linhas](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)
- **Leve**: Apenas 100 linhas. Zero inchaço, zero dependências, zero aprisionamento a fornecedores.
- **Expressivo**: Tudo o que você adora—([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agentes](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Fluxo de Trabalho](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), e mais.
- **[Codificação Agêntica](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)**: Deixe que Agentes de IA (ex: Cursor AI) construam Agentes—aumento de produtividade de 10x!
Comece com o Pocket Flow:
- Para instalar, ```pip install pocketflow``` ou apenas copie o [código-fonte](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) (apenas 100 linhas).
- Para saber mais, consulte a [documentação](https://the-pocket.github.io/PocketFlow/). Para entender a motivação, leia a [história](https://zacharyhuang.substack.com/p/i-built-an-llm-framework-in-just).
- Tem perguntas? Consulte este [Assistente de IA](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant), ou [crie uma issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
- 🎉 Junte-se ao nosso [Discord](https://discord.gg/hUHHE9Sa6T) para se conectar com outros desenvolvedores construindo com o Pocket Flow!
- 🎉 O Pocket Flow é inicialmente em Python, mas agora temos versões em [Typescript](https://github.com/The-Pocket/PocketFlow-Typescript), [Java](https://github.com/The-Pocket/PocketFlow-Java), [C++](https://github.com/The-Pocket/PocketFlow-CPP) e [Go](https://github.com/The-Pocket/PocketFlow-Go)!
## Por que Pocket Flow?
Os frameworks LLM atuais são pesados... Você só precisa de 100 linhas para um Framework LLM!
## Como funciona o Pocket Flow?
As [100 linhas](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) capturam a abstração central dos frameworks LLM: o Grafo!
A partir daí, é fácil implementar padrões de design populares como ([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agentes](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Fluxo de Trabalho](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), etc.
✨ Abaixo estão tutoriais básicos:
| Nome | Dificuldade | Descrição |
| :-------------: | :-------------: | :--------------------- |
| [Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *Básico* | Um chatbot básico com histórico de conversação |
| [Saída Estruturada](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *Básico* | Extraindo dados estruturados de currículos por prompt |
| [Fluxo de Trabalho](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *Básico* | Um fluxo de escrita que esboça, escreve conteúdo e aplica estilo |
| [Agente](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *Básico* | Um agente de pesquisa que pode buscar na web e responder perguntas |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *Básico* | Um processo simples de Geração Aumentada por Recuperação |
| [Processamento em Lote](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *Básico* | Um processador em lote que traduz conteúdo markdown para vários idiomas |
| [Streaming](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *Básico* | Uma demonstração de streaming LLM em tempo real com capacidade de interrupção pelo usuário |
| [Guardrail de Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Básico* | Um chatbot de consultoria de viagens que processa apenas consultas relacionadas a viagens |
| [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ★☆☆ *Iniciante* | Um processador de qualificação de currículos usando o padrão map-reduce para avaliação em lote |
| [Multi-Agente](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Iniciante* | Um jogo de Tabu para comunicação assíncrona entre dois agentes |
| [Supervisor](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *Iniciante* | O agente de pesquisa está ficando pouco confiável... Vamos criar um processo de supervisão |
| [Paralelo](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *Iniciante* | Uma demonstração de execução paralela que mostra um aumento de velocidade de 3x |
| [Fluxo Paralelo](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *Iniciante* | Uma demonstração de processamento de imagem paralelo mostrando um aumento de velocidade de 8x com múltiplos filtros |
| [Voto Majoritário](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ★☆☆ *Iniciante* | Melhore a precisão de raciocínio agregando múltiplas tentativas de solução |
| [Pensamento](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *Iniciante* | Resolva problemas complexos de raciocínio através de Cadeia-de-Pensamento |
| [Memória](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *Iniciante* | Um chatbot com memória de curto e longo prazo |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *Iniciante* | Converta linguagem natural para consultas SQL com um loop de autodepuração |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *Iniciante* | Agente usando o Protocolo de Contexto de Modelo para operações numéricas |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *Iniciante* | Agente envolvido com o protocolo Agente-para-Agente para comunicação entre agentes |
| [Web HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-web-hitl) | ★☆☆ *Iniciante* | Um serviço web mínimo para um loop de revisão humana com atualizações SSE |
👀 Quer ver outros tutoriais para iniciantes? [Crie uma issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
## Como usar o Pocket Flow?
🚀 Através da **Codificação Agêntica**—o paradigma mais rápido de desenvolvimento de aplicativos LLM—onde *humanos projetam* e *agentes codificam*!
✨ Abaixo estão exemplos de aplicativos LLM mais complexos:
| Nome do Aplicativo | Dificuldade | Tópicos | Design Humano | Código do Agente |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Construir Cursor com Cursor](https://github.com/The-Pocket/Tutorial-Cursor) Logo chegaremos à singularidade ... | ★★★ *Avançado* | [Agente](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [Doc de Design](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [Código de Fluxo](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [Construtor de Conhecimento de Base de Código](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) A vida é curta demais para ficar olhando o código dos outros em confusão | ★★☆ *Médio* | [Fluxo de Trabalho](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [Doc de Design](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [Código de Fluxo](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [Pergunte à IA Paul Graham](https://github.com/The-Pocket/Tutorial-YC-Partner) Pergunte à IA Paul Graham, caso você não consiga entrar | ★★☆ *Médio* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [Doc de Design](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [Código de Fluxo](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [Resumidor de Youtube](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) Explica vídeos do YouTube para você como se você tivesse 5 anos | ★☆☆ *Iniciante* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [Doc de Design](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [Código de Fluxo](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [Gerador de Abertura a Frio](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) Quebra-gelos instantâneos que transformam leads frios em quentes | ★☆☆ *Iniciante* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [Busca Web](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [Doc de Design](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Código de Fluxo](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- Quer aprender **Codificação Agêntica**?
- Confira [meu YouTube](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1) para tutoriais em vídeo sobre como alguns dos aplicativos acima são feitos!
- Quer construir seu próprio aplicativo LLM? Leia este [post](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)! Comece com [este modelo](https://github.com/The-Pocket/PocketFlow-Template-Python)!
================================================
FILE: cookbook/pocketflow-batch/translations/README_RUSSIAN.md
================================================
[English](https://github.com/The-Pocket/PocketFlow/blob/main/README.md) | [中文](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_CHINESE.md) | [Español](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_SPANISH.md) | [日本語](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_JAPANESE.md) | [Deutsch](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_GERMAN.md) | Русский| [Português](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_PORTUGUESE.md) | [Français](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_FRENCH.md) | [한국어](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_KOREAN.md)

[](https://the-pocket.github.io/PocketFlow/)
Pocket Flow — это минималистичный фреймворк для LLM всего в [100 строк](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)
- **Легкий**: Всего 100 строк. Никакого лишнего веса, никаких зависимостей, никакой привязки к вендорам.
- **Выразительный**: Всё, что вы любите — ([Мульти-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Агенты](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Рабочие процессы](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) и многое другое.
- **[Агентское кодирование](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)**: Позвольте ИИ-агентам (например, Cursor AI) создавать других агентов — повысьте продуктивность в 10 раз!
Начало работы с Pocket Flow:
- Для установки, ```pip install pocketflow``` или просто скопируйте [исходный код](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) (всего 100 строк).
- Чтобы узнать больше, ознакомьтесь с [документацией](https://the-pocket.github.io/PocketFlow/). Чтобы понять мотивацию, прочитайте [историю](https://zacharyhuang.substack.com/p/i-built-an-llm-framework-in-just).
- Есть вопросы? Спросите этого [ИИ-ассистента](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant) или [создайте issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
- 🎉 Присоединяйтесь к нашему [Discord](https://discord.gg/hUHHE9Sa6T), чтобы общаться с другими разработчиками, использующими Pocket Flow!
- 🎉 Pocket Flow изначально написан на Python, но теперь у нас есть версии на [Typescript](https://github.com/The-Pocket/PocketFlow-Typescript), [Java](https://github.com/The-Pocket/PocketFlow-Java), [C++](https://github.com/The-Pocket/PocketFlow-CPP) и [Go](https://github.com/The-Pocket/PocketFlow-Go)!
## Почему Pocket Flow?
Современные фреймворки для LLM слишком громоздкие... Для фреймворка LLM достаточно всего 100 строк!
| | **Абстракция** | **Обертки для конкретных приложений** | **Обертки для конкретных вендоров** | **Строк** | **Размер** |
|----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:|
| LangChain | Agent, Chain | Много (напр., QA, Суммаризация) | Много (напр., OpenAI, Pinecone и т.д.) | 405K | +166MB |
| CrewAI | Agent, Chain | Много (напр., FileReadTool, SerperDevTool) | Много (напр., OpenAI, Anthropic, Pinecone и т.д.) | 18K | +173MB |
| SmolAgent | Agent | Несколько (напр., CodeAgent, VisitWebTool) | Несколько (напр., DuckDuckGo, Hugging Face и т.д.) | 8K | +198MB |
| LangGraph | Agent, Graph | Несколько (напр., Semantic Search) | Несколько (напр., PostgresStore, SqliteSaver и т.д.) | 37K | +51MB |
| AutoGen | Agent | Несколько (напр., Tool Agent, Chat Agent) | Много [Опционально] (напр., OpenAI, Pinecone и т.д.) | 7K (только ядро) | +26MB (только ядро) |
| **PocketFlow** | **Graph** | **Нет** | **Нет** | **100** | **+56KB** |
## Как работает Pocket Flow?
[100 строк](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) охватывают ключевую абстракцию фреймворков LLM: Граф!
Отсюда легко реализовать популярные шаблоны проектирования, такие как ([Мульти-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Агенты](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Рабочие процессы](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) и другие.
✨ Ниже приведены базовые руководства:
| Название | Сложность | Описание |
| :-------------: | :-------------: | :--------------------- |
| [Чат](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *Простейший* | Базовый чат-бот с историей разговора |
| [Структурированный вывод](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *Простейший* | Извлечение структурированных данных из резюме с помощью промптов |
| [Рабочий процесс](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *Простейший* | Процесс написания, который создает план, пишет контент и применяет стили |
| [Агент](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *Простейший* | Исследовательский агент, который может искать в интернете и отвечать на вопросы |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *Простейший* | Простой процесс генерации с извлечением информации |
| [Пакетная обработка](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *Простейший* | Пакетный процессор, который переводит markdown-контент на несколько языков |
| [Потоковая передача](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *Простейший* | Демонстрация потоковой передачи LLM в реальном времени с возможностью прерывания пользователем |
| [Ограничение чата](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Простейший* | Чат-бот туристического консультанта, обрабатывающий только запросы, связанные с путешествиями |
| [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ★☆☆ *Начальный* | Процессор квалификации резюме, использующий паттерн map-reduce для пакетной оценки |
| [Мульти-агент](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Начальный* | Игра Табу для асинхронного общения между двумя агентами |
| [Супервизор](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *Начальный* | Исследовательский агент становится ненадежным... Давайте создадим процесс надзора|
| [Параллельное выполнение](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *Начальный* | Демонстрация параллельного выполнения, показывающая 3-кратное ускорение |
| [Параллельный поток](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *Начальный* | Демонстрация параллельной обработки изображений, показывающая 8-кратное ускорение с несколькими фильтрами |
| [Голосование большинством](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ★☆☆ *Начальный* | Повышение точности рассуждений путем агрегации нескольких попыток решения |
| [Мышление](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *Начальный* | Решение сложных задач рассуждения с помощью цепочки размышлений |
| [Память](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *Начальный* | Чат-бот с кратковременной и долговременной памятью |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *Начальный* | Преобразование естественного языка в SQL-запросы с автоматическим циклом отладки |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *Начальный* | Агент, использующий протокол контекста модели для числовых операций |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *Начальный* | Агент, обернутый протоколом агент-к-агенту для межагентного взаимодействия |
| [Web HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-web-hitl) | ★☆☆ *Начальный* | Минимальный веб-сервис для цикла проверки человеком с обновлениями SSE |
👀 Хотите увидеть другие руководства для начинающих? [Создайте issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
## Как использовать Pocket Flow?
🚀 Через **Агентское кодирование** — самую быструю парадигму разработки LLM-приложений, где *люди проектируют*, а *агенты кодируют*!
✨ Ниже приведены примеры более сложных LLM-приложений:
| Название приложения | Сложность | Темы | Дизайн от человека | Код от агента |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Создание Cursor с помощью Cursor](https://github.com/The-Pocket/Tutorial-Cursor) Скоро достигнем сингулярности ... | ★★★ *Продвинутый* | [Агент](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [Документация по дизайну](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [Код потока](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [Конструктор знаний о кодовой базе](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) Жизнь слишком коротка, чтобы в растерянности смотреть на чужой код | ★★☆ *Средний* | [Рабочий процесс](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [Документация по дизайну](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [Код потока](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [Спроси ИИ Пола Грэма](https://github.com/The-Pocket/Tutorial-YC-Partner) Спроси ИИ Пола Грэма, если тебя не приняли | ★★☆ *Средний* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [Документация по дизайну](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [Код потока](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [Суммаризатор YouTube](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) Объясняет YouTube-видео как для 5-летнего | ★☆☆ *Начальный* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [Документация по дизайну](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [Код потока](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [Генератор холодных открытий](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) Мгновенные ледоколы, превращающие холодных лидов в горячих | ★☆☆ *Начальный* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [Веб-поиск](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [Документация по дизайну](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Код потока](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- Хотите изучить **Агентское кодирование**?
- Посмотрите [мой YouTube](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1) для видеоуроков о том, как создаются некоторые из вышеперечисленных приложений!
- Хотите создать свое собственное LLM-приложение? Прочитайте эту [статью](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)! Начните с [этого шаблона](https://github.com/The-Pocket/PocketFlow-Template-Python)!
================================================
FILE: cookbook/pocketflow-batch/translations/README_SPANISH.md
================================================
[English](https://github.com/The-Pocket/PocketFlow/blob/main/README.md) | [中文](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_CHINESE.md) | Español | [日本語](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_JAPANESE.md) | [Deutsch](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_GERMAN.md) | [Русский](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_RUSSIAN.md) | [Português](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_PORTUGUESE.md) | [Français](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_FRENCH.md) | [한국어](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_KOREAN.md)

[](https://the-pocket.github.io/PocketFlow/)
Pocket Flow es un framework minimalista de LLM de [100 líneas](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)
- **Ligero**: Solo 100 líneas. Cero hinchazón, cero dependencias, cero vinculación a proveedores.
- **Expresivo**: Todo lo que amas—([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agentes](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Flujo de Trabajo](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), y más.
- **[Programación mediante Agentes](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)**: Permite que los Agentes de IA (por ejemplo, Cursor AI) construyan Agentes—¡multiplicando la productividad por 10!
Comienza con Pocket Flow:
- Para instalar, ```pip install pocketflow``` o simplemente copia el [código fuente](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) (solo 100 líneas).
- Para aprender más, consulta la [documentación](https://the-pocket.github.io/PocketFlow/). Para conocer la motivación, lee la [historia](https://zacharyhuang.substack.com/p/i-built-an-llm-framework-in-just).
- ¿Tienes preguntas? Consulta este [Asistente de IA](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant), o [¡crea un issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
- 🎉 ¡Únete a nuestro [Discord](https://discord.gg/hUHHE9Sa6T) para conectar con otros desarrolladores construyendo con Pocket Flow!
- 🎉 Pocket Flow inicialmente está en Python, ¡pero ahora tenemos versiones en [Typescript](https://github.com/The-Pocket/PocketFlow-Typescript), [Java](https://github.com/The-Pocket/PocketFlow-Java), [C++](https://github.com/The-Pocket/PocketFlow-CPP) y [Go](https://github.com/The-Pocket/PocketFlow-Go)!
## ¿Por qué Pocket Flow?
Los frameworks actuales de LLM están sobrecargados... ¡Solo necesitas 100 líneas para un framework de LLM!
## ¿Cómo funciona Pocket Flow?
Las [100 líneas](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) capturan la abstracción principal de los frameworks de LLM: ¡el Grafo!
A partir de ahí, es fácil implementar patrones de diseño populares como ([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agentes](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Flujo de Trabajo](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), etc.
✨ A continuación se presentan tutoriales básicos:
| Nombre | Dificultad | Descripción |
| :-------------: | :-------------: | :--------------------- |
| [Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *Principiante* | Un chatbot básico con historial de conversación |
| [Salida Estructurada](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *Principiante* | Extracción de datos estructurados de currículums mediante prompts |
| [Flujo de Trabajo](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *Principiante* | Un flujo de escritura que esquematiza, escribe contenido y aplica estilo |
| [Agente](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *Principiante* | Un agente de investigación que puede buscar en la web y responder preguntas |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *Principiante* | Un simple proceso de Generación aumentada por Recuperación |
| [Procesamiento por Lotes](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *Principiante* | Un procesador por lotes que traduce contenido markdown a múltiples idiomas |
| [Streaming](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *Principiante* | Una demostración de streaming LLM en tiempo real con capacidad de interrupción del usuario |
| [Protección de Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Principiante* | Un chatbot asesor de viajes que solo procesa consultas relacionadas con viajes |
| [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ★☆☆ *Inicial* | Un procesador de calificación de currículums que utiliza el patrón map-reduce para evaluación por lotes |
| [Multi-Agente](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Inicial* | Un juego de palabras Tabú para comunicación asíncrona entre dos agentes |
| [Supervisor](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *Inicial* | El agente de investigación se vuelve poco fiable... Construyamos un proceso de supervisión|
| [Paralelo](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *Inicial* | Una demostración de ejecución paralela que muestra una aceleración de 3x |
| [Flujo Paralelo](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *Inicial* | Una demostración de procesamiento de imágenes en paralelo que muestra una aceleración de 8x con múltiples filtros |
| [Voto por Mayoría](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ★☆☆ *Inicial* | Mejora de la precisión del razonamiento mediante la agregación de múltiples intentos de solución |
| [Pensamiento](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *Inicial* | Resolver problemas de razonamiento complejos a través de Cadena de Pensamiento |
| [Memoria](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *Inicial* | Un chatbot con memoria a corto y largo plazo |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *Inicial* | Convertir lenguaje natural a consultas SQL con un bucle de auto-depuración |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *Inicial* | Agente que utiliza el Protocolo de Contexto de Modelo para operaciones numéricas |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *Inicial* | Agente envuelto con protocolo Agente-a-Agente para comunicación entre agentes |
| [Web HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-web-hitl) | ★☆☆ *Inicial* | Un servicio web mínimo para un bucle de revisión humana con actualizaciones SSE |
👀 ¿Quieres ver otros tutoriales para principiantes? [¡Crea un issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
## ¿Cómo usar Pocket Flow?
🚀 A través de la **Programación mediante Agentes**—el paradigma de desarrollo de aplicaciones LLM más rápido- donde *los humanos diseñan* y *los agentes programan*!
✨ A continuación hay ejemplos de aplicaciones LLM más complejas:
| Nombre de la App | Dificultad | Temas | Diseño Humano | Código del Agente |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Construir Cursor con Cursor](https://github.com/The-Pocket/Tutorial-Cursor) Pronto alcanzaremos la singularidad ... | ★★★ *Avanzado* | [Agente](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [Doc de Diseño](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [Código de Flujo](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [Constructor de Conocimiento de Código Base](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) La vida es demasiado corta para mirar el código de otros con confusión | ★★☆ *Medio* | [Flujo de Trabajo](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [Doc de Diseño](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [Código de Flujo](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [Pregunta a AI Paul Graham](https://github.com/The-Pocket/Tutorial-YC-Partner) Pregunta a AI Paul Graham, en caso de que no entres | ★★☆ *Medio* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [Doc de Diseño](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [Código de Flujo](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [Resumidor de Youtube](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) Explica videos de YouTube como si tuvieras 5 años | ★☆☆ *Principiante* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [Doc de Diseño](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [Código de Flujo](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [Generador de Introducción para Email Frío](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) Rompehielos instantáneos que convierten leads fríos en calientes | ★☆☆ *Principiante* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [Búsqueda Web](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [Doc de Diseño](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Código de Flujo](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- ¿Quieres aprender **Programación mediante Agentes**?
- ¡Consulta [mi YouTube](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1) para ver tutoriales en video sobre cómo se crearon algunas de las aplicaciones anteriores!
- ¿Quieres construir tu propia aplicación LLM? ¡Lee este [post](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)! ¡Comienza con [esta plantilla](https://github.com/The-Pocket/PocketFlow-Template-Python)!
================================================
FILE: cookbook/pocketflow-batch/utils.py
================================================
from anthropic import Anthropic
import os
def call_llm(prompt):
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "your-api-key"))
response = client.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=20000,
thinking={
"type": "enabled",
"budget_tokens": 16000
},
messages=[
{"role": "user", "content": prompt}
]
)
return response.content[1].text
if __name__ == "__main__":
print("## Testing call_llm")
prompt = "In a few words, what is the meaning of life?"
print(f"## Prompt: {prompt}")
response = call_llm(prompt)
print(f"## Response: {response}")
================================================
FILE: cookbook/pocketflow-batch-flow/README.md
================================================
# PocketFlow BatchFlow Example
This example demonstrates the BatchFlow concept in PocketFlow by implementing an image processor that applies different filters to multiple images.
## What this Example Demonstrates
- How to use BatchFlow to run a Flow multiple times with different parameters
- Key concepts of BatchFlow:
1. Creating a base Flow for single-item processing
2. Using BatchFlow to process multiple items with different parameters
3. Managing parameters across multiple Flow executions
## Project Structure
```
pocketflow-batch-flow/
├── README.md
├── requirements.txt
├── images/
│ ├── cat.jpg # Sample image 1
│ ├── dog.jpg # Sample image 2
│ └── bird.jpg # Sample image 3
├── main.py # Entry point
├── flow.py # Flow and BatchFlow definitions
└── nodes.py # Node implementations for image processing
```
## How it Works
The example processes multiple images with different filters:
1. **Base Flow**: Processes a single image
- Load image
- Apply filter (grayscale, blur, or sepia)
- Save processed image
2. **BatchFlow**: Processes multiple image-filter combinations
- Takes a list of parameters (image + filter combinations)
- Runs the base Flow for each parameter set
- Organizes output in a structured way
## Installation
```bash
pip install -r requirements.txt
```
## Usage
```bash
python main.py
```
## Sample Output
```
Processing images with filters...
Processing cat.jpg with grayscale filter...
Processing cat.jpg with blur filter...
Processing dog.jpg with sepia filter...
...
All images processed successfully!
Check the 'output' directory for results.
```
## Key Concepts Illustrated
1. **Parameter Management**: Shows how BatchFlow manages different parameter sets
2. **Flow Reuse**: Demonstrates running the same Flow multiple times
3. **Batch Processing**: Shows how to process multiple items efficiently
4. **Real-world Application**: Provides a practical example of batch processing
================================================
FILE: cookbook/pocketflow-batch-flow/flow.py
================================================
from pocketflow import Flow, BatchFlow
from nodes import LoadImage, ApplyFilter, SaveImage
def create_base_flow():
"""Create the base Flow for processing a single image."""
# Create nodes
load = LoadImage()
filter_node = ApplyFilter()
save = SaveImage()
# Connect nodes
load - "apply_filter" >> filter_node
filter_node - "save" >> save
# Create and return flow
return Flow(start=load)
class ImageBatchFlow(BatchFlow):
"""BatchFlow for processing multiple images with different filters."""
def prep(self, shared):
"""Generate parameters for each image-filter combination."""
# List of images to process
images = ["cat.jpg", "dog.jpg", "bird.jpg"]
# List of filters to apply
filters = ["grayscale", "blur", "sepia"]
# Generate all combinations
params = []
for img in images:
for f in filters:
params.append({
"input": img,
"filter": f
})
return params
def create_flow():
"""Create the complete batch processing flow."""
# Create base flow for single image processing
base_flow = create_base_flow()
# Wrap in BatchFlow for multiple images
batch_flow = ImageBatchFlow(start=base_flow)
return batch_flow
================================================
FILE: cookbook/pocketflow-batch-flow/main.py
================================================
import os
from PIL import Image
import numpy as np
from flow import create_flow
def main():
# Create and run flow
print("Processing images with filters...")
flow = create_flow()
flow.run({})
print("\nAll images processed successfully!")
print("Check the 'output' directory for results.")
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-batch-flow/nodes.py
================================================
"""Node implementations for image processing."""
import os
from PIL import Image, ImageEnhance, ImageFilter
from pocketflow import Node
class LoadImage(Node):
"""Node that loads an image file."""
def prep(self, shared):
"""Get image path from parameters."""
return os.path.join("images", self.params["input"])
def exec(self, image_path):
"""Load the image using PIL."""
return Image.open(image_path)
def post(self, shared, prep_res, exec_res):
"""Store the image in shared store."""
shared["image"] = exec_res
return "apply_filter"
class ApplyFilter(Node):
"""Node that applies a filter to an image."""
def prep(self, shared):
"""Get image and filter type."""
return shared["image"], self.params["filter"]
def exec(self, inputs):
"""Apply the specified filter."""
image, filter_type = inputs
if filter_type == "grayscale":
return image.convert("L")
elif filter_type == "blur":
return image.filter(ImageFilter.BLUR)
elif filter_type == "sepia":
# Sepia implementation
enhancer = ImageEnhance.Color(image)
grayscale = enhancer.enhance(0.3)
colorize = ImageEnhance.Brightness(grayscale)
return colorize.enhance(1.2)
else:
raise ValueError(f"Unknown filter: {filter_type}")
def post(self, shared, prep_res, exec_res):
"""Store the filtered image."""
shared["filtered_image"] = exec_res
return "save"
class SaveImage(Node):
"""Node that saves the processed image."""
def prep(self, shared):
"""Get filtered image and prepare output path."""
# Create output directory if it doesn't exist
os.makedirs("output", exist_ok=True)
# Generate output filename
input_name = os.path.splitext(self.params["input"])[0]
filter_name = self.params["filter"]
output_path = os.path.join("output", f"{input_name}_{filter_name}.jpg")
return shared["filtered_image"], output_path
def exec(self, inputs):
"""Save the image to file."""
image, output_path = inputs
image.save(output_path, "JPEG")
return output_path
def post(self, shared, prep_res, exec_res):
"""Print success message."""
print(f"Saved filtered image to: {exec_res}")
return "default"
================================================
FILE: cookbook/pocketflow-batch-flow/requirements.txt
================================================
pocketflow
Pillow>=10.0.0
================================================
FILE: cookbook/pocketflow-batch-node/README.md
================================================
# PocketFlow BatchNode Example
This example demonstrates the BatchNode concept in PocketFlow by implementing a CSV processor that handles large files by processing them in chunks.
## What this Example Demonstrates
- How to use BatchNode to process large inputs in chunks
- The three key methods of BatchNode:
1. `prep`: Splits input into chunks
2. `exec`: Processes each chunk independently
3. `post`: Combines results from all chunks
## Project Structure
```
pocketflow-batch-node/
├── README.md
├── requirements.txt
├── data/
│ └── sales.csv # Sample large CSV file
├── main.py # Entry point
├── flow.py # Flow definition
└── nodes.py # BatchNode implementation
```
## How it Works
The example processes a large CSV file containing sales data:
1. **Chunking (prep)**: The CSV file is read and split into chunks of N rows
2. **Processing (exec)**: Each chunk is processed to calculate:
- Total sales
- Average sale value
- Number of transactions
3. **Combining (post)**: Results from all chunks are aggregated into final statistics
## Installation
```bash
pip install -r requirements.txt
```
## Usage
```bash
python main.py
```
## Sample Output
```
Processing sales.csv in chunks...
Final Statistics:
- Total Sales: $1,234,567.89
- Average Sale: $123.45
- Total Transactions: 10,000
```
## Key Concepts Illustrated
1. **Chunk-based Processing**: Shows how BatchNode handles large inputs by breaking them into manageable pieces
2. **Independent Processing**: Demonstrates how each chunk is processed separately
3. **Result Aggregation**: Shows how individual results are combined into a final output
================================================
FILE: cookbook/pocketflow-batch-node/data/sales.csv
================================================
date,amount,product
2024-01-01,114.9,B
2024-01-02,95.85,A
2024-01-03,119.43,C
2024-01-04,145.69,A
2024-01-05,92.98,B
2024-01-06,92.98,C
2024-01-07,147.38,B
2024-01-08,123.02,A
2024-01-09,85.92,A
2024-01-10,116.28,B
2024-01-11,86.1,B
2024-01-12,86.03,C
2024-01-13,107.26,B
2024-01-14,42.6,B
2024-01-15,48.25,A
2024-01-16,83.13,A
2024-01-17,69.62,B
2024-01-18,109.43,B
2024-01-19,72.76,A
2024-01-20,57.63,C
2024-01-21,143.97,C
2024-01-22,93.23,C
2024-01-23,102.03,C
2024-01-24,57.26,A
2024-01-25,83.67,B
2024-01-26,103.33,C
2024-01-27,65.47,A
2024-01-28,111.27,B
2024-01-29,81.98,C
2024-01-30,91.25,B
2024-01-31,81.95,C
2024-02-01,155.57,B
2024-02-02,99.6,C
2024-02-03,68.27,C
2024-02-04,124.68,B
2024-02-05,63.37,B
2024-02-06,106.27,B
2024-02-07,41.21,A
2024-02-08,60.15,A
2024-02-09,105.91,B
2024-02-10,122.15,A
2024-02-11,105.14,C
2024-02-12,96.53,B
2024-02-13,90.97,A
2024-02-14,55.64,B
2024-02-15,78.4,C
2024-02-16,86.18,C
2024-02-17,131.71,A
2024-02-18,110.31,C
2024-02-19,47.11,B
2024-02-20,109.72,A
2024-02-21,88.45,B
2024-02-22,79.69,B
2024-02-23,118.35,B
2024-02-24,130.93,C
2024-02-25,127.94,B
2024-02-26,74.82,C
2024-02-27,90.72,B
2024-02-28,109.94,C
2024-02-29,129.27,C
2024-03-01,85.62,C
2024-03-02,94.43,B
2024-03-03,66.81,B
2024-03-04,64.11,C
2024-03-05,124.38,B
2024-03-06,140.69,C
2024-03-07,97.84,B
2024-03-08,130.11,C
2024-03-09,110.85,C
2024-03-10,80.65,A
2024-03-11,110.84,B
2024-03-12,146.14,B
2024-03-13,98.93,A
2024-03-14,146.94,C
2024-03-15,21.41,C
2024-03-16,124.66,A
2024-03-17,102.61,A
2024-03-18,91.03,C
2024-03-19,102.75,B
2024-03-20,40.37,A
2024-03-21,93.41,A
2024-03-22,110.71,A
2024-03-23,144.34,A
2024-03-24,84.45,A
2024-03-25,75.75,A
2024-03-26,84.95,C
2024-03-27,127.46,C
2024-03-28,109.86,A
2024-03-29,84.11,C
2024-03-30,115.4,B
2024-03-31,102.91,B
2024-04-01,129.06,A
2024-04-02,78.94,B
2024-04-03,90.17,B
2024-04-04,88.24,C
2024-04-05,56.09,C
2024-04-06,108.88,C
2024-04-07,107.83,C
2024-04-08,100.15,B
2024-04-09,92.96,A
2024-04-10,57.54,C
2024-04-11,87.38,B
2024-04-12,89.72,C
2024-04-13,75.93,B
2024-04-14,95.16,C
2024-04-15,112.12,A
2024-04-16,156.59,C
2024-04-17,105.24,A
2024-04-18,107.73,C
2024-04-19,97.77,C
2024-04-20,42.44,C
2024-04-21,99.2,B
2024-04-22,101.81,B
2024-04-23,173.9,A
2024-04-24,94.23,A
2024-04-25,109.05,B
2024-04-26,98.96,A
2024-04-27,64.94,A
2024-04-28,134.28,B
2024-04-29,122.56,B
2024-04-30,123.73,B
2024-05-01,72.72,B
2024-05-02,142.08,C
2024-05-03,57.94,B
2024-05-04,117.61,A
2024-05-05,165.71,C
2024-05-06,70.28,C
2024-05-07,83.01,A
2024-05-08,102.99,C
2024-05-09,84.9,A
2024-05-10,53.48,C
2024-05-11,102.06,C
2024-05-12,68.13,A
2024-05-13,114.21,A
2024-05-14,72.42,A
2024-05-15,146.5,A
2024-05-16,76.5,B
2024-05-17,90.34,B
2024-05-18,124.41,A
2024-05-19,63.07,B
2024-05-20,106.82,A
2024-05-21,139.21,A
2024-05-22,51.78,C
2024-05-23,105.54,A
2024-05-24,107.8,A
2024-05-25,123.45,A
2024-05-26,62.89,A
2024-05-27,60.39,C
2024-05-28,115.66,A
2024-05-29,108.91,C
2024-05-30,107.51,A
2024-05-31,110.39,C
2024-06-01,79.6,B
2024-06-02,106.97,C
2024-06-03,108.79,C
2024-06-04,78.57,A
2024-06-05,155.97,C
2024-06-06,114.21,C
2024-06-07,64.26,A
2024-06-08,119.7,B
2024-06-09,70.76,B
2024-06-10,123.61,B
2024-06-11,134.76,C
2024-06-12,75.38,A
2024-06-13,128.9,B
2024-06-14,112.38,B
2024-06-15,124.66,B
2024-06-16,156.9,B
2024-06-17,92.64,A
2024-06-18,77.39,B
2024-06-19,73.31,C
2024-06-20,75.53,C
2024-06-21,97.69,A
2024-06-22,110.23,B
2024-06-23,108.3,B
2024-06-24,124.82,C
2024-06-25,100.39,A
2024-06-26,143.61,C
2024-06-27,92.06,A
2024-06-28,181.61,A
2024-06-29,118.77,A
2024-06-30,74.29,C
2024-07-01,67.87,A
2024-07-02,114.47,C
2024-07-03,93.3,C
2024-07-04,121.42,B
2024-07-05,114.2,C
2024-07-06,97.82,C
2024-07-07,74.6,B
2024-07-08,54.55,B
2024-07-09,86.6,B
2024-07-10,125.69,A
2024-07-11,106.42,A
2024-07-12,62.63,B
2024-07-13,105.2,A
2024-07-14,111.56,B
2024-07-15,73.48,A
2024-07-16,104.61,C
2024-07-17,101.75,A
2024-07-18,65.71,A
2024-07-19,110.73,C
2024-07-20,116.82,B
2024-07-21,132.49,A
2024-07-22,131.61,A
2024-07-23,58.67,C
2024-07-24,71.87,C
2024-07-25,115.45,C
2024-07-26,115.41,C
2024-07-27,115.45,B
2024-07-28,215.58,A
2024-07-29,117.13,A
2024-07-30,134.07,A
2024-07-31,128.62,A
2024-08-01,119.54,B
2024-08-02,90.54,A
2024-08-03,122.77,B
2024-08-04,76.82,B
2024-08-05,92.9,C
2024-08-06,85.44,C
2024-08-07,102.46,A
2024-08-08,169.44,B
2024-08-09,43.98,C
2024-08-10,120.59,B
2024-08-11,51.62,C
2024-08-12,85.84,B
2024-08-13,132.67,A
2024-08-14,101.93,A
2024-08-15,67.67,B
2024-08-16,78.54,C
2024-08-17,120.39,C
2024-08-18,78.09,A
2024-08-19,106.49,A
2024-08-20,101.37,A
2024-08-21,80.45,C
2024-08-22,164.32,C
2024-08-23,119.02,A
2024-08-24,39.25,B
2024-08-25,105.59,C
2024-08-26,80.15,A
2024-08-27,125.57,B
2024-08-28,76.22,C
2024-08-29,96.56,B
2024-08-30,115.15,B
2024-08-31,125.97,C
2024-09-01,63.99,A
2024-09-02,89.96,B
2024-09-03,85.75,A
2024-09-04,80.4,C
2024-09-05,152.96,B
2024-09-06,112.15,B
2024-09-07,62.17,B
2024-09-08,127.54,B
2024-09-09,163.66,B
2024-09-10,130.97,C
2024-09-11,54.42,B
2024-09-12,85.47,C
2024-09-13,138.01,A
2024-09-14,78.77,C
2024-09-15,113.31,B
2024-09-16,123.24,A
2024-09-17,72.19,C
2024-09-18,98.21,C
2024-09-19,2.76,C
2024-09-20,69.27,B
2024-09-21,92.42,B
2024-09-22,62.57,C
2024-09-23,148.97,C
2024-09-24,57.1,A
2024-09-25,86.8,B
2024-09-26,103.92,B
2024-09-27,143.24,A
2024-09-28,56.92,C
2024-09-29,134.89,C
2024-09-30,100.31,A
2024-10-01,70.55,C
2024-10-02,113.86,C
2024-10-03,105.97,A
2024-10-04,81.99,A
2024-10-05,102.09,A
2024-10-06,88.44,C
2024-10-07,103.41,A
2024-10-08,119.86,C
2024-10-09,147.58,B
2024-10-10,62.87,B
2024-10-11,163.99,A
2024-10-12,41.44,C
2024-10-13,95.45,C
2024-10-14,117.65,C
2024-10-15,108.43,A
2024-10-16,81.32,C
2024-10-17,93.76,C
2024-10-18,85.21,C
2024-10-19,82.32,B
2024-10-20,125.49,C
2024-10-21,110.71,B
2024-10-22,79.21,C
2024-10-23,126.99,C
2024-10-24,109.22,C
2024-10-25,124.39,B
2024-10-26,118.89,C
2024-10-27,75.13,A
2024-10-28,83.19,C
2024-10-29,122.42,B
2024-10-30,118.31,B
2024-10-31,99.37,A
2024-11-01,103.52,A
2024-11-02,138.33,A
2024-11-03,82.25,B
2024-11-04,116.41,C
2024-11-05,93.93,C
2024-11-06,93.47,A
2024-11-07,132.96,A
2024-11-08,124.76,C
2024-11-09,124.41,A
2024-11-10,139.16,C
2024-11-11,100.63,C
2024-11-12,120.46,C
2024-11-13,90.69,C
2024-11-14,109.72,B
2024-11-15,96.1,C
2024-11-16,102.91,A
2024-11-17,117.85,A
2024-11-18,75.45,A
2024-11-19,162.77,A
2024-11-20,69.82,B
2024-11-21,63.57,C
2024-11-22,134.74,C
2024-11-23,123.75,A
2024-11-24,118.72,A
2024-11-25,118.85,A
2024-11-26,99.63,C
2024-11-27,73.08,B
2024-11-28,102.27,B
2024-11-29,79.69,B
2024-11-30,129.25,B
2024-12-01,95.59,A
2024-12-02,75.24,C
2024-12-03,90.36,A
2024-12-04,112.39,B
2024-12-05,83.09,A
2024-12-06,75.33,B
2024-12-07,107.31,C
2024-12-08,107.35,B
2024-12-09,84.79,A
2024-12-10,85.87,C
2024-12-11,106.96,B
2024-12-12,56.56,B
2024-12-13,57.78,C
2024-12-14,78.45,A
2024-12-15,93.6,C
2024-12-16,109.33,A
2024-12-17,144.26,B
2024-12-18,125.73,B
2024-12-19,95.2,C
2024-12-20,99.43,B
2024-12-21,69.92,C
2024-12-22,99.44,B
2024-12-23,91.34,B
2024-12-24,109.68,A
2024-12-25,75.18,C
2024-12-26,115.58,C
2024-12-27,145.98,B
2024-12-28,96.74,A
2024-12-29,112.05,A
2024-12-30,120.7,C
2024-12-31,87.96,C
2025-01-01,106.72,B
2025-01-02,100.38,C
2025-01-03,102.93,B
2025-01-04,76.81,A
2025-01-05,100.74,C
2025-01-06,114.94,A
2025-01-07,143.53,A
2025-01-08,128.78,B
2025-01-09,164.6,A
2025-01-10,76.98,B
2025-01-11,126.17,B
2025-01-12,105.5,A
2025-01-13,165.69,A
2025-01-14,75.75,B
2025-01-15,74.81,B
2025-01-16,82.02,A
2025-01-17,36.28,B
2025-01-18,84.23,B
2025-01-19,77.23,C
2025-01-20,104.51,C
2025-01-21,110.25,C
2025-01-22,156.29,C
2025-01-23,128.51,C
2025-01-24,82.69,B
2025-01-25,73.05,B
2025-01-26,114.76,C
2025-01-27,60.39,A
2025-01-28,154.94,C
2025-01-29,135.38,A
2025-01-30,85.92,B
2025-01-31,48.61,A
2025-02-01,140.62,B
2025-02-02,96.56,A
2025-02-03,137.13,B
2025-02-04,52.17,B
2025-02-05,82.02,C
2025-02-06,100.16,B
2025-02-07,101.41,C
2025-02-08,86.5,C
2025-02-09,118.69,C
2025-02-10,67.97,A
2025-02-11,95.73,A
2025-02-12,103.61,B
2025-02-13,115.43,C
2025-02-14,121.35,C
2025-02-15,66.26,B
2025-02-16,53.98,C
2025-02-17,138.33,B
2025-02-18,109.97,C
2025-02-19,77.55,A
2025-02-20,146.53,B
2025-02-21,103.47,B
2025-02-22,135.38,A
2025-02-23,102.03,B
2025-02-24,161.82,A
2025-02-25,152.66,C
2025-02-26,92.53,A
2025-02-27,129.15,A
2025-02-28,119.36,B
2025-03-01,141.06,C
2025-03-02,71.05,A
2025-03-03,120.58,C
2025-03-04,131.75,A
2025-03-05,47.24,B
2025-03-06,64.5,A
2025-03-07,38.82,A
2025-03-08,91.92,C
2025-03-09,121.53,A
2025-03-10,145.07,B
2025-03-11,102.22,B
2025-03-12,148.86,A
2025-03-13,58.6,A
2025-03-14,48.9,C
2025-03-15,98.33,C
2025-03-16,111.52,B
2025-03-17,99.02,C
2025-03-18,37.98,A
2025-03-19,97.33,B
2025-03-20,60.87,C
2025-03-21,120.09,B
2025-03-22,111.0,A
2025-03-23,71.8,C
2025-03-24,84.58,A
2025-03-25,68.22,A
2025-03-26,98.12,B
2025-03-27,128.65,B
2025-03-28,70.43,B
2025-03-29,115.12,C
2025-03-30,84.09,C
2025-03-31,76.21,B
2025-04-01,96.79,C
2025-04-02,68.94,C
2025-04-03,83.39,B
2025-04-04,64.06,A
2025-04-05,158.94,C
2025-04-06,101.06,C
2025-04-07,79.01,A
2025-04-08,106.42,A
2025-04-09,96.63,A
2025-04-10,93.37,B
2025-04-11,118.43,A
2025-04-12,122.73,B
2025-04-13,84.08,A
2025-04-14,82.73,A
2025-04-15,91.75,B
2025-04-16,30.94,B
2025-04-17,54.54,A
2025-04-18,141.01,A
2025-04-19,149.35,A
2025-04-20,92.53,C
2025-04-21,117.3,A
2025-04-22,109.34,B
2025-04-23,192.37,C
2025-04-24,133.59,B
2025-04-25,96.16,C
2025-04-26,71.33,C
2025-04-27,51.81,B
2025-04-28,106.1,C
2025-04-29,77.31,B
2025-04-30,57.33,A
2025-05-01,80.6,C
2025-05-02,67.55,B
2025-05-03,150.61,C
2025-05-04,126.45,B
2025-05-05,99.76,A
2025-05-06,144.4,A
2025-05-07,102.32,C
2025-05-08,74.16,C
2025-05-09,145.69,B
2025-05-10,116.17,B
2025-05-11,68.88,C
2025-05-12,94.29,B
2025-05-13,73.73,B
2025-05-14,58.52,B
2025-05-15,127.79,C
2025-05-16,157.28,A
2025-05-17,58.04,A
2025-05-18,116.89,B
2025-05-19,80.48,C
2025-05-20,85.39,B
2025-05-21,82.23,A
2025-05-22,74.08,B
2025-05-23,101.46,A
2025-05-24,75.07,C
2025-05-25,108.11,B
2025-05-26,98.49,C
2025-05-27,92.83,B
2025-05-28,72.77,B
2025-05-29,82.7,B
2025-05-30,122.66,B
2025-05-31,115.03,B
2025-06-01,70.67,C
2025-06-02,102.98,C
2025-06-03,122.54,A
2025-06-04,49.92,B
2025-06-05,116.3,A
2025-06-06,80.12,C
2025-06-07,117.12,A
2025-06-08,77.1,C
2025-06-09,45.85,B
2025-06-10,51.17,A
2025-06-11,101.44,A
2025-06-12,107.79,A
2025-06-13,72.87,A
2025-06-14,119.16,B
2025-06-15,50.15,C
2025-06-16,98.02,C
2025-06-17,63.67,B
2025-06-18,80.44,C
2025-06-19,101.42,A
2025-06-20,74.19,B
2025-06-21,88.46,C
2025-06-22,130.19,B
2025-06-23,82.69,B
2025-06-24,125.07,C
2025-06-25,66.11,C
2025-06-26,115.89,B
2025-06-27,143.25,B
2025-06-28,25.85,C
2025-06-29,76.09,B
2025-06-30,117.31,C
2025-07-01,93.91,B
2025-07-02,111.13,B
2025-07-03,81.88,B
2025-07-04,102.6,C
2025-07-05,95.33,A
2025-07-06,135.03,C
2025-07-07,107.63,B
2025-07-08,110.13,A
2025-07-09,87.64,A
2025-07-10,85.37,B
2025-07-11,87.02,C
2025-07-12,111.83,A
2025-07-13,87.37,A
2025-07-14,108.69,C
2025-07-15,162.26,C
2025-07-16,126.13,A
2025-07-17,90.22,C
2025-07-18,136.04,A
2025-07-19,87.76,B
2025-07-20,38.86,C
2025-07-21,69.76,A
2025-07-22,43.88,C
2025-07-23,89.45,C
2025-07-24,100.55,A
2025-07-25,150.29,B
2025-07-26,109.81,A
2025-07-27,93.43,C
2025-07-28,124.88,A
2025-07-29,33.67,C
2025-07-30,107.07,A
2025-07-31,123.13,B
2025-08-01,55.64,B
2025-08-02,134.31,B
2025-08-03,110.15,B
2025-08-04,87.54,A
2025-08-05,118.98,B
2025-08-06,168.12,A
2025-08-07,105.46,C
2025-08-08,107.45,C
2025-08-09,86.22,C
2025-08-10,74.5,B
2025-08-11,124.91,B
2025-08-12,74.32,B
2025-08-13,102.15,C
2025-08-14,85.67,C
2025-08-15,114.37,C
2025-08-16,110.01,C
2025-08-17,131.13,A
2025-08-18,84.7,A
2025-08-19,91.9,B
2025-08-20,70.64,B
2025-08-21,86.67,C
2025-08-22,111.32,A
2025-08-23,122.71,A
2025-08-24,72.34,B
2025-08-25,126.09,A
2025-08-26,140.67,B
2025-08-27,112.4,C
2025-08-28,156.3,C
2025-08-29,76.79,B
2025-08-30,62.66,C
2025-08-31,46.64,C
2025-09-01,144.88,A
2025-09-02,119.63,A
2025-09-03,98.33,A
2025-09-04,108.4,A
2025-09-05,66.24,A
2025-09-06,173.37,B
2025-09-07,103.88,B
2025-09-08,103.28,A
2025-09-09,121.77,C
2025-09-10,114.43,C
2025-09-11,106.72,B
2025-09-12,76.29,B
2025-09-13,114.14,B
2025-09-14,156.46,C
2025-09-15,140.36,B
2025-09-16,147.8,C
2025-09-17,84.66,B
2025-09-18,70.31,C
2025-09-19,96.23,B
2025-09-20,101.67,B
2025-09-21,132.83,C
2025-09-22,49.23,B
2025-09-23,145.89,A
2025-09-24,95.26,C
2025-09-25,87.19,B
2025-09-26,69.64,C
2025-09-27,50.35,B
2025-09-28,124.7,A
2025-09-29,102.2,B
2025-09-30,61.3,B
2025-10-01,61.15,A
2025-10-02,89.93,C
2025-10-03,150.07,B
2025-10-04,92.21,B
2025-10-05,54.91,A
2025-10-06,92.63,C
2025-10-07,91.82,C
2025-10-08,19.09,A
2025-10-09,98.37,A
2025-10-10,93.07,B
2025-10-11,120.89,A
2025-10-12,155.47,A
2025-10-13,133.8,A
2025-10-14,91.93,C
2025-10-15,66.8,A
2025-10-16,177.2,B
2025-10-17,101.78,B
2025-10-18,100.42,B
2025-10-19,99.28,A
2025-10-20,105.94,C
2025-10-21,95.67,A
2025-10-22,82.79,A
2025-10-23,83.59,B
2025-10-24,99.02,A
2025-10-25,83.7,C
2025-10-26,78.61,B
2025-10-27,103.19,C
2025-10-28,92.35,C
2025-10-29,145.12,C
2025-10-30,20.47,A
2025-10-31,132.75,A
2025-11-01,137.38,A
2025-11-02,37.8,A
2025-11-03,89.72,B
2025-11-04,88.86,A
2025-11-05,57.77,B
2025-11-06,76.67,C
2025-11-07,66.68,B
2025-11-08,152.57,A
2025-11-09,128.07,B
2025-11-10,138.15,A
2025-11-11,121.65,A
2025-11-12,66.13,B
2025-11-13,84.26,B
2025-11-14,114.68,C
2025-11-15,63.34,C
2025-11-16,121.39,C
2025-11-17,92.79,C
2025-11-18,88.76,C
2025-11-19,121.33,B
2025-11-20,113.33,C
2025-11-21,89.17,B
2025-11-22,134.78,C
2025-11-23,67.57,A
2025-11-24,118.48,C
2025-11-25,117.79,A
2025-11-26,90.71,B
2025-11-27,109.78,C
2025-11-28,62.47,B
2025-11-29,127.72,C
2025-11-30,94.45,B
2025-12-01,84.32,C
2025-12-02,131.47,C
2025-12-03,78.87,A
2025-12-04,57.75,B
2025-12-05,53.3,B
2025-12-06,118.18,B
2025-12-07,61.59,B
2025-12-08,152.64,B
2025-12-09,37.54,A
2025-12-10,150.89,C
2025-12-11,106.33,A
2025-12-12,97.1,B
2025-12-13,83.65,B
2025-12-14,111.97,C
2025-12-15,98.87,A
2025-12-16,133.1,C
2025-12-17,103.43,C
2025-12-18,104.51,A
2025-12-19,89.09,C
2025-12-20,98.29,A
2025-12-21,109.23,A
2025-12-22,48.69,B
2025-12-23,59.55,C
2025-12-24,122.3,B
2025-12-25,105.13,C
2025-12-26,94.48,C
2025-12-27,100.55,B
2025-12-28,110.43,B
2025-12-29,83.81,B
2025-12-30,76.65,A
2025-12-31,105.88,C
2026-01-01,70.65,A
2026-01-02,112.25,C
2026-01-03,48.92,C
2026-01-04,130.87,A
2026-01-05,114.18,A
2026-01-06,107.68,C
2026-01-07,129.48,B
2026-01-08,149.96,B
2026-01-09,130.43,B
2026-01-10,44.77,B
2026-01-11,61.61,C
2026-01-12,81.26,B
2026-01-13,100.78,B
2026-01-14,115.53,B
2026-01-15,78.23,C
2026-01-16,105.6,A
2026-01-17,77.34,B
2026-01-18,81.65,C
2026-01-19,57.8,C
2026-01-20,72.3,B
2026-01-21,59.45,A
2026-01-22,70.72,A
2026-01-23,131.61,A
2026-01-24,71.52,C
2026-01-25,178.97,B
2026-01-26,114.8,B
2026-01-27,105.55,C
2026-01-28,74.25,B
2026-01-29,121.01,B
2026-01-30,82.73,A
2026-01-31,103.66,B
2026-02-01,176.8,B
2026-02-02,97.12,C
2026-02-03,134.48,C
2026-02-04,78.9,A
2026-02-05,98.95,C
2026-02-06,153.12,A
2026-02-07,81.19,A
2026-02-08,154.37,B
2026-02-09,121.23,A
2026-02-10,83.13,B
2026-02-11,118.97,A
2026-02-12,129.18,C
2026-02-13,118.65,C
2026-02-14,52.89,C
2026-02-15,78.19,A
2026-02-16,92.57,A
2026-02-17,97.77,B
2026-02-18,118.62,B
2026-02-19,105.33,B
2026-02-20,59.94,B
2026-02-21,111.41,A
2026-02-22,118.32,A
2026-02-23,116.79,B
2026-02-24,132.42,A
2026-02-25,125.02,A
2026-02-26,113.78,A
2026-02-27,97.9,A
2026-02-28,50.17,B
2026-03-01,112.89,B
2026-03-02,106.23,A
2026-03-03,108.15,C
2026-03-04,61.7,B
2026-03-05,67.57,B
2026-03-06,131.59,C
2026-03-07,98.81,B
2026-03-08,120.45,C
2026-03-09,100.85,C
2026-03-10,100.89,A
2026-03-11,128.15,B
2026-03-12,84.52,C
2026-03-13,102.88,A
2026-03-14,86.13,B
2026-03-15,86.97,A
2026-03-16,90.72,A
2026-03-17,106.66,C
2026-03-18,85.64,B
2026-03-19,137.67,A
2026-03-20,73.16,C
2026-03-21,94.39,C
2026-03-22,86.81,A
2026-03-23,143.41,A
2026-03-24,105.9,C
2026-03-25,130.96,A
2026-03-26,55.43,A
2026-03-27,108.01,B
2026-03-28,126.69,C
2026-03-29,102.47,A
2026-03-30,131.96,C
2026-03-31,84.48,B
2026-04-01,142.28,C
2026-04-02,168.97,C
2026-04-03,89.11,C
2026-04-04,86.63,B
2026-04-05,143.6,B
2026-04-06,147.39,C
2026-04-07,84.31,C
2026-04-08,87.39,B
2026-04-09,91.55,A
2026-04-10,59.67,B
2026-04-11,72.44,C
2026-04-12,69.88,A
2026-04-13,76.97,A
2026-04-14,98.96,A
2026-04-15,107.03,B
2026-04-16,146.52,B
2026-04-17,70.05,B
2026-04-18,129.53,B
2026-04-19,93.58,C
2026-04-20,98.52,A
2026-04-21,120.24,C
2026-04-22,66.32,B
2026-04-23,111.47,B
2026-04-24,104.99,C
2026-04-25,114.77,A
2026-04-26,108.68,A
2026-04-27,173.66,B
2026-04-28,80.87,B
2026-04-29,84.07,B
2026-04-30,81.31,B
2026-05-01,83.34,A
2026-05-02,80.88,C
2026-05-03,135.67,A
2026-05-04,142.62,C
2026-05-05,82.88,B
2026-05-06,75.03,B
2026-05-07,114.14,C
2026-05-08,83.43,B
2026-05-09,118.99,A
2026-05-10,106.09,B
2026-05-11,54.53,B
2026-05-12,146.43,B
2026-05-13,153.88,B
2026-05-14,81.62,A
2026-05-15,88.37,C
2026-05-16,108.58,B
2026-05-17,110.03,B
2026-05-18,119.76,A
2026-05-19,160.31,A
2026-05-20,94.69,A
2026-05-21,76.05,B
2026-05-22,58.62,C
2026-05-23,78.07,A
2026-05-24,99.01,A
2026-05-25,153.84,A
2026-05-26,84.47,C
2026-05-27,106.71,B
2026-05-28,99.51,B
2026-05-29,135.65,B
2026-05-30,175.81,A
2026-05-31,84.07,B
2026-06-01,85.32,A
2026-06-02,131.32,C
2026-06-03,120.46,C
2026-06-04,155.4,B
2026-06-05,117.52,A
2026-06-06,89.22,A
2026-06-07,117.72,B
2026-06-08,133.26,C
2026-06-09,124.61,C
2026-06-10,115.22,C
2026-06-11,132.0,C
2026-06-12,135.08,C
2026-06-13,141.46,B
2026-06-14,119.46,B
2026-06-15,94.99,B
2026-06-16,104.4,C
2026-06-17,136.2,C
2026-06-18,75.49,B
2026-06-19,111.06,B
2026-06-20,88.2,C
2026-06-21,100.86,A
2026-06-22,138.35,B
2026-06-23,105.73,C
2026-06-24,101.39,C
2026-06-25,59.2,B
2026-06-26,122.39,A
2026-06-27,119.36,C
2026-06-28,164.9,B
2026-06-29,90.77,C
2026-06-30,106.57,C
2026-07-01,107.48,C
2026-07-02,147.32,C
2026-07-03,97.14,A
2026-07-04,108.37,B
2026-07-05,118.24,B
2026-07-06,105.6,B
2026-07-07,86.61,B
2026-07-08,105.82,B
2026-07-09,132.21,A
2026-07-10,69.2,C
2026-07-11,103.99,C
2026-07-12,79.0,B
2026-07-13,135.85,C
2026-07-14,54.3,C
2026-07-15,83.23,C
2026-07-16,111.32,B
2026-07-17,146.97,B
2026-07-18,98.03,A
2026-07-19,83.34,B
2026-07-20,156.43,A
2026-07-21,56.56,B
2026-07-22,34.04,C
2026-07-23,113.2,A
2026-07-24,84.94,A
2026-07-25,69.36,A
2026-07-26,121.25,B
2026-07-27,107.31,A
2026-07-28,83.08,B
2026-07-29,61.59,A
2026-07-30,126.17,B
2026-07-31,119.51,A
2026-08-01,97.02,C
2026-08-02,155.4,C
2026-08-03,67.9,A
2026-08-04,54.23,B
2026-08-05,79.24,B
2026-08-06,98.63,B
2026-08-07,107.3,C
2026-08-08,92.76,C
2026-08-09,110.56,C
2026-08-10,62.45,B
2026-08-11,143.31,B
2026-08-12,97.54,B
2026-08-13,133.52,A
2026-08-14,110.28,A
2026-08-15,113.7,A
2026-08-16,117.09,C
2026-08-17,113.43,C
2026-08-18,119.28,B
2026-08-19,139.87,A
2026-08-20,105.9,B
2026-08-21,121.27,A
2026-08-22,97.31,A
2026-08-23,143.2,B
2026-08-24,79.71,B
2026-08-25,154.03,A
2026-08-26,98.8,A
2026-08-27,57.08,C
2026-08-28,103.84,B
2026-08-29,79.57,B
2026-08-30,125.22,B
2026-08-31,80.42,B
2026-09-01,86.61,A
2026-09-02,43.31,C
2026-09-03,86.43,B
2026-09-04,27.28,A
2026-09-05,52.48,A
2026-09-06,122.81,A
2026-09-07,123.57,A
2026-09-08,112.76,A
2026-09-09,70.99,B
2026-09-10,98.57,B
2026-09-11,99.89,C
2026-09-12,65.25,B
2026-09-13,145.1,A
2026-09-14,126.32,C
2026-09-15,93.37,A
2026-09-16,100.81,A
2026-09-17,106.25,B
2026-09-18,38.75,A
2026-09-19,92.58,B
2026-09-20,79.54,A
2026-09-21,69.95,C
2026-09-22,91.57,A
2026-09-23,153.93,B
2026-09-24,119.23,A
2026-09-25,82.86,B
2026-09-26,117.18,B
2026-09-27,141.98,C
2026-09-28,127.74,B
2026-09-29,101.79,C
2026-09-30,80.59,C
2026-10-01,120.95,A
2026-10-02,111.8,C
2026-10-03,126.86,B
2026-10-04,119.06,B
2026-10-05,131.49,C
2026-10-06,83.94,A
2026-10-07,139.52,C
2026-10-08,105.93,C
2026-10-09,162.26,B
2026-10-10,79.32,A
2026-10-11,152.08,A
2026-10-12,105.94,A
2026-10-13,80.46,A
2026-10-14,85.48,B
2026-10-15,90.39,A
2026-10-16,112.72,A
2026-10-17,115.69,A
2026-10-18,82.79,B
2026-10-19,99.27,B
2026-10-20,164.27,A
2026-10-21,151.83,B
2026-10-22,113.09,C
2026-10-23,101.14,A
2026-10-24,103.6,B
2026-10-25,118.41,A
2026-10-26,69.32,A
2026-10-27,92.28,B
2026-10-28,49.94,C
2026-10-29,111.98,A
2026-10-30,119.42,C
2026-10-31,85.5,B
2026-11-01,147.22,A
2026-11-02,63.23,B
2026-11-03,56.07,B
2026-11-04,106.73,B
2026-11-05,131.41,B
2026-11-06,150.52,A
2026-11-07,86.23,C
2026-11-08,132.36,C
2026-11-09,98.84,A
2026-11-10,94.82,A
2026-11-11,126.51,C
2026-11-12,119.57,C
2026-11-13,52.71,A
2026-11-14,144.3,B
2026-11-15,141.4,A
2026-11-16,81.23,C
2026-11-17,111.87,B
2026-11-18,114.82,C
2026-11-19,107.82,B
2026-11-20,83.49,C
2026-11-21,79.85,B
2026-11-22,99.23,A
2026-11-23,135.18,A
2026-11-24,116.31,B
2026-11-25,88.88,B
2026-11-26,123.15,A
2026-11-27,14.54,A
2026-11-28,134.46,B
2026-11-29,47.81,B
2026-11-30,89.13,A
2026-12-01,66.41,C
2026-12-02,61.16,A
2026-12-03,134.82,B
2026-12-04,85.97,B
2026-12-05,110.4,C
2026-12-06,98.59,A
2026-12-07,114.31,B
2026-12-08,102.3,A
2026-12-09,61.51,B
2026-12-10,129.89,C
2026-12-11,85.19,A
2026-12-12,53.3,A
2026-12-13,87.16,C
2026-12-14,145.02,B
2026-12-15,125.51,B
2026-12-16,89.54,A
2026-12-17,89.52,C
2026-12-18,90.35,A
2026-12-19,162.3,A
2026-12-20,111.46,B
2026-12-21,112.9,B
2026-12-22,130.91,B
2026-12-23,107.16,B
2026-12-24,92.23,B
2026-12-25,94.11,C
2026-12-26,97.85,A
2026-12-27,98.88,C
2026-12-28,121.83,A
2026-12-29,101.56,B
2026-12-30,121.98,C
2026-12-31,97.58,A
2027-01-01,102.36,B
2027-01-02,40.05,B
2027-01-03,127.49,C
2027-01-04,110.39,C
2027-01-05,129.94,C
2027-01-06,13.11,B
2027-01-07,162.65,A
2027-01-08,95.81,A
2027-01-09,133.25,C
2027-01-10,68.8,A
2027-01-11,118.38,B
2027-01-12,68.4,A
2027-01-13,81.29,B
2027-01-14,157.42,C
2027-01-15,94.28,A
2027-01-16,106.52,B
2027-01-17,126.1,C
2027-01-18,114.87,C
2027-01-19,104.51,C
2027-01-20,110.95,C
2027-01-21,172.1,A
2027-01-22,98.27,B
2027-01-23,106.03,A
2027-01-24,131.52,B
2027-01-25,133.17,C
2027-01-26,135.61,A
2027-01-27,119.16,C
2027-01-28,65.71,B
2027-01-29,149.0,C
2027-01-30,65.61,A
2027-01-31,109.08,A
2027-02-01,77.37,B
2027-02-02,98.08,B
2027-02-03,109.86,A
2027-02-04,109.64,A
2027-02-05,112.66,A
2027-02-06,148.41,A
2027-02-07,113.61,B
2027-02-08,92.68,A
2027-02-09,128.92,B
2027-02-10,135.68,A
2027-02-11,63.17,A
2027-02-12,117.92,C
2027-02-13,121.04,C
2027-02-14,91.07,A
2027-02-15,141.27,B
2027-02-16,95.5,A
2027-02-17,103.77,C
2027-02-18,94.81,A
2027-02-19,100.47,C
2027-02-20,67.11,A
2027-02-21,56.8,A
2027-02-22,147.84,B
2027-02-23,74.59,C
2027-02-24,70.26,C
2027-02-25,35.4,C
2027-02-26,80.83,B
2027-02-27,60.31,A
2027-02-28,149.26,B
2027-03-01,130.29,A
2027-03-02,79.36,B
2027-03-03,167.57,B
2027-03-04,129.45,C
2027-03-05,90.26,B
2027-03-06,25.02,A
2027-03-07,168.73,B
2027-03-08,58.31,B
2027-03-09,50.64,A
2027-03-10,130.68,C
2027-03-11,173.19,A
2027-03-12,141.53,A
2027-03-13,116.92,A
2027-03-14,117.84,B
2027-03-15,125.6,B
2027-03-16,122.77,A
2027-03-17,108.44,A
2027-03-18,103.13,C
2027-03-19,98.12,B
2027-03-20,77.38,B
2027-03-21,91.58,B
2027-03-22,49.21,B
2027-03-23,97.05,A
2027-03-24,70.34,A
2027-03-25,66.89,C
2027-03-26,105.4,B
2027-03-27,141.76,B
2027-03-28,127.55,A
2027-03-29,52.88,C
2027-03-30,70.31,A
2027-03-31,128.22,B
2027-04-01,70.53,B
2027-04-02,93.26,C
2027-04-03,116.5,A
2027-04-04,70.95,B
2027-04-05,103.16,B
2027-04-06,59.98,C
2027-04-07,81.96,B
2027-04-08,109.59,B
2027-04-09,52.21,A
2027-04-10,113.21,C
2027-04-11,99.41,C
2027-04-12,116.57,C
2027-04-13,106.72,A
2027-04-14,140.92,C
2027-04-15,103.76,B
2027-04-16,87.12,C
2027-04-17,103.67,B
2027-04-18,116.3,C
2027-04-19,101.47,C
2027-04-20,101.22,A
2027-04-21,78.94,C
2027-04-22,80.11,C
2027-04-23,57.92,A
2027-04-24,152.49,B
2027-04-25,62.68,C
2027-04-26,79.21,C
2027-04-27,78.45,B
2027-04-28,126.85,A
2027-04-29,91.15,C
2027-04-30,137.43,A
2027-05-01,79.8,B
2027-05-02,108.37,B
2027-05-03,74.94,A
2027-05-04,164.35,B
2027-05-05,64.37,B
2027-05-06,109.29,C
2027-05-07,119.01,A
2027-05-08,112.41,C
2027-05-09,94.44,A
2027-05-10,96.11,C
2027-05-11,101.31,B
2027-05-12,95.59,A
2027-05-13,128.92,A
2027-05-14,166.32,A
2027-05-15,83.28,A
2027-05-16,58.91,B
2027-05-17,97.35,B
2027-05-18,177.39,B
2027-05-19,75.89,C
2027-05-20,149.17,B
2027-05-21,150.33,C
2027-05-22,83.39,C
2027-05-23,117.07,B
2027-05-24,148.85,B
2027-05-25,88.63,B
2027-05-26,93.89,C
2027-05-27,82.55,A
2027-05-28,69.56,B
2027-05-29,80.52,C
2027-05-30,63.28,C
2027-05-31,101.02,B
2027-06-01,76.9,C
2027-06-02,107.01,B
2027-06-03,53.32,C
2027-06-04,109.93,A
2027-06-05,125.01,C
2027-06-06,40.19,B
2027-06-07,111.22,A
2027-06-08,136.83,B
2027-06-09,63.71,C
2027-06-10,150.18,A
2027-06-11,112.57,A
2027-06-12,78.85,C
2027-06-13,98.33,A
2027-06-14,116.75,A
2027-06-15,102.28,A
2027-06-16,116.16,B
2027-06-17,72.38,B
2027-06-18,105.08,A
2027-06-19,57.59,C
2027-06-20,96.66,C
2027-06-21,72.88,B
2027-06-22,77.93,C
2027-06-23,137.08,A
2027-06-24,132.74,C
2027-06-25,118.27,B
2027-06-26,67.23,B
2027-06-27,90.51,A
2027-06-28,136.39,B
2027-06-29,104.25,B
2027-06-30,169.58,B
2027-07-01,111.8,C
2027-07-02,105.76,B
2027-07-03,90.73,C
2027-07-04,104.01,B
2027-07-05,95.43,B
2027-07-06,121.24,C
2027-07-07,128.7,C
2027-07-08,76.42,A
2027-07-09,60.06,A
2027-07-10,44.91,C
2027-07-11,115.24,B
2027-07-12,66.9,A
2027-07-13,35.41,C
2027-07-14,111.66,C
2027-07-15,174.79,B
2027-07-16,99.82,C
2027-07-17,125.15,A
2027-07-18,102.45,B
2027-07-19,97.03,C
2027-07-20,127.57,C
2027-07-21,91.29,B
2027-07-22,108.02,B
2027-07-23,109.65,B
2027-07-24,79.96,A
2027-07-25,129.76,A
2027-07-26,94.75,C
2027-07-27,77.33,C
2027-07-28,116.1,B
2027-07-29,73.05,C
2027-07-30,100.85,C
2027-07-31,99.73,B
2027-08-01,132.58,A
2027-08-02,114.24,A
2027-08-03,99.25,A
2027-08-04,124.53,C
2027-08-05,141.71,B
2027-08-06,116.73,A
2027-08-07,100.31,C
2027-08-08,60.64,B
2027-08-09,68.05,A
2027-08-10,90.84,A
2027-08-11,81.71,B
2027-08-12,94.39,C
2027-08-13,101.7,C
2027-08-14,115.89,A
2027-08-15,97.89,B
2027-08-16,114.6,C
2027-08-17,101.93,B
2027-08-18,40.74,B
2027-08-19,71.82,A
2027-08-20,95.68,B
2027-08-21,63.71,B
2027-08-22,118.0,B
2027-08-23,145.92,C
2027-08-24,136.56,A
2027-08-25,93.6,A
2027-08-26,144.72,B
2027-08-27,104.46,B
2027-08-28,89.89,C
2027-08-29,81.6,A
2027-08-30,90.93,C
2027-08-31,88.35,A
2027-09-01,105.11,C
2027-09-02,104.82,B
2027-09-03,100.09,C
2027-09-04,113.11,B
2027-09-05,135.72,A
2027-09-06,128.49,A
2027-09-07,55.45,B
2027-09-08,23.38,A
2027-09-09,128.03,C
2027-09-10,58.99,B
2027-09-11,93.26,A
2027-09-12,64.9,C
2027-09-13,45.94,C
2027-09-14,116.24,A
2027-09-15,122.77,A
2027-09-16,82.7,C
2027-09-17,22.27,B
2027-09-18,83.61,A
2027-09-19,111.75,A
2027-09-20,55.63,B
2027-09-21,105.5,B
2027-09-22,99.54,A
2027-09-23,117.38,B
2027-09-24,103.59,C
2027-09-25,70.81,C
2027-09-26,135.9,C
2027-09-27,95.24,A
2027-09-28,99.18,B
2027-09-29,72.0,C
2027-09-30,86.7,B
2027-10-01,73.46,C
2027-10-02,94.81,B
2027-10-03,151.35,C
2027-10-04,58.84,C
2027-10-05,51.59,C
2027-10-06,144.14,C
2027-10-07,93.72,C
2027-10-08,79.93,C
2027-10-09,131.2,C
2027-10-10,81.83,C
2027-10-11,154.78,C
2027-10-12,120.34,B
2027-10-13,85.36,B
2027-10-14,164.72,A
2027-10-15,81.83,C
2027-10-16,122.26,B
2027-10-17,108.98,B
2027-10-18,139.05,B
2027-10-19,146.85,B
2027-10-20,100.96,B
2027-10-21,77.4,B
2027-10-22,113.8,A
2027-10-23,79.67,B
2027-10-24,160.4,A
2027-10-25,104.1,A
2027-10-26,89.04,C
2027-10-27,105.54,C
2027-10-28,59.59,C
2027-10-29,70.85,B
2027-10-30,136.01,C
2027-10-31,80.29,B
2027-11-01,68.59,B
2027-11-02,116.1,C
2027-11-03,135.57,C
2027-11-04,121.57,A
2027-11-05,129.88,A
2027-11-06,77.3,B
2027-11-07,57.35,B
2027-11-08,145.04,C
2027-11-09,90.32,C
2027-11-10,92.48,A
2027-11-11,139.85,C
2027-11-12,116.69,B
2027-11-13,113.68,C
2027-11-14,164.95,A
2027-11-15,80.69,B
2027-11-16,127.84,A
2027-11-17,101.71,A
2027-11-18,108.06,B
2027-11-19,145.85,A
2027-11-20,115.24,C
2027-11-21,116.15,C
2027-11-22,132.18,A
2027-11-23,89.05,C
2027-11-24,74.82,A
2027-11-25,68.66,C
2027-11-26,41.01,A
2027-11-27,161.69,C
2027-11-28,66.9,B
2027-11-29,93.36,B
2027-11-30,91.7,C
2027-12-01,109.22,A
2027-12-02,124.47,C
2027-12-03,125.81,A
2027-12-04,82.51,C
2027-12-05,94.99,C
2027-12-06,108.48,B
2027-12-07,92.54,A
2027-12-08,148.22,A
2027-12-09,114.73,A
2027-12-10,122.05,A
2027-12-11,119.89,A
2027-12-12,135.2,B
2027-12-13,105.43,C
2027-12-14,61.1,A
2027-12-15,111.99,A
2027-12-16,80.46,C
2027-12-17,84.14,B
2027-12-18,117.59,B
2027-12-19,137.15,B
2027-12-20,100.64,C
2027-12-21,109.26,C
2027-12-22,151.07,B
2027-12-23,107.22,B
2027-12-24,178.05,C
2027-12-25,116.97,B
2027-12-26,47.18,A
2027-12-27,122.6,C
2027-12-28,111.43,B
2027-12-29,138.69,A
2027-12-30,120.2,C
2027-12-31,95.85,C
2028-01-01,63.27,A
2028-01-02,93.73,C
2028-01-03,74.48,B
2028-01-04,82.58,C
2028-01-05,117.66,B
2028-01-06,150.1,A
2028-01-07,111.84,A
2028-01-08,64.12,C
2028-01-09,113.34,A
2028-01-10,135.9,A
2028-01-11,81.71,C
2028-01-12,95.98,B
2028-01-13,100.44,C
2028-01-14,76.45,C
2028-01-15,119.45,B
2028-01-16,96.37,B
2028-01-17,112.59,B
2028-01-18,73.38,C
2028-01-19,86.88,B
2028-01-20,121.67,A
2028-01-21,88.82,B
2028-01-22,151.81,A
2028-01-23,88.01,B
2028-01-24,106.74,A
2028-01-25,127.98,A
2028-01-26,57.45,A
2028-01-27,47.18,A
2028-01-28,54.23,A
2028-01-29,137.88,B
2028-01-30,83.44,C
2028-01-31,176.75,C
2028-02-01,83.07,C
2028-02-02,105.54,C
2028-02-03,146.26,C
2028-02-04,160.18,A
2028-02-05,161.85,B
2028-02-06,136.25,A
2028-02-07,130.72,B
2028-02-08,117.78,B
2028-02-09,123.35,B
2028-02-10,83.46,B
2028-02-11,75.45,C
2028-02-12,99.9,B
2028-02-13,94.89,B
2028-02-14,86.4,C
2028-02-15,120.89,A
2028-02-16,128.66,A
2028-02-17,102.65,A
2028-02-18,144.33,A
2028-02-19,65.75,B
2028-02-20,94.19,C
2028-02-21,78.5,C
2028-02-22,44.0,A
2028-02-23,97.52,C
2028-02-24,96.35,B
2028-02-25,145.4,A
2028-02-26,118.92,B
2028-02-27,69.27,B
2028-02-28,155.62,B
2028-02-29,136.63,B
2028-03-01,117.46,B
2028-03-02,93.21,A
2028-03-03,71.22,B
2028-03-04,88.83,A
2028-03-05,132.66,C
2028-03-06,156.54,B
2028-03-07,146.3,B
2028-03-08,85.33,C
2028-03-09,66.41,A
2028-03-10,104.23,B
2028-03-11,46.95,C
2028-03-12,109.7,C
2028-03-13,95.57,B
2028-03-14,86.02,A
2028-03-15,52.16,A
2028-03-16,115.41,B
2028-03-17,84.02,C
2028-03-18,64.9,B
2028-03-19,13.83,A
2028-03-20,99.17,C
2028-03-21,153.17,B
2028-03-22,149.84,C
2028-03-23,86.29,A
2028-03-24,81.93,A
2028-03-25,114.06,A
2028-03-26,70.05,B
2028-03-27,109.05,C
2028-03-28,122.98,B
2028-03-29,136.81,B
2028-03-30,97.0,B
2028-03-31,93.89,B
2028-04-01,73.66,C
2028-04-02,75.19,C
2028-04-03,93.21,B
2028-04-04,111.02,B
2028-04-05,127.41,A
2028-04-06,75.9,A
2028-04-07,144.78,C
2028-04-08,91.87,A
2028-04-09,99.36,C
2028-04-10,77.58,C
2028-04-11,27.27,C
2028-04-12,126.52,A
2028-04-13,122.11,A
2028-04-14,91.56,C
2028-04-15,102.01,C
2028-04-16,115.48,B
2028-04-17,53.12,C
2028-04-18,84.13,C
2028-04-19,123.83,B
2028-04-20,62.37,C
2028-04-21,108.81,C
2028-04-22,59.3,C
2028-04-23,113.99,B
2028-04-24,98.93,A
2028-04-25,51.55,C
2028-04-26,134.94,B
2028-04-27,77.96,C
2028-04-28,75.69,B
2028-04-29,106.02,C
2028-04-30,134.46,B
2028-05-01,69.53,A
2028-05-02,101.85,A
2028-05-03,112.86,A
2028-05-04,120.79,C
2028-05-05,105.29,B
2028-05-06,88.99,C
2028-05-07,75.17,C
2028-05-08,102.58,B
2028-05-09,67.84,C
2028-05-10,12.36,B
2028-05-11,113.1,B
2028-05-12,127.12,A
2028-05-13,29.11,A
2028-05-14,69.71,A
2028-05-15,118.57,B
2028-05-16,161.72,A
2028-05-17,100.62,B
2028-05-18,78.16,C
2028-05-19,94.51,A
2028-05-20,141.25,B
2028-05-21,80.62,B
2028-05-22,76.02,B
2028-05-23,85.52,B
2028-05-24,71.4,A
2028-05-25,103.68,A
2028-05-26,148.74,A
2028-05-27,109.69,B
2028-05-28,92.43,C
2028-05-29,91.25,B
2028-05-30,53.1,C
2028-05-31,126.49,A
2028-06-01,97.66,B
2028-06-02,94.59,B
2028-06-03,195.79,A
2028-06-04,108.96,B
2028-06-05,77.45,C
2028-06-06,87.21,C
2028-06-07,134.45,C
2028-06-08,103.4,B
2028-06-09,56.85,B
2028-06-10,127.58,C
2028-06-11,79.96,B
2028-06-12,156.2,C
2028-06-13,132.4,A
2028-06-14,86.58,C
2028-06-15,138.43,A
2028-06-16,102.04,A
2028-06-17,125.58,A
2028-06-18,114.54,C
2028-06-19,74.61,C
2028-06-20,80.69,B
2028-06-21,130.9,B
2028-06-22,89.96,A
2028-06-23,87.89,C
2028-06-24,71.35,B
2028-06-25,112.71,B
2028-06-26,161.88,B
2028-06-27,67.97,C
2028-06-28,100.73,B
2028-06-29,142.37,A
2028-06-30,97.61,C
2028-07-01,113.57,B
2028-07-02,68.13,B
2028-07-03,112.85,C
2028-07-04,94.39,B
2028-07-05,129.57,B
2028-07-06,135.62,A
2028-07-07,177.69,C
2028-07-08,117.39,B
2028-07-09,109.77,C
2028-07-10,105.83,C
2028-07-11,89.41,A
2028-07-12,110.15,A
2028-07-13,91.14,C
2028-07-14,105.05,A
2028-07-15,139.53,A
2028-07-16,69.8,B
2028-07-17,134.2,A
2028-07-18,139.51,A
2028-07-19,96.46,B
2028-07-20,36.34,B
2028-07-21,81.77,C
2028-07-22,138.91,A
2028-07-23,99.31,B
2028-07-24,70.02,A
2028-07-25,84.86,C
2028-07-26,125.22,A
2028-07-27,116.4,A
2028-07-28,92.83,A
2028-07-29,89.0,B
2028-07-30,88.25,B
2028-07-31,72.33,C
2028-08-01,148.46,A
2028-08-02,90.33,C
2028-08-03,136.51,C
2028-08-04,145.64,B
2028-08-05,129.95,A
2028-08-06,87.05,A
2028-08-07,112.11,C
2028-08-08,99.27,A
2028-08-09,72.89,B
2028-08-10,109.73,C
2028-08-11,64.63,C
2028-08-12,135.63,B
2028-08-13,86.06,B
2028-08-14,106.03,B
2028-08-15,108.5,B
2028-08-16,92.23,B
2028-08-17,117.6,B
2028-08-18,85.75,C
2028-08-19,126.14,A
2028-08-20,59.62,A
2028-08-21,103.79,C
2028-08-22,158.17,A
2028-08-23,69.99,A
2028-08-24,79.67,C
2028-08-25,115.42,A
2028-08-26,105.39,A
2028-08-27,110.52,C
2028-08-28,114.68,A
2028-08-29,119.04,B
2028-08-30,133.29,C
2028-08-31,112.29,A
2028-09-01,92.76,A
2028-09-02,120.18,B
2028-09-03,157.0,C
2028-09-04,96.02,C
2028-09-05,70.76,A
2028-09-06,133.21,B
2028-09-07,96.39,B
2028-09-08,34.82,B
2028-09-09,125.42,B
2028-09-10,83.94,B
2028-09-11,97.28,B
2028-09-12,109.96,C
2028-09-13,105.71,A
2028-09-14,121.28,A
2028-09-15,86.94,B
2028-09-16,115.39,C
2028-09-17,92.21,C
2028-09-18,122.16,B
2028-09-19,118.46,A
2028-09-20,71.94,B
2028-09-21,132.58,C
2028-09-22,83.92,B
2028-09-23,124.24,A
2028-09-24,111.02,A
2028-09-25,155.15,C
2028-09-26,93.3,B
2028-09-27,89.52,B
2028-09-28,99.42,C
2028-09-29,90.9,B
2028-09-30,124.0,C
2028-10-01,51.51,B
2028-10-02,68.39,A
2028-10-03,67.97,C
2028-10-04,128.51,B
2028-10-05,151.32,B
2028-10-06,96.87,B
2028-10-07,94.94,C
2028-10-08,102.1,A
2028-10-09,134.86,B
2028-10-10,72.18,C
2028-10-11,107.15,B
2028-10-12,129.26,A
2028-10-13,115.03,A
2028-10-14,105.69,A
2028-10-15,130.03,C
2028-10-16,18.9,B
2028-10-17,120.34,C
2028-10-18,80.38,B
2028-10-19,45.08,C
2028-10-20,115.34,B
2028-10-21,141.21,C
2028-10-22,95.88,B
2028-10-23,128.59,C
2028-10-24,148.37,A
2028-10-25,139.45,C
2028-10-26,149.2,A
2028-10-27,122.26,A
2028-10-28,102.26,B
2028-10-29,51.94,A
2028-10-30,92.62,A
2028-10-31,74.7,B
2028-11-01,165.13,C
2028-11-02,94.72,C
2028-11-03,103.7,A
2028-11-04,116.54,C
2028-11-05,101.31,A
2028-11-06,150.85,C
2028-11-07,81.32,C
2028-11-08,105.84,C
2028-11-09,77.73,A
2028-11-10,60.4,B
2028-11-11,81.65,C
2028-11-12,98.89,C
2028-11-13,87.12,C
2028-11-14,79.23,A
2028-11-15,57.81,B
2028-11-16,97.51,A
2028-11-17,54.86,C
2028-11-18,122.8,A
2028-11-19,102.47,C
2028-11-20,56.27,C
2028-11-21,90.72,A
2028-11-22,77.44,C
2028-11-23,109.58,C
2028-11-24,140.21,B
2028-11-25,43.74,A
2028-11-26,103.45,A
2028-11-27,95.2,C
2028-11-28,120.14,C
2028-11-29,106.4,A
2028-11-30,77.44,A
2028-12-01,90.43,A
2028-12-02,76.12,C
2028-12-03,132.28,B
2028-12-04,100.64,B
2028-12-05,157.04,C
2028-12-06,98.18,C
2028-12-07,78.75,C
2028-12-08,54.59,A
2028-12-09,45.91,C
2028-12-10,52.48,A
2028-12-11,108.01,B
2028-12-12,115.26,C
2028-12-13,52.56,C
2028-12-14,126.85,A
2028-12-15,85.51,A
2028-12-16,104.4,A
2028-12-17,148.37,B
2028-12-18,126.91,B
2028-12-19,91.94,C
2028-12-20,73.26,C
2028-12-21,35.45,A
2028-12-22,78.43,B
2028-12-23,93.67,C
2028-12-24,70.38,A
2028-12-25,96.06,A
2028-12-26,102.31,B
2028-12-27,93.25,B
2028-12-28,80.5,C
2028-12-29,105.06,C
2028-12-30,113.26,A
2028-12-31,67.29,A
2029-01-01,142.33,A
2029-01-02,97.04,C
2029-01-03,100.57,B
2029-01-04,121.25,C
2029-01-05,107.0,B
2029-01-06,128.59,C
2029-01-07,108.61,C
2029-01-08,81.63,B
2029-01-09,110.85,C
2029-01-10,65.69,C
2029-01-11,103.26,A
2029-01-12,99.0,B
2029-01-13,93.76,B
2029-01-14,96.14,A
2029-01-15,43.54,B
2029-01-16,83.54,A
2029-01-17,102.79,B
2029-01-18,104.8,B
2029-01-19,69.17,B
2029-01-20,137.97,B
2029-01-21,74.01,A
2029-01-22,129.08,A
2029-01-23,112.82,B
2029-01-24,80.61,C
2029-01-25,153.26,A
2029-01-26,64.19,B
2029-01-27,127.57,A
2029-01-28,130.02,C
2029-01-29,79.88,C
2029-01-30,141.77,C
2029-01-31,92.5,B
2029-02-01,108.66,B
2029-02-02,107.81,C
2029-02-03,95.97,A
2029-02-04,124.32,A
2029-02-05,123.8,C
2029-02-06,47.54,B
2029-02-07,139.13,A
2029-02-08,50.13,A
2029-02-09,130.98,A
2029-02-10,133.8,B
2029-02-11,67.27,C
2029-02-12,87.68,B
2029-02-13,66.83,A
2029-02-14,93.55,C
2029-02-15,90.76,B
2029-02-16,123.39,C
2029-02-17,139.31,A
2029-02-18,141.87,B
2029-02-19,83.13,C
2029-02-20,93.72,B
2029-02-21,49.5,A
2029-02-22,75.82,C
2029-02-23,128.95,A
2029-02-24,148.47,C
2029-02-25,62.97,A
2029-02-26,82.23,A
2029-02-27,99.21,B
2029-02-28,108.4,A
2029-03-01,75.71,B
2029-03-02,112.72,A
2029-03-03,85.78,A
2029-03-04,99.57,A
2029-03-05,116.39,C
2029-03-06,100.19,B
2029-03-07,86.91,C
2029-03-08,96.71,B
2029-03-09,97.35,A
2029-03-10,88.9,C
2029-03-11,92.24,B
2029-03-12,147.96,A
2029-03-13,116.83,B
2029-03-14,91.14,B
2029-03-15,120.91,A
2029-03-16,89.99,C
2029-03-17,135.19,A
2029-03-18,111.09,C
2029-03-19,96.78,B
2029-03-20,113.43,B
2029-03-21,52.87,B
2029-03-22,66.2,C
2029-03-23,64.18,A
2029-03-24,104.29,C
2029-03-25,151.98,B
2029-03-26,166.94,B
2029-03-27,119.14,A
2029-03-28,115.03,C
2029-03-29,45.97,B
2029-03-30,83.72,C
2029-03-31,76.37,C
2029-04-01,81.37,A
2029-04-02,94.96,A
2029-04-03,85.84,B
2029-04-04,40.62,A
2029-04-05,122.44,C
2029-04-06,67.82,A
2029-04-07,107.18,C
2029-04-08,162.22,B
2029-04-09,72.42,C
2029-04-10,24.09,A
2029-04-11,91.42,A
2029-04-12,133.03,B
2029-04-13,158.75,A
2029-04-14,63.11,B
2029-04-15,114.9,B
2029-04-16,86.04,A
2029-04-17,96.82,C
2029-04-18,179.33,B
2029-04-19,54.91,C
2029-04-20,107.61,A
2029-04-21,114.03,C
2029-04-22,132.57,C
2029-04-23,102.94,C
2029-04-24,109.24,C
2029-04-25,88.25,A
2029-04-26,108.07,A
2029-04-27,89.7,C
2029-04-28,118.65,A
2029-04-29,88.91,A
2029-04-30,111.31,B
2029-05-01,99.12,A
2029-05-02,133.78,C
2029-05-03,98.46,A
2029-05-04,46.81,A
2029-05-05,137.86,A
2029-05-06,72.83,A
2029-05-07,80.39,B
2029-05-08,82.13,C
2029-05-09,141.23,C
2029-05-10,35.93,A
2029-05-11,194.13,A
2029-05-12,131.68,B
2029-05-13,106.7,A
2029-05-14,98.35,B
2029-05-15,108.57,C
2029-05-16,115.63,C
2029-05-17,119.36,B
2029-05-18,116.67,C
2029-05-19,102.69,A
2029-05-20,94.08,A
2029-05-21,95.46,B
2029-05-22,94.15,C
2029-05-23,134.01,C
2029-05-24,117.81,B
2029-05-25,11.79,B
2029-05-26,119.68,C
2029-05-27,105.84,B
2029-05-28,99.44,A
2029-05-29,88.34,C
2029-05-30,133.72,B
2029-05-31,128.43,B
2029-06-01,76.81,B
2029-06-02,112.21,C
2029-06-03,70.85,A
2029-06-04,58.61,A
2029-06-05,81.2,C
2029-06-06,125.87,B
2029-06-07,128.59,C
2029-06-08,115.39,C
2029-06-09,121.75,B
2029-06-10,115.49,C
2029-06-11,80.76,A
2029-06-12,112.96,C
2029-06-13,124.01,B
2029-06-14,122.63,C
2029-06-15,135.67,A
2029-06-16,121.25,C
2029-06-17,110.54,C
2029-06-18,132.1,C
2029-06-19,99.2,C
2029-06-20,73.54,B
2029-06-21,95.11,A
2029-06-22,77.65,A
2029-06-23,79.74,A
2029-06-24,95.66,C
2029-06-25,76.23,A
2029-06-26,90.76,B
2029-06-27,43.19,C
2029-06-28,106.4,C
2029-06-29,100.04,A
2029-06-30,75.49,C
2029-07-01,119.78,C
2029-07-02,128.13,A
2029-07-03,51.77,B
2029-07-04,77.12,B
2029-07-05,76.93,A
2029-07-06,71.8,B
2029-07-07,124.88,B
2029-07-08,94.19,B
2029-07-09,92.06,C
2029-07-10,39.88,C
2029-07-11,119.06,C
2029-07-12,62.82,B
2029-07-13,101.8,C
2029-07-14,108.32,B
2029-07-15,140.82,A
2029-07-16,60.74,B
2029-07-17,9.41,C
2029-07-18,105.52,B
2029-07-19,154.02,C
2029-07-20,137.17,C
2029-07-21,106.29,A
2029-07-22,85.25,A
2029-07-23,124.21,C
2029-07-24,70.79,B
2029-07-25,114.29,A
2029-07-26,115.16,A
2029-07-27,131.81,C
2029-07-28,182.79,B
2029-07-29,111.77,A
2029-07-30,84.73,A
2029-07-31,99.23,B
2029-08-01,46.93,A
2029-08-02,79.16,B
2029-08-03,87.72,C
2029-08-04,84.28,A
2029-08-05,104.57,C
2029-08-06,75.33,B
2029-08-07,133.63,C
2029-08-08,100.01,C
2029-08-09,99.72,C
2029-08-10,90.16,B
2029-08-11,104.66,B
2029-08-12,124.75,B
2029-08-13,73.99,B
2029-08-14,80.26,B
2029-08-15,90.89,A
2029-08-16,59.62,A
2029-08-17,75.42,A
2029-08-18,85.71,C
2029-08-19,126.23,C
2029-08-20,107.88,B
2029-08-21,105.81,B
2029-08-22,125.53,A
2029-08-23,95.88,B
2029-08-24,111.71,C
2029-08-25,96.9,B
2029-08-26,107.96,A
2029-08-27,82.52,B
2029-08-28,26.84,C
2029-08-29,95.97,A
2029-08-30,142.68,B
2029-08-31,127.79,C
2029-09-01,128.96,A
2029-09-02,137.08,A
2029-09-03,102.66,C
2029-09-04,105.92,B
2029-09-05,81.47,B
2029-09-06,90.52,B
2029-09-07,118.47,A
2029-09-08,136.12,A
2029-09-09,95.82,B
2029-09-10,86.49,A
2029-09-11,100.02,A
2029-09-12,118.04,C
2029-09-13,56.68,B
2029-09-14,31.11,B
2029-09-15,83.48,B
2029-09-16,63.38,A
2029-09-17,84.76,A
2029-09-18,95.57,A
2029-09-19,86.4,B
2029-09-20,143.57,A
2029-09-21,109.8,B
2029-09-22,109.01,A
2029-09-23,118.67,B
2029-09-24,65.84,C
2029-09-25,131.17,A
2029-09-26,97.73,B
2029-09-27,120.11,C
2029-09-28,67.84,C
2029-09-29,53.39,A
2029-09-30,124.54,B
2029-10-01,111.29,A
2029-10-02,72.94,C
2029-10-03,73.91,A
2029-10-04,133.76,B
2029-10-05,64.32,C
2029-10-06,149.28,A
2029-10-07,72.98,A
2029-10-08,119.15,A
2029-10-09,90.14,A
2029-10-10,118.1,A
2029-10-11,83.68,C
2029-10-12,95.12,C
2029-10-13,101.23,B
2029-10-14,69.93,A
2029-10-15,122.22,B
2029-10-16,84.6,C
2029-10-17,93.14,A
2029-10-18,70.17,B
2029-10-19,23.13,A
2029-10-20,94.27,A
2029-10-21,172.38,B
2029-10-22,123.54,C
2029-10-23,99.42,B
2029-10-24,92.11,B
2029-10-25,100.67,B
2029-10-26,116.41,A
2029-10-27,64.58,C
2029-10-28,133.43,C
2029-10-29,121.46,A
2029-10-30,121.55,C
2029-10-31,113.15,B
2029-11-01,100.59,C
2029-11-02,120.19,A
2029-11-03,117.75,C
2029-11-04,89.38,A
2029-11-05,82.79,B
2029-11-06,103.06,A
2029-11-07,146.47,C
2029-11-08,62.83,B
2029-11-09,55.97,C
2029-11-10,104.94,A
2029-11-11,101.53,B
2029-11-12,105.2,A
2029-11-13,107.32,C
2029-11-14,93.3,A
2029-11-15,144.7,C
2029-11-16,51.97,C
2029-11-17,75.15,B
2029-11-18,96.9,B
2029-11-19,50.7,A
2029-11-20,94.72,C
2029-11-21,149.84,C
2029-11-22,100.63,B
2029-11-23,106.92,B
2029-11-24,62.2,B
2029-11-25,81.51,B
2029-11-26,88.74,A
2029-11-27,90.47,A
2029-11-28,138.45,A
2029-11-29,116.73,A
2029-11-30,66.66,B
2029-12-01,107.4,A
2029-12-02,114.95,A
2029-12-03,134.2,A
2029-12-04,147.42,B
2029-12-05,69.55,C
2029-12-06,75.67,B
2029-12-07,62.27,C
2029-12-08,92.98,C
2029-12-09,113.99,A
2029-12-10,129.62,A
2029-12-11,97.72,A
2029-12-12,90.4,B
2029-12-13,104.55,B
2029-12-14,74.95,A
2029-12-15,162.69,B
2029-12-16,51.77,B
2029-12-17,105.54,C
2029-12-18,160.71,B
2029-12-19,100.2,C
2029-12-20,94.3,A
2029-12-21,89.28,B
2029-12-22,94.59,C
2029-12-23,141.19,B
2029-12-24,33.64,C
2029-12-25,146.0,C
2029-12-26,57.28,B
2029-12-27,92.0,A
2029-12-28,87.12,C
2029-12-29,117.66,A
2029-12-30,52.06,B
2029-12-31,113.87,B
2030-01-01,160.73,A
2030-01-02,59.1,B
2030-01-03,105.69,C
2030-01-04,80.14,A
2030-01-05,112.78,B
2030-01-06,100.57,B
2030-01-07,80.76,A
2030-01-08,114.64,C
2030-01-09,154.13,A
2030-01-10,94.27,B
2030-01-11,121.59,B
2030-01-12,61.2,A
2030-01-13,71.31,C
2030-01-14,114.17,C
2030-01-15,144.52,C
2030-01-16,110.67,A
2030-01-17,90.61,C
2030-01-18,99.98,C
2030-01-19,62.49,C
2030-01-20,118.14,C
2030-01-21,126.47,B
2030-01-22,86.44,A
2030-01-23,85.9,A
2030-01-24,107.98,C
2030-01-25,86.9,A
2030-01-26,98.02,A
2030-01-27,162.99,B
2030-01-28,92.59,A
2030-01-29,89.25,B
2030-01-30,80.57,A
2030-01-31,122.33,B
2030-02-01,94.56,B
2030-02-02,80.52,A
2030-02-03,139.64,B
2030-02-04,142.59,B
2030-02-05,81.99,A
2030-02-06,44.0,B
2030-02-07,130.23,B
2030-02-08,79.46,B
2030-02-09,123.72,B
2030-02-10,40.9,B
2030-02-11,126.78,C
2030-02-12,63.66,B
2030-02-13,121.92,B
2030-02-14,100.43,B
2030-02-15,71.38,B
2030-02-16,87.79,A
2030-02-17,120.59,C
2030-02-18,103.18,A
2030-02-19,117.53,C
2030-02-20,159.29,B
2030-02-21,53.07,B
2030-02-22,148.52,C
2030-02-23,103.13,C
2030-02-24,73.04,A
2030-02-25,60.09,C
2030-02-26,94.33,B
2030-02-27,127.65,A
2030-02-28,96.17,C
2030-03-01,145.33,C
2030-03-02,56.46,A
2030-03-03,99.64,C
2030-03-04,62.43,B
2030-03-05,110.91,A
2030-03-06,126.61,C
2030-03-07,87.38,B
2030-03-08,21.87,C
2030-03-09,105.97,A
2030-03-10,113.1,C
2030-03-11,112.13,C
2030-03-12,137.07,C
2030-03-13,67.87,B
2030-03-14,120.41,B
2030-03-15,135.78,C
2030-03-16,46.64,C
2030-03-17,109.59,A
2030-03-18,84.87,C
2030-03-19,97.55,A
2030-03-20,110.43,B
2030-03-21,85.38,C
2030-03-22,79.73,A
2030-03-23,101.02,B
2030-03-24,67.38,A
2030-03-25,67.43,B
2030-03-26,120.38,C
2030-03-27,65.54,A
2030-03-28,119.99,C
2030-03-29,113.88,A
2030-03-30,48.23,B
2030-03-31,79.67,A
2030-04-01,135.82,A
2030-04-02,70.57,C
2030-04-03,86.07,C
2030-04-04,113.86,C
2030-04-05,123.5,B
2030-04-06,92.45,C
2030-04-07,82.07,A
2030-04-08,142.67,B
2030-04-09,152.17,C
2030-04-10,129.37,A
2030-04-11,102.56,A
2030-04-12,75.75,B
2030-04-13,75.09,A
2030-04-14,115.68,C
2030-04-15,112.55,A
2030-04-16,142.05,C
2030-04-17,119.51,C
2030-04-18,54.91,B
2030-04-19,131.56,A
2030-04-20,70.06,B
2030-04-21,88.48,B
2030-04-22,107.51,B
2030-04-23,159.87,C
2030-04-24,193.3,C
2030-04-25,118.2,C
2030-04-26,94.5,C
2030-04-27,116.04,A
2030-04-28,126.63,A
2030-04-29,90.38,C
2030-04-30,153.86,C
2030-05-01,106.9,A
2030-05-02,114.93,B
2030-05-03,119.98,B
2030-05-04,112.65,C
2030-05-05,125.17,B
2030-05-06,81.48,C
2030-05-07,83.25,C
2030-05-08,67.0,C
2030-05-09,113.19,A
2030-05-10,123.37,B
2030-05-11,113.73,A
2030-05-12,150.23,B
2030-05-13,99.83,A
2030-05-14,120.06,A
2030-05-15,67.25,C
2030-05-16,88.39,C
2030-05-17,120.87,A
2030-05-18,125.47,B
2030-05-19,91.18,B
2030-05-20,97.85,C
2030-05-21,54.46,C
2030-05-22,89.29,B
2030-05-23,126.71,B
2030-05-24,117.26,B
2030-05-25,115.02,C
2030-05-26,101.49,B
2030-05-27,100.21,C
2030-05-28,80.19,A
2030-05-29,120.96,A
2030-05-30,112.63,A
2030-05-31,114.76,B
2030-06-01,84.22,C
2030-06-02,35.4,B
2030-06-03,132.91,C
2030-06-04,85.63,B
2030-06-05,74.12,A
2030-06-06,120.8,C
2030-06-07,88.24,B
2030-06-08,131.8,C
2030-06-09,118.51,C
2030-06-10,120.51,B
2030-06-11,59.02,B
2030-06-12,136.36,C
2030-06-13,107.84,C
2030-06-14,88.92,A
2030-06-15,104.3,C
2030-06-16,46.71,A
2030-06-17,112.26,A
2030-06-18,69.12,A
2030-06-19,59.42,C
2030-06-20,54.33,A
2030-06-21,133.38,C
2030-06-22,81.12,C
2030-06-23,146.01,B
2030-06-24,83.93,C
2030-06-25,48.78,C
2030-06-26,66.5,A
2030-06-27,137.07,C
2030-06-28,95.32,C
2030-06-29,83.55,B
2030-06-30,104.8,B
2030-07-01,115.05,A
2030-07-02,133.52,A
2030-07-03,143.45,B
2030-07-04,89.21,B
2030-07-05,60.22,C
2030-07-06,87.6,A
2030-07-07,107.81,A
2030-07-08,71.09,C
2030-07-09,71.29,C
2030-07-10,110.31,A
2030-07-11,98.54,B
2030-07-12,100.98,B
2030-07-13,77.25,C
2030-07-14,93.09,A
2030-07-15,72.27,C
2030-07-16,126.71,B
2030-07-17,131.06,B
2030-07-18,44.61,C
2030-07-19,72.11,B
2030-07-20,55.1,A
2030-07-21,80.5,C
2030-07-22,97.5,C
2030-07-23,56.51,C
2030-07-24,72.34,B
2030-07-25,69.88,A
2030-07-26,106.22,C
2030-07-27,102.08,C
2030-07-28,78.35,B
2030-07-29,105.3,A
2030-07-30,83.6,C
2030-07-31,91.85,B
2030-08-01,150.2,C
2030-08-02,140.21,B
2030-08-03,61.01,A
2030-08-04,124.89,B
2030-08-05,124.34,C
2030-08-06,65.55,C
2030-08-07,124.56,B
2030-08-08,146.14,A
2030-08-09,66.32,A
2030-08-10,72.47,C
2030-08-11,130.53,C
2030-08-12,108.14,B
2030-08-13,116.54,B
2030-08-14,110.22,B
2030-08-15,111.72,A
2030-08-16,60.21,A
2030-08-17,131.42,C
2030-08-18,135.09,C
2030-08-19,93.12,C
2030-08-20,98.7,B
2030-08-21,54.07,A
2030-08-22,115.43,C
2030-08-23,117.16,C
2030-08-24,98.13,C
2030-08-25,133.73,C
2030-08-26,89.98,C
2030-08-27,116.94,C
2030-08-28,69.41,A
2030-08-29,99.29,C
2030-08-30,94.77,A
2030-08-31,106.76,B
2030-09-01,88.91,A
2030-09-02,96.06,B
2030-09-03,124.78,A
2030-09-04,86.9,A
2030-09-05,51.8,C
2030-09-06,152.49,A
2030-09-07,141.44,A
2030-09-08,61.23,C
2030-09-09,120.69,B
2030-09-10,84.91,C
2030-09-11,107.9,C
2030-09-12,108.83,C
2030-09-13,92.97,B
2030-09-14,76.49,A
2030-09-15,79.28,A
2030-09-16,72.51,C
2030-09-17,75.05,C
2030-09-18,97.98,A
2030-09-19,78.53,A
2030-09-20,120.46,A
2030-09-21,144.62,B
2030-09-22,82.6,B
2030-09-23,107.18,C
2030-09-24,114.99,B
2030-09-25,114.16,C
2030-09-26,102.28,A
2030-09-27,122.28,A
2030-09-28,114.47,C
2030-09-29,62.87,A
2030-09-30,126.07,C
2030-10-01,126.62,C
2030-10-02,77.1,C
2030-10-03,101.14,B
2030-10-04,120.5,C
2030-10-05,93.72,C
2030-10-06,132.19,C
2030-10-07,170.92,B
2030-10-08,76.42,A
2030-10-09,58.57,B
2030-10-10,109.11,C
2030-10-11,121.65,B
2030-10-12,93.07,A
2030-10-13,143.6,A
2030-10-14,59.84,C
2030-10-15,120.79,A
2030-10-16,81.82,B
2030-10-17,151.58,B
2030-10-18,159.78,A
2030-10-19,77.0,C
2030-10-20,83.51,C
2030-10-21,125.79,B
2030-10-22,88.39,A
2030-10-23,98.64,B
2030-10-24,100.76,B
2030-10-25,42.41,B
2030-10-26,99.58,C
2030-10-27,79.31,A
2030-10-28,85.21,A
2030-10-29,143.31,C
2030-10-30,62.3,B
2030-10-31,124.4,B
2030-11-01,91.63,C
2030-11-02,91.61,A
2030-11-03,123.71,C
2030-11-04,110.2,A
2030-11-05,117.12,A
2030-11-06,129.05,A
2030-11-07,90.06,B
2030-11-08,81.63,B
2030-11-09,67.45,C
2030-11-10,75.24,C
2030-11-11,188.47,B
2030-11-12,137.34,C
2030-11-13,59.47,B
2030-11-14,60.33,B
2030-11-15,114.46,A
2030-11-16,116.42,A
2030-11-17,116.47,B
2030-11-18,92.35,A
2030-11-19,96.24,B
2030-11-20,109.84,A
2030-11-21,102.58,C
2030-11-22,33.42,C
2030-11-23,93.11,B
2030-11-24,74.46,B
2030-11-25,105.26,A
2030-11-26,189.56,A
2030-11-27,111.02,B
2030-11-28,90.59,C
2030-11-29,127.65,C
2030-11-30,114.48,B
2030-12-01,112.6,C
2030-12-02,118.21,B
2030-12-03,161.7,A
2030-12-04,66.07,C
2030-12-05,114.21,C
2030-12-06,72.21,A
2030-12-07,116.67,A
2030-12-08,72.44,C
2030-12-09,87.48,C
2030-12-10,91.15,B
2030-12-11,129.27,B
2030-12-12,127.55,C
2030-12-13,62.63,B
2030-12-14,101.65,A
2030-12-15,78.71,B
2030-12-16,62.24,B
2030-12-17,93.49,C
2030-12-18,90.75,B
2030-12-19,172.8,C
2030-12-20,112.99,C
2030-12-21,58.67,C
2030-12-22,83.06,A
2030-12-23,130.64,B
2030-12-24,162.65,B
2030-12-25,52.42,B
2030-12-26,43.6,C
2030-12-27,156.13,A
2030-12-28,111.69,C
2030-12-29,73.95,B
2030-12-30,116.04,C
2030-12-31,20.93,C
2031-01-01,100.1,B
2031-01-02,109.83,C
2031-01-03,127.73,C
2031-01-04,69.58,A
2031-01-05,102.57,C
2031-01-06,72.24,A
2031-01-07,107.66,B
2031-01-08,73.14,A
2031-01-09,87.76,C
2031-01-10,70.13,B
2031-01-11,119.53,C
2031-01-12,125.75,C
2031-01-13,92.96,A
2031-01-14,101.15,C
2031-01-15,56.54,A
2031-01-16,91.01,B
2031-01-17,98.49,B
2031-01-18,178.62,C
2031-01-19,66.64,A
2031-01-20,143.93,C
2031-01-21,129.91,C
2031-01-22,108.42,C
2031-01-23,152.76,C
2031-01-24,111.16,A
2031-01-25,111.67,C
2031-01-26,98.41,B
2031-01-27,136.71,A
2031-01-28,132.89,C
2031-01-29,81.23,A
2031-01-30,60.42,C
2031-01-31,93.91,B
2031-02-01,76.0,B
2031-02-02,98.09,B
2031-02-03,137.12,A
2031-02-04,86.28,B
2031-02-05,98.72,C
2031-02-06,101.74,C
2031-02-07,125.45,C
2031-02-08,32.59,A
2031-02-09,81.8,A
2031-02-10,106.34,B
2031-02-11,136.0,A
2031-02-12,85.24,C
2031-02-13,43.7,C
2031-02-14,118.59,C
2031-02-15,80.94,A
2031-02-16,64.31,C
2031-02-17,81.27,B
2031-02-18,94.46,A
2031-02-19,81.86,C
2031-02-20,33.83,C
2031-02-21,126.91,A
2031-02-22,138.25,A
2031-02-23,119.55,B
2031-02-24,65.87,A
2031-02-25,93.96,C
2031-02-26,99.8,B
2031-02-27,117.96,C
2031-02-28,120.05,C
2031-03-01,77.97,C
2031-03-02,102.46,A
2031-03-03,113.72,A
2031-03-04,143.67,C
2031-03-05,121.14,A
2031-03-06,123.67,C
2031-03-07,102.51,A
2031-03-08,142.31,A
2031-03-09,112.29,A
2031-03-10,74.17,B
2031-03-11,142.1,A
2031-03-12,120.94,B
2031-03-13,86.71,B
2031-03-14,85.62,B
2031-03-15,108.9,C
2031-03-16,113.9,A
2031-03-17,105.92,C
2031-03-18,109.34,B
2031-03-19,151.0,A
2031-03-20,132.15,C
2031-03-21,105.72,B
2031-03-22,128.24,C
2031-03-23,69.02,C
2031-03-24,111.94,A
2031-03-25,154.28,A
2031-03-26,93.46,C
2031-03-27,74.57,A
2031-03-28,80.44,A
2031-03-29,67.31,C
2031-03-30,76.46,A
2031-03-31,88.88,A
2031-04-01,57.83,B
2031-04-02,100.47,A
2031-04-03,127.07,B
2031-04-04,72.77,C
2031-04-05,145.58,B
2031-04-06,115.32,C
2031-04-07,130.92,A
2031-04-08,80.26,A
2031-04-09,125.64,B
2031-04-10,67.24,C
2031-04-11,126.71,A
2031-04-12,105.16,B
2031-04-13,116.6,B
2031-04-14,64.73,B
2031-04-15,73.15,A
2031-04-16,117.92,C
2031-04-17,71.55,A
2031-04-18,113.9,A
2031-04-19,58.99,C
2031-04-20,125.45,B
2031-04-21,63.02,A
2031-04-22,116.57,B
2031-04-23,118.77,B
2031-04-24,79.1,C
2031-04-25,117.46,C
2031-04-26,107.81,C
2031-04-27,83.83,B
2031-04-28,69.74,B
2031-04-29,41.12,A
2031-04-30,110.49,B
2031-05-01,53.06,C
2031-05-02,102.86,C
2031-05-03,92.1,C
2031-05-04,120.37,C
2031-05-05,90.94,B
2031-05-06,90.12,C
2031-05-07,121.96,A
2031-05-08,110.05,B
2031-05-09,109.48,C
2031-05-10,114.08,A
2031-05-11,53.93,C
2031-05-12,122.7,B
2031-05-13,118.37,B
2031-05-14,69.5,A
2031-05-15,92.68,B
2031-05-16,98.82,A
2031-05-17,95.97,C
2031-05-18,110.02,B
2031-05-19,142.94,C
2031-05-20,132.45,C
2031-05-21,60.63,B
2031-05-22,118.66,C
2031-05-23,139.87,A
2031-05-24,111.6,A
2031-05-25,132.73,A
2031-05-26,160.37,B
2031-05-27,130.71,C
2031-05-28,107.48,C
2031-05-29,131.35,C
2031-05-30,104.35,C
2031-05-31,100.72,A
2031-06-01,89.46,B
2031-06-02,146.9,A
2031-06-03,75.45,A
2031-06-04,145.97,B
2031-06-05,115.0,B
2031-06-06,58.02,A
2031-06-07,111.03,C
2031-06-08,37.0,B
2031-06-09,118.77,B
2031-06-10,126.56,A
2031-06-11,82.23,C
2031-06-12,103.71,B
2031-06-13,158.62,A
2031-06-14,84.83,B
2031-06-15,68.23,C
2031-06-16,144.45,A
2031-06-17,158.88,A
2031-06-18,100.11,C
2031-06-19,130.34,A
2031-06-20,140.24,C
2031-06-21,77.73,A
2031-06-22,85.44,C
2031-06-23,136.93,B
2031-06-24,150.55,B
2031-06-25,116.89,B
2031-06-26,73.61,A
2031-06-27,159.61,C
2031-06-28,84.07,C
2031-06-29,89.95,B
2031-06-30,110.27,B
2031-07-01,146.62,B
2031-07-02,125.62,C
2031-07-03,112.45,B
2031-07-04,113.9,A
2031-07-05,101.31,B
2031-07-06,116.74,B
2031-07-07,24.11,C
2031-07-08,91.1,A
2031-07-09,107.24,C
2031-07-10,65.47,A
2031-07-11,111.59,A
2031-07-12,93.87,B
2031-07-13,152.66,B
2031-07-14,147.19,C
2031-07-15,86.03,C
2031-07-16,95.47,C
2031-07-17,97.78,A
2031-07-18,86.45,C
2031-07-19,105.85,A
2031-07-20,77.25,A
2031-07-21,66.08,A
2031-07-22,118.68,A
2031-07-23,118.89,A
2031-07-24,75.87,B
2031-07-25,126.86,A
2031-07-26,81.05,A
2031-07-27,107.59,B
2031-07-28,124.61,B
2031-07-29,98.99,B
2031-07-30,113.64,C
2031-07-31,84.51,C
2031-08-01,94.11,A
2031-08-02,93.82,A
2031-08-03,77.6,A
2031-08-04,94.7,A
2031-08-05,53.54,B
2031-08-06,85.27,C
2031-08-07,91.45,B
2031-08-08,92.31,C
2031-08-09,92.76,B
2031-08-10,98.15,A
2031-08-11,114.38,C
2031-08-12,126.24,A
2031-08-13,80.51,C
2031-08-14,63.9,C
2031-08-15,68.74,A
2031-08-16,85.38,A
2031-08-17,89.44,A
2031-08-18,76.9,B
2031-08-19,61.12,C
2031-08-20,86.32,C
2031-08-21,105.44,C
2031-08-22,117.85,A
2031-08-23,83.28,C
2031-08-24,87.61,C
2031-08-25,72.2,A
2031-08-26,99.06,A
2031-08-27,74.55,C
2031-08-28,117.19,B
2031-08-29,46.42,A
2031-08-30,89.21,B
2031-08-31,109.03,B
2031-09-01,105.52,B
2031-09-02,180.79,B
2031-09-03,110.49,A
2031-09-04,69.88,C
2031-09-05,97.14,C
2031-09-06,46.71,A
2031-09-07,97.58,A
2031-09-08,75.01,B
2031-09-09,127.46,B
2031-09-10,83.51,A
2031-09-11,96.5,C
2031-09-12,80.93,B
2031-09-13,152.17,A
2031-09-14,90.36,A
2031-09-15,154.98,A
2031-09-16,124.42,B
2031-09-17,114.46,A
2031-09-18,111.06,C
2031-09-19,111.81,A
2031-09-20,42.17,B
2031-09-21,91.63,C
2031-09-22,125.35,C
2031-09-23,98.53,A
2031-09-24,57.84,A
2031-09-25,98.43,C
2031-09-26,151.12,A
2031-09-27,137.41,B
2031-09-28,98.14,C
2031-09-29,128.54,A
2031-09-30,89.0,C
2031-10-01,48.88,A
2031-10-02,72.27,A
2031-10-03,146.89,B
2031-10-04,91.78,A
2031-10-05,92.7,A
2031-10-06,91.0,A
2031-10-07,157.12,B
2031-10-08,148.78,A
2031-10-09,166.56,A
2031-10-10,95.23,A
2031-10-11,108.87,C
2031-10-12,54.5,B
2031-10-13,144.31,C
2031-10-14,64.97,B
2031-10-15,106.5,B
2031-10-16,67.08,C
2031-10-17,82.33,B
2031-10-18,74.88,B
2031-10-19,81.76,C
2031-10-20,83.83,A
2031-10-21,83.55,B
2031-10-22,125.0,C
2031-10-23,66.85,B
2031-10-24,106.62,C
2031-10-25,136.54,A
2031-10-26,84.6,B
2031-10-27,78.46,A
2031-10-28,93.08,A
2031-10-29,135.39,C
2031-10-30,105.82,B
2031-10-31,84.06,B
2031-11-01,114.52,B
2031-11-02,66.93,B
2031-11-03,120.44,B
2031-11-04,112.26,C
2031-11-05,90.77,B
2031-11-06,74.84,A
2031-11-07,73.4,A
2031-11-08,116.04,B
2031-11-09,136.87,C
2031-11-10,80.88,C
2031-11-11,113.75,B
2031-11-12,37.39,A
2031-11-13,82.46,B
2031-11-14,99.07,B
2031-11-15,72.71,B
2031-11-16,71.9,C
2031-11-17,79.97,C
2031-11-18,108.77,A
2031-11-19,94.38,A
2031-11-20,32.85,B
2031-11-21,36.38,A
2031-11-22,81.79,B
2031-11-23,113.73,A
2031-11-24,17.57,B
2031-11-25,85.01,C
2031-11-26,84.21,B
2031-11-27,141.65,C
2031-11-28,88.45,C
2031-11-29,111.49,A
2031-11-30,104.24,A
2031-12-01,36.08,C
2031-12-02,123.05,B
2031-12-03,106.46,B
2031-12-04,115.25,A
2031-12-05,217.79,B
2031-12-06,37.48,B
2031-12-07,151.74,B
2031-12-08,91.38,C
2031-12-09,108.62,C
2031-12-10,98.63,B
2031-12-11,87.27,A
2031-12-12,82.91,B
2031-12-13,109.89,A
2031-12-14,54.48,C
2031-12-15,122.52,B
2031-12-16,87.51,B
2031-12-17,66.1,A
2031-12-18,86.5,A
2031-12-19,137.71,B
2031-12-20,83.94,B
2031-12-21,110.75,A
2031-12-22,78.07,C
2031-12-23,122.74,A
2031-12-24,120.57,B
2031-12-25,155.46,C
2031-12-26,94.73,C
2031-12-27,120.06,C
2031-12-28,102.94,C
2031-12-29,138.88,B
2031-12-30,78.44,C
2031-12-31,122.35,A
2032-01-01,94.16,A
2032-01-02,98.07,A
2032-01-03,102.75,B
2032-01-04,107.57,C
2032-01-05,96.51,A
2032-01-06,106.44,A
2032-01-07,147.34,A
2032-01-08,129.56,C
2032-01-09,126.07,A
2032-01-10,86.33,C
2032-01-11,73.32,C
2032-01-12,128.66,C
2032-01-13,126.28,B
2032-01-14,144.18,B
2032-01-15,81.88,C
2032-01-16,93.11,C
2032-01-17,50.82,B
2032-01-18,88.23,B
2032-01-19,129.9,C
2032-01-20,86.01,B
2032-01-21,119.21,A
2032-01-22,94.28,C
2032-01-23,109.72,C
2032-01-24,65.57,B
2032-01-25,102.56,A
2032-01-26,10.27,B
2032-01-27,94.39,A
2032-01-28,51.09,A
2032-01-29,136.18,C
2032-01-30,123.32,A
2032-01-31,114.0,C
2032-02-01,145.63,A
2032-02-02,71.53,C
2032-02-03,152.42,A
2032-02-04,127.97,B
2032-02-05,92.9,A
2032-02-06,134.07,B
2032-02-07,66.81,A
2032-02-08,75.26,A
2032-02-09,81.74,C
2032-02-10,84.13,A
2032-02-11,68.3,A
2032-02-12,136.69,A
2032-02-13,92.23,B
2032-02-14,110.58,C
2032-02-15,82.89,C
2032-02-16,45.38,A
2032-02-17,108.1,A
2032-02-18,42.63,B
2032-02-19,97.94,A
2032-02-20,58.95,C
2032-02-21,159.62,C
2032-02-22,127.34,B
2032-02-23,103.17,B
2032-02-24,137.91,C
2032-02-25,74.61,B
2032-02-26,116.3,A
2032-02-27,105.99,A
2032-02-28,107.92,B
2032-02-29,138.17,C
2032-03-01,121.97,B
2032-03-02,108.66,C
2032-03-03,50.35,B
2032-03-04,71.2,B
2032-03-05,96.32,B
2032-03-06,102.8,A
2032-03-07,66.09,A
2032-03-08,172.35,C
2032-03-09,145.49,B
2032-03-10,118.06,A
2032-03-11,102.16,B
2032-03-12,93.63,C
2032-03-13,71.44,A
2032-03-14,102.32,C
2032-03-15,107.73,A
2032-03-16,62.75,B
2032-03-17,110.03,C
2032-03-18,95.34,A
2032-03-19,42.77,A
2032-03-20,74.19,B
2032-03-21,87.59,C
2032-03-22,156.63,C
2032-03-23,116.7,C
2032-03-24,59.94,B
2032-03-25,114.58,A
2032-03-26,53.58,A
2032-03-27,132.48,C
2032-03-28,85.87,A
2032-03-29,97.19,C
2032-03-30,139.77,B
2032-03-31,61.39,B
2032-04-01,58.09,B
2032-04-02,82.49,C
2032-04-03,131.15,A
2032-04-04,54.42,B
2032-04-05,15.04,C
2032-04-06,86.47,C
2032-04-07,116.55,B
2032-04-08,136.01,B
2032-04-09,86.11,B
2032-04-10,87.66,A
2032-04-11,134.62,A
2032-04-12,43.91,C
2032-04-13,88.34,B
2032-04-14,105.71,B
2032-04-15,113.48,A
2032-04-16,84.71,B
2032-04-17,101.03,C
2032-04-18,25.35,B
2032-04-19,80.25,A
2032-04-20,113.61,C
2032-04-21,70.53,C
2032-04-22,101.77,B
2032-04-23,113.41,C
2032-04-24,89.72,B
2032-04-25,105.11,B
2032-04-26,71.12,A
2032-04-27,93.8,C
2032-04-28,118.31,B
2032-04-29,104.71,B
2032-04-30,82.4,B
2032-05-01,106.73,C
2032-05-02,121.44,C
2032-05-03,38.5,C
2032-05-04,134.77,B
2032-05-05,89.91,C
2032-05-06,112.76,B
2032-05-07,135.92,C
2032-05-08,58.85,C
2032-05-09,78.72,B
2032-05-10,91.34,C
2032-05-11,76.49,B
2032-05-12,152.05,C
2032-05-13,74.3,C
2032-05-14,83.33,C
2032-05-15,106.13,C
2032-05-16,63.94,A
2032-05-17,88.13,B
2032-05-18,109.52,A
2032-05-19,90.01,C
2032-05-20,97.2,C
2032-05-21,84.12,B
2032-05-22,54.57,B
2032-05-23,109.65,B
2032-05-24,152.65,B
2032-05-25,100.55,C
2032-05-26,106.76,B
2032-05-27,120.78,A
2032-05-28,61.92,B
2032-05-29,151.08,B
2032-05-30,106.07,A
2032-05-31,148.96,A
2032-06-01,78.01,A
2032-06-02,154.54,A
2032-06-03,123.25,B
2032-06-04,116.59,A
2032-06-05,107.02,C
2032-06-06,92.54,B
2032-06-07,136.02,C
2032-06-08,104.21,B
2032-06-09,40.99,B
2032-06-10,66.48,C
2032-06-11,94.42,A
2032-06-12,109.3,A
2032-06-13,98.3,C
2032-06-14,136.57,A
2032-06-15,41.47,B
2032-06-16,104.31,B
2032-06-17,45.46,B
2032-06-18,122.79,A
2032-06-19,97.17,A
2032-06-20,112.59,C
2032-06-21,74.08,B
2032-06-22,138.38,C
2032-06-23,131.25,A
2032-06-24,117.51,C
2032-06-25,96.11,A
2032-06-26,117.4,B
2032-06-27,78.79,B
2032-06-28,125.67,A
2032-06-29,149.48,A
2032-06-30,132.12,A
2032-07-01,78.11,B
2032-07-02,110.84,A
2032-07-03,61.21,B
2032-07-04,117.17,C
2032-07-05,113.52,B
2032-07-06,43.91,C
2032-07-07,65.15,B
2032-07-08,91.51,B
2032-07-09,90.97,A
2032-07-10,63.72,B
2032-07-11,111.67,A
2032-07-12,107.54,A
2032-07-13,94.17,A
2032-07-14,77.33,B
2032-07-15,131.46,A
2032-07-16,149.66,C
2032-07-17,85.49,C
2032-07-18,81.63,B
2032-07-19,114.74,A
2032-07-20,89.26,C
2032-07-21,95.82,B
2032-07-22,122.19,B
2032-07-23,42.72,B
2032-07-24,139.55,B
2032-07-25,102.18,B
2032-07-26,87.66,A
2032-07-27,97.32,A
2032-07-28,98.87,A
2032-07-29,48.06,A
2032-07-30,144.85,B
2032-07-31,101.24,B
2032-08-01,113.29,C
2032-08-02,128.55,C
2032-08-03,69.37,A
2032-08-04,114.2,B
2032-08-05,91.97,C
2032-08-06,125.4,C
2032-08-07,36.18,C
2032-08-08,97.03,C
2032-08-09,81.92,A
2032-08-10,112.97,B
2032-08-11,114.1,A
2032-08-12,78.77,C
2032-08-13,78.63,C
2032-08-14,96.68,B
2032-08-15,73.1,B
2032-08-16,125.26,A
2032-08-17,88.92,B
2032-08-18,12.79,A
2032-08-19,88.76,A
2032-08-20,68.84,C
2032-08-21,51.06,A
2032-08-22,62.88,C
2032-08-23,103.28,A
2032-08-24,139.86,A
2032-08-25,109.4,B
2032-08-26,81.8,B
2032-08-27,113.68,C
2032-08-28,86.23,B
2032-08-29,79.16,A
2032-08-30,65.37,C
2032-08-31,47.45,C
2032-09-01,88.3,C
2032-09-02,104.74,A
2032-09-03,97.1,B
2032-09-04,87.52,C
2032-09-05,71.63,A
2032-09-06,118.25,B
2032-09-07,60.49,B
2032-09-08,123.28,C
2032-09-09,69.93,A
2032-09-10,77.43,B
2032-09-11,56.0,A
2032-09-12,84.96,B
2032-09-13,129.26,C
2032-09-14,115.47,C
2032-09-15,129.35,B
2032-09-16,115.66,A
2032-09-17,66.89,B
2032-09-18,90.08,A
2032-09-19,76.59,B
2032-09-20,139.92,C
2032-09-21,64.1,A
2032-09-22,126.81,C
2032-09-23,126.79,A
2032-09-24,154.89,B
2032-09-25,87.71,C
2032-09-26,121.37,C
2032-09-27,168.45,A
2032-09-28,81.47,A
2032-09-29,53.95,A
2032-09-30,43.6,C
2032-10-01,121.38,B
2032-10-02,43.51,A
2032-10-03,88.83,B
2032-10-04,113.12,A
2032-10-05,105.56,B
2032-10-06,112.76,A
2032-10-07,106.67,B
2032-10-08,138.37,C
2032-10-09,71.43,A
2032-10-10,79.69,A
2032-10-11,76.82,C
2032-10-12,124.91,A
2032-10-13,127.01,A
2032-10-14,113.54,C
2032-10-15,135.5,C
2032-10-16,64.66,B
2032-10-17,150.02,C
2032-10-18,145.7,C
2032-10-19,122.07,A
2032-10-20,153.45,C
2032-10-21,50.3,A
2032-10-22,84.27,C
2032-10-23,77.94,C
2032-10-24,121.64,B
2032-10-25,68.5,C
2032-10-26,122.72,C
2032-10-27,141.12,A
2032-10-28,120.86,A
2032-10-29,108.51,B
2032-10-30,70.36,C
2032-10-31,74.59,A
2032-11-01,137.49,C
2032-11-02,123.38,B
2032-11-03,98.83,A
2032-11-04,87.46,A
2032-11-05,39.23,B
2032-11-06,67.87,C
2032-11-07,157.34,A
2032-11-08,61.59,A
2032-11-09,105.68,B
2032-11-10,130.22,B
2032-11-11,62.35,B
2032-11-12,105.54,C
2032-11-13,128.14,C
2032-11-14,100.37,C
2032-11-15,186.05,B
2032-11-16,49.94,A
2032-11-17,131.76,C
2032-11-18,94.82,B
2032-11-19,123.16,C
2032-11-20,113.24,A
2032-11-21,78.01,A
2032-11-22,106.87,B
2032-11-23,44.26,A
2032-11-24,118.1,B
2032-11-25,108.94,B
2032-11-26,119.16,B
2032-11-27,131.74,B
2032-11-28,111.03,A
2032-11-29,104.44,A
2032-11-30,73.57,A
2032-12-01,78.63,A
2032-12-02,135.6,B
2032-12-03,143.09,B
2032-12-04,92.86,A
2032-12-05,101.38,C
2032-12-06,72.86,C
2032-12-07,135.18,C
2032-12-08,119.96,A
2032-12-09,158.24,B
2032-12-10,73.65,B
2032-12-11,88.66,C
2032-12-12,106.94,C
2032-12-13,119.39,B
2032-12-14,93.53,C
2032-12-15,73.81,C
2032-12-16,126.44,B
2032-12-17,121.63,C
2032-12-18,72.51,C
2032-12-19,140.66,A
2032-12-20,135.11,C
2032-12-21,104.03,A
2032-12-22,102.39,B
2032-12-23,116.62,A
2032-12-24,74.15,B
2032-12-25,100.9,A
2032-12-26,35.43,A
2032-12-27,126.29,A
2032-12-28,53.16,C
2032-12-29,145.1,C
2032-12-30,90.1,C
2032-12-31,93.65,C
2033-01-01,81.17,C
2033-01-02,91.36,A
2033-01-03,142.56,C
2033-01-04,25.37,B
2033-01-05,138.31,A
2033-01-06,110.14,A
2033-01-07,63.79,B
2033-01-08,67.74,C
2033-01-09,150.29,B
2033-01-10,71.63,A
2033-01-11,65.4,A
2033-01-12,134.12,A
2033-01-13,110.16,B
2033-01-14,71.86,C
2033-01-15,106.5,A
2033-01-16,69.23,C
2033-01-17,133.02,A
2033-01-18,131.84,C
2033-01-19,115.96,B
2033-01-20,110.85,A
2033-01-21,152.84,A
2033-01-22,99.99,A
2033-01-23,64.54,B
2033-01-24,113.48,B
2033-01-25,162.58,C
2033-01-26,69.56,B
2033-01-27,89.16,A
2033-01-28,112.49,C
2033-01-29,98.38,B
2033-01-30,70.53,C
2033-01-31,133.66,C
2033-02-01,169.6,C
2033-02-02,105.88,A
2033-02-03,72.88,A
2033-02-04,53.53,B
2033-02-05,107.74,A
2033-02-06,133.12,C
2033-02-07,114.26,C
2033-02-08,99.93,A
2033-02-09,82.32,C
2033-02-10,67.24,A
2033-02-11,125.04,C
2033-02-12,127.41,B
2033-02-13,53.63,B
2033-02-14,147.7,A
2033-02-15,117.22,A
2033-02-16,141.99,A
2033-02-17,59.74,A
2033-02-18,59.03,A
2033-02-19,95.53,C
2033-02-20,115.08,B
2033-02-21,153.89,C
2033-02-22,121.18,B
2033-02-23,92.72,C
2033-02-24,69.21,C
2033-02-25,136.9,C
2033-02-26,71.06,A
2033-02-27,148.85,B
2033-02-28,91.47,A
2033-03-01,147.72,A
2033-03-02,120.37,C
2033-03-03,95.89,B
2033-03-04,84.42,B
2033-03-05,89.77,B
2033-03-06,112.85,B
2033-03-07,102.31,A
2033-03-08,82.19,A
2033-03-09,95.03,B
2033-03-10,102.36,A
2033-03-11,36.14,B
2033-03-12,113.75,A
2033-03-13,70.61,C
2033-03-14,65.4,C
2033-03-15,49.38,B
2033-03-16,46.18,B
2033-03-17,59.34,A
2033-03-18,78.73,B
2033-03-19,158.6,B
2033-03-20,84.22,A
2033-03-21,105.33,B
2033-03-22,112.01,B
2033-03-23,103.93,C
2033-03-24,97.68,C
2033-03-25,64.14,C
2033-03-26,143.53,B
2033-03-27,154.22,A
2033-03-28,49.52,C
2033-03-29,69.28,B
2033-03-30,91.61,C
2033-03-31,71.06,A
2033-04-01,115.18,A
2033-04-02,78.15,A
2033-04-03,164.95,A
2033-04-04,135.72,A
2033-04-05,106.38,A
2033-04-06,130.81,B
2033-04-07,133.18,C
2033-04-08,83.08,A
2033-04-09,75.51,C
2033-04-10,102.34,C
2033-04-11,125.85,B
2033-04-12,104.17,A
2033-04-13,52.69,A
2033-04-14,75.92,B
2033-04-15,97.78,B
2033-04-16,97.73,B
2033-04-17,159.18,B
2033-04-18,58.42,B
2033-04-19,115.17,C
2033-04-20,144.67,B
2033-04-21,168.14,A
2033-04-22,87.87,A
2033-04-23,114.74,B
2033-04-24,117.09,B
2033-04-25,105.86,B
2033-04-26,97.03,B
2033-04-27,113.08,B
2033-04-28,24.03,A
2033-04-29,120.46,C
2033-04-30,103.79,B
2033-05-01,93.33,B
2033-05-02,161.41,B
2033-05-03,79.75,A
2033-05-04,87.91,A
2033-05-05,159.75,C
2033-05-06,75.04,C
2033-05-07,83.5,C
2033-05-08,95.59,C
2033-05-09,125.22,A
2033-05-10,106.23,C
2033-05-11,61.22,B
2033-05-12,84.03,A
2033-05-13,81.78,A
2033-05-14,97.68,A
2033-05-15,112.78,B
2033-05-16,112.55,A
2033-05-17,46.72,B
2033-05-18,131.92,C
2033-05-19,107.58,A
2033-05-20,141.54,B
2033-05-21,113.33,A
2033-05-22,133.04,C
2033-05-23,114.0,A
2033-05-24,140.39,C
2033-05-25,115.67,A
2033-05-26,97.06,B
2033-05-27,168.17,C
2033-05-28,126.67,A
2033-05-29,117.21,C
2033-05-30,61.71,C
2033-05-31,61.35,B
2033-06-01,108.78,B
2033-06-02,104.37,B
2033-06-03,81.58,B
2033-06-04,104.23,C
2033-06-05,147.66,C
2033-06-06,120.86,A
2033-06-07,65.77,A
2033-06-08,96.65,A
2033-06-09,75.85,B
2033-06-10,88.37,C
2033-06-11,87.59,C
2033-06-12,85.67,A
2033-06-13,62.35,A
2033-06-14,96.17,C
2033-06-15,83.18,B
2033-06-16,12.12,A
2033-06-17,161.59,A
2033-06-18,132.66,C
2033-06-19,88.71,C
2033-06-20,100.56,A
2033-06-21,64.85,A
2033-06-22,150.87,B
2033-06-23,156.92,B
2033-06-24,104.7,B
2033-06-25,130.71,B
2033-06-26,105.26,A
2033-06-27,59.9,A
2033-06-28,87.65,A
2033-06-29,103.96,B
2033-06-30,86.36,A
2033-07-01,93.44,C
2033-07-02,97.26,B
2033-07-03,97.57,C
2033-07-04,101.57,B
2033-07-05,68.18,A
2033-07-06,78.49,B
2033-07-07,61.46,C
2033-07-08,129.33,B
2033-07-09,36.83,C
2033-07-10,134.61,A
2033-07-11,141.59,C
2033-07-12,90.93,A
2033-07-13,21.91,C
2033-07-14,89.16,A
2033-07-15,98.07,C
2033-07-16,69.68,A
2033-07-17,84.54,A
2033-07-18,145.91,A
2033-07-19,119.95,C
2033-07-20,72.26,C
2033-07-21,52.07,A
2033-07-22,90.19,B
2033-07-23,93.6,A
2033-07-24,114.89,B
2033-07-25,83.94,A
2033-07-26,115.34,C
2033-07-27,158.05,B
2033-07-28,124.47,C
2033-07-29,98.56,B
2033-07-30,94.51,B
2033-07-31,89.3,B
2033-08-01,135.42,B
2033-08-02,81.18,C
2033-08-03,101.36,B
2033-08-04,101.54,A
2033-08-05,84.95,A
2033-08-06,58.83,B
2033-08-07,109.69,C
2033-08-08,98.17,C
2033-08-09,115.01,B
2033-08-10,83.99,C
2033-08-11,136.62,C
2033-08-12,73.7,C
2033-08-13,151.36,B
2033-08-14,47.57,A
2033-08-15,113.04,A
2033-08-16,114.26,A
2033-08-17,76.13,A
2033-08-18,112.73,A
2033-08-19,138.39,C
2033-08-20,33.5,C
2033-08-21,115.08,C
2033-08-22,25.2,B
2033-08-23,71.03,C
2033-08-24,148.87,A
2033-08-25,82.55,C
2033-08-26,64.78,A
2033-08-27,94.06,C
2033-08-28,162.04,A
2033-08-29,45.05,C
2033-08-30,68.46,A
2033-08-31,144.92,A
2033-09-01,155.73,B
2033-09-02,96.9,C
2033-09-03,62.83,A
2033-09-04,162.88,C
2033-09-05,147.83,A
2033-09-06,120.37,B
2033-09-07,75.63,B
2033-09-08,98.53,B
2033-09-09,95.19,B
2033-09-10,109.93,A
2033-09-11,143.53,C
2033-09-12,126.38,B
2033-09-13,67.68,B
2033-09-14,141.29,B
2033-09-15,109.39,C
2033-09-16,120.61,A
2033-09-17,144.0,A
2033-09-18,66.65,A
2033-09-19,98.93,A
2033-09-20,84.06,C
2033-09-21,52.96,C
2033-09-22,110.4,C
2033-09-23,175.35,B
2033-09-24,44.8,C
2033-09-25,99.03,A
2033-09-26,119.22,C
2033-09-27,103.69,B
2033-09-28,96.61,A
2033-09-29,61.02,C
2033-09-30,106.98,A
2033-10-01,77.3,A
2033-10-02,34.3,C
2033-10-03,135.84,C
2033-10-04,128.75,B
2033-10-05,101.55,C
2033-10-06,106.87,C
2033-10-07,132.23,C
2033-10-08,106.73,A
2033-10-09,127.12,B
2033-10-10,91.08,A
2033-10-11,139.36,C
2033-10-12,109.62,B
2033-10-13,105.82,C
2033-10-14,61.9,A
2033-10-15,108.61,A
2033-10-16,75.04,C
2033-10-17,80.85,B
2033-10-18,75.54,A
2033-10-19,68.01,C
2033-10-20,163.74,C
2033-10-21,139.94,C
2033-10-22,157.61,B
2033-10-23,63.31,C
2033-10-24,102.14,C
2033-10-25,61.19,A
2033-10-26,79.13,C
2033-10-27,72.46,B
2033-10-28,137.19,C
2033-10-29,88.11,B
2033-10-30,132.05,C
2033-10-31,118.12,C
2033-11-01,169.11,C
2033-11-02,55.62,B
2033-11-03,137.78,A
2033-11-04,134.39,B
2033-11-05,70.8,A
2033-11-06,130.21,B
2033-11-07,110.08,C
2033-11-08,93.18,C
2033-11-09,75.28,A
2033-11-10,78.43,A
2033-11-11,161.49,B
2033-11-12,100.02,B
2033-11-13,123.46,A
2033-11-14,76.27,A
2033-11-15,76.6,B
2033-11-16,132.46,A
2033-11-17,59.51,A
2033-11-18,87.54,A
2033-11-19,101.02,A
2033-11-20,103.34,C
2033-11-21,94.63,C
2033-11-22,111.87,A
2033-11-23,120.79,B
2033-11-24,122.04,C
2033-11-25,70.43,B
2033-11-26,91.48,B
2033-11-27,135.84,C
2033-11-28,126.84,B
2033-11-29,58.81,B
2033-11-30,110.54,A
2033-12-01,40.65,A
2033-12-02,101.41,A
2033-12-03,156.93,C
2033-12-04,71.56,A
2033-12-05,74.97,B
2033-12-06,122.92,B
2033-12-07,53.72,B
2033-12-08,81.02,B
2033-12-09,117.89,B
2033-12-10,80.97,C
2033-12-11,92.92,B
2033-12-12,122.33,C
2033-12-13,112.65,B
2033-12-14,108.0,C
2033-12-15,89.83,C
2033-12-16,111.1,B
2033-12-17,96.29,B
2033-12-18,113.73,B
2033-12-19,85.09,B
2033-12-20,94.52,A
2033-12-21,70.3,A
2033-12-22,98.95,C
2033-12-23,75.67,C
2033-12-24,66.58,A
2033-12-25,107.76,C
2033-12-26,106.38,B
2033-12-27,123.49,C
2033-12-28,87.07,C
2033-12-29,112.24,C
2033-12-30,116.52,A
2033-12-31,113.34,A
2034-01-01,75.23,C
2034-01-02,104.48,B
2034-01-03,110.92,C
2034-01-04,100.22,A
2034-01-05,37.41,B
2034-01-06,104.81,C
2034-01-07,166.38,A
2034-01-08,89.2,B
2034-01-09,78.16,C
2034-01-10,111.2,A
2034-01-11,109.37,B
2034-01-12,135.78,C
2034-01-13,62.26,B
2034-01-14,121.94,B
2034-01-15,117.71,B
2034-01-16,95.74,A
2034-01-17,110.16,C
2034-01-18,130.88,B
2034-01-19,126.17,B
2034-01-20,65.71,B
2034-01-21,124.6,A
2034-01-22,98.06,B
2034-01-23,102.19,A
2034-01-24,102.2,A
2034-01-25,95.49,B
2034-01-26,135.75,B
2034-01-27,97.49,A
2034-01-28,66.41,A
2034-01-29,88.19,B
2034-01-30,105.36,A
2034-01-31,136.88,A
2034-02-01,118.09,B
2034-02-02,74.48,B
2034-02-03,174.56,C
2034-02-04,107.94,B
2034-02-05,138.26,B
2034-02-06,95.45,B
2034-02-07,116.0,C
2034-02-08,125.18,A
2034-02-09,136.55,B
2034-02-10,131.72,B
2034-02-11,126.17,C
2034-02-12,90.55,C
2034-02-13,82.85,C
2034-02-14,109.98,A
2034-02-15,127.99,C
2034-02-16,93.32,A
2034-02-17,131.96,B
2034-02-18,143.58,B
2034-02-19,72.51,B
2034-02-20,74.89,C
2034-02-21,95.79,B
2034-02-22,109.23,C
2034-02-23,84.26,A
2034-02-24,140.57,B
2034-02-25,112.72,A
2034-02-26,101.18,C
2034-02-27,56.92,C
2034-02-28,60.53,B
2034-03-01,108.43,B
2034-03-02,36.02,C
2034-03-03,130.38,B
2034-03-04,95.26,B
2034-03-05,197.29,A
2034-03-06,169.24,C
2034-03-07,94.56,A
2034-03-08,96.81,B
2034-03-09,129.87,A
2034-03-10,151.1,C
2034-03-11,50.86,A
2034-03-12,46.41,A
2034-03-13,81.34,B
2034-03-14,117.48,C
2034-03-15,114.91,B
2034-03-16,132.09,B
2034-03-17,64.01,C
2034-03-18,30.52,A
2034-03-19,125.74,B
2034-03-20,75.15,A
2034-03-21,39.45,C
2034-03-22,104.35,B
2034-03-23,123.8,A
2034-03-24,96.34,A
2034-03-25,86.26,B
2034-03-26,95.22,C
2034-03-27,88.88,A
2034-03-28,59.56,C
2034-03-29,78.56,B
2034-03-30,115.64,A
2034-03-31,109.65,A
2034-04-01,105.04,A
2034-04-02,70.15,C
2034-04-03,94.55,A
2034-04-04,34.69,A
2034-04-05,105.35,B
2034-04-06,142.87,A
2034-04-07,55.78,C
2034-04-08,82.42,B
2034-04-09,109.87,B
2034-04-10,137.59,A
2034-04-11,86.55,B
2034-04-12,79.23,A
2034-04-13,40.93,B
2034-04-14,103.1,C
2034-04-15,161.26,A
2034-04-16,108.32,C
2034-04-17,99.33,A
2034-04-18,109.66,C
2034-04-19,99.67,C
2034-04-20,75.61,B
2034-04-21,124.7,A
2034-04-22,106.77,B
2034-04-23,100.53,C
2034-04-24,70.74,A
2034-04-25,139.11,C
2034-04-26,119.94,C
2034-04-27,83.41,C
2034-04-28,130.05,C
2034-04-29,85.07,B
2034-04-30,76.54,C
2034-05-01,77.21,B
2034-05-02,46.87,C
2034-05-03,114.15,A
2034-05-04,45.07,C
2034-05-05,63.15,C
2034-05-06,37.73,A
2034-05-07,97.42,A
2034-05-08,95.49,C
2034-05-09,90.2,C
2034-05-10,68.72,B
2034-05-11,64.83,A
2034-05-12,113.93,A
2034-05-13,83.48,B
2034-05-14,109.49,C
2034-05-15,73.44,C
2034-05-16,105.43,A
2034-05-17,139.1,B
2034-05-18,117.58,C
2034-05-19,87.63,A
2034-05-20,107.72,C
2034-05-21,92.78,C
2034-05-22,100.24,C
2034-05-23,90.23,A
2034-05-24,95.07,B
2034-05-25,106.36,B
2034-05-26,70.2,C
2034-05-27,69.52,B
2034-05-28,58.34,C
2034-05-29,51.99,A
2034-05-30,63.48,A
2034-05-31,104.78,A
2034-06-01,73.22,C
2034-06-02,115.41,B
2034-06-03,101.04,A
2034-06-04,40.94,C
2034-06-05,119.65,B
2034-06-06,124.32,B
2034-06-07,70.18,B
2034-06-08,112.61,C
2034-06-09,74.31,C
2034-06-10,102.93,C
2034-06-11,62.07,A
2034-06-12,46.21,C
2034-06-13,88.52,A
2034-06-14,96.29,A
2034-06-15,88.07,A
2034-06-16,64.48,C
2034-06-17,88.0,A
2034-06-18,108.08,C
2034-06-19,98.65,C
2034-06-20,105.48,A
2034-06-21,79.9,A
2034-06-22,135.15,A
2034-06-23,64.71,C
2034-06-24,114.63,C
2034-06-25,135.46,A
2034-06-26,114.43,A
2034-06-27,93.92,B
2034-06-28,84.15,A
2034-06-29,131.08,B
2034-06-30,57.31,B
2034-07-01,70.9,C
2034-07-02,88.07,C
2034-07-03,137.13,B
2034-07-04,111.39,B
2034-07-05,70.96,A
2034-07-06,90.69,B
2034-07-07,81.57,A
2034-07-08,78.71,C
2034-07-09,129.37,A
2034-07-10,58.85,C
2034-07-11,148.26,B
2034-07-12,124.85,B
2034-07-13,132.73,B
2034-07-14,86.11,A
2034-07-15,95.5,C
2034-07-16,57.01,C
2034-07-17,84.26,C
2034-07-18,114.81,C
2034-07-19,138.88,C
2034-07-20,66.79,A
2034-07-21,89.03,C
2034-07-22,95.46,B
2034-07-23,58.13,C
2034-07-24,117.74,B
2034-07-25,78.28,A
2034-07-26,94.14,A
2034-07-27,86.44,B
2034-07-28,76.32,C
2034-07-29,99.79,A
2034-07-30,93.53,A
2034-07-31,88.84,A
2034-08-01,78.15,C
2034-08-02,109.59,B
2034-08-03,149.61,B
2034-08-04,104.2,C
2034-08-05,129.88,A
2034-08-06,58.29,C
2034-08-07,104.73,C
2034-08-08,67.08,B
2034-08-09,55.69,C
2034-08-10,77.82,C
2034-08-11,135.76,A
2034-08-12,128.5,A
2034-08-13,107.29,B
2034-08-14,54.02,A
2034-08-15,94.11,C
2034-08-16,109.05,C
2034-08-17,105.27,C
2034-08-18,44.69,B
2034-08-19,73.29,A
2034-08-20,98.08,C
2034-08-21,116.09,A
2034-08-22,34.12,A
2034-08-23,95.14,A
2034-08-24,104.29,A
2034-08-25,125.49,B
2034-08-26,120.79,B
2034-08-27,117.48,B
2034-08-28,76.0,C
2034-08-29,42.76,C
2034-08-30,88.21,C
2034-08-31,130.05,B
2034-09-01,141.8,A
2034-09-02,121.32,B
2034-09-03,112.88,C
2034-09-04,111.39,C
2034-09-05,83.32,A
2034-09-06,96.1,B
2034-09-07,150.07,A
2034-09-08,71.72,B
2034-09-09,148.44,C
2034-09-10,90.34,A
2034-09-11,139.76,A
2034-09-12,57.8,B
2034-09-13,117.58,B
2034-09-14,77.9,B
2034-09-15,57.54,C
2034-09-16,106.56,B
2034-09-17,130.28,C
2034-09-18,74.65,B
2034-09-19,55.03,B
2034-09-20,97.24,C
2034-09-21,97.38,B
2034-09-22,116.45,C
2034-09-23,128.49,C
2034-09-24,98.22,B
2034-09-25,155.67,B
2034-09-26,106.73,A
2034-09-27,89.61,B
2034-09-28,51.65,A
2034-09-29,92.12,B
2034-09-30,89.91,C
2034-10-01,128.81,C
2034-10-02,113.87,A
2034-10-03,64.62,B
2034-10-04,105.17,A
2034-10-05,120.15,B
2034-10-06,145.84,B
2034-10-07,126.63,C
2034-10-08,122.83,A
2034-10-09,101.8,C
2034-10-10,111.65,C
2034-10-11,137.49,C
2034-10-12,60.02,A
2034-10-13,89.52,B
2034-10-14,76.82,B
2034-10-15,111.37,A
2034-10-16,136.91,B
2034-10-17,82.09,A
2034-10-18,28.29,A
2034-10-19,87.63,B
2034-10-20,127.4,C
2034-10-21,116.13,A
2034-10-22,112.85,A
2034-10-23,91.6,A
2034-10-24,61.63,A
2034-10-25,115.46,B
2034-10-26,74.97,B
2034-10-27,165.52,A
2034-10-28,117.12,C
2034-10-29,82.58,B
2034-10-30,81.66,C
2034-10-31,97.21,C
2034-11-01,92.81,A
2034-11-02,133.51,B
2034-11-03,105.59,A
2034-11-04,78.05,A
2034-11-05,156.71,C
2034-11-06,101.48,C
2034-11-07,123.07,C
2034-11-08,81.75,A
2034-11-09,110.89,A
2034-11-10,109.33,A
2034-11-11,43.31,A
2034-11-12,160.46,A
2034-11-13,138.72,A
2034-11-14,88.08,A
2034-11-15,66.93,A
2034-11-16,110.43,B
2034-11-17,102.6,B
2034-11-18,110.67,B
2034-11-19,105.75,B
2034-11-20,115.19,A
2034-11-21,143.42,A
2034-11-22,117.04,C
2034-11-23,68.51,C
2034-11-24,140.88,B
2034-11-25,149.22,C
2034-11-26,194.56,B
2034-11-27,66.3,A
2034-11-28,107.29,A
2034-11-29,37.54,C
2034-11-30,116.59,A
2034-12-01,83.55,A
2034-12-02,157.7,B
2034-12-03,76.76,C
2034-12-04,49.32,A
2034-12-05,85.86,C
2034-12-06,40.74,A
2034-12-07,122.53,B
2034-12-08,38.05,A
2034-12-09,100.85,C
2034-12-10,37.67,C
2034-12-11,90.39,B
2034-12-12,149.3,A
2034-12-13,110.82,B
2034-12-14,74.1,C
2034-12-15,99.06,B
2034-12-16,100.54,A
2034-12-17,114.18,A
2034-12-18,58.99,B
2034-12-19,117.78,C
2034-12-20,18.87,C
2034-12-21,81.1,B
2034-12-22,85.35,A
2034-12-23,119.0,A
2034-12-24,100.68,A
2034-12-25,57.83,A
2034-12-26,70.45,C
2034-12-27,105.96,A
2034-12-28,97.63,B
2034-12-29,82.63,B
2034-12-30,85.57,B
2034-12-31,120.89,B
2035-01-01,84.47,A
2035-01-02,97.36,C
2035-01-03,165.1,A
2035-01-04,57.29,C
2035-01-05,44.44,A
2035-01-06,123.51,C
2035-01-07,79.64,B
2035-01-08,90.45,B
2035-01-09,76.57,C
2035-01-10,92.21,C
2035-01-11,147.96,C
2035-01-12,124.06,B
2035-01-13,125.87,B
2035-01-14,57.51,A
2035-01-15,65.47,C
2035-01-16,14.83,C
2035-01-17,75.72,C
2035-01-18,128.7,B
2035-01-19,115.51,C
2035-01-20,93.35,C
2035-01-21,99.33,B
2035-01-22,79.33,C
2035-01-23,106.14,A
2035-01-24,122.98,B
2035-01-25,51.52,B
2035-01-26,108.99,B
2035-01-27,127.97,A
2035-01-28,87.55,B
2035-01-29,121.02,B
2035-01-30,184.44,B
2035-01-31,96.87,B
2035-02-01,111.8,A
2035-02-02,77.86,C
2035-02-03,105.44,C
2035-02-04,113.81,B
2035-02-05,105.85,A
2035-02-06,69.41,B
2035-02-07,106.19,C
2035-02-08,143.87,C
2035-02-09,113.46,A
2035-02-10,128.86,C
2035-02-11,92.89,B
2035-02-12,122.5,A
2035-02-13,108.38,C
2035-02-14,71.47,B
2035-02-15,109.56,A
2035-02-16,70.25,B
2035-02-17,81.01,C
2035-02-18,105.68,B
2035-02-19,108.92,A
2035-02-20,64.32,B
2035-02-21,131.62,C
2035-02-22,117.16,A
2035-02-23,78.86,A
2035-02-24,53.87,C
2035-02-25,95.65,B
2035-02-26,86.62,B
2035-02-27,103.39,C
2035-02-28,89.11,A
2035-03-01,39.78,C
2035-03-02,88.35,A
2035-03-03,90.83,A
2035-03-04,119.01,B
2035-03-05,79.05,C
2035-03-06,150.36,A
2035-03-07,42.65,B
2035-03-08,139.34,C
2035-03-09,93.27,C
2035-03-10,125.79,A
2035-03-11,102.39,A
2035-03-12,134.19,A
2035-03-13,88.38,C
2035-03-14,67.04,A
2035-03-15,142.62,C
2035-03-16,96.6,B
2035-03-17,106.65,C
2035-03-18,137.04,A
2035-03-19,69.04,C
2035-03-20,117.86,A
2035-03-21,82.3,B
2035-03-22,96.03,A
2035-03-23,102.17,A
2035-03-24,54.99,A
2035-03-25,76.63,A
2035-03-26,140.53,A
2035-03-27,126.59,A
2035-03-28,98.12,B
2035-03-29,107.29,B
2035-03-30,91.17,C
2035-03-31,140.7,A
2035-04-01,93.99,C
2035-04-02,106.96,B
2035-04-03,134.38,B
2035-04-04,76.68,A
2035-04-05,146.76,A
2035-04-06,54.09,C
2035-04-07,148.89,B
2035-04-08,88.82,C
2035-04-09,92.72,C
2035-04-10,43.3,C
2035-04-11,86.36,A
2035-04-12,68.81,B
2035-04-13,81.77,C
2035-04-14,95.88,B
2035-04-15,128.73,B
2035-04-16,106.23,C
2035-04-17,109.23,C
2035-04-18,67.13,A
2035-04-19,73.58,B
2035-04-20,153.96,C
2035-04-21,77.94,C
2035-04-22,149.69,B
2035-04-23,92.71,C
2035-04-24,100.07,B
2035-04-25,132.11,C
2035-04-26,138.16,B
2035-04-27,96.68,C
2035-04-28,88.35,C
2035-04-29,98.32,A
2035-04-30,110.14,B
2035-05-01,126.74,A
2035-05-02,100.13,C
2035-05-03,118.57,B
2035-05-04,149.15,B
2035-05-05,111.44,C
2035-05-06,109.35,C
2035-05-07,54.43,C
2035-05-08,21.08,C
2035-05-09,111.65,B
2035-05-10,97.16,A
2035-05-11,84.56,B
2035-05-12,107.31,B
2035-05-13,98.19,B
2035-05-14,133.83,B
2035-05-15,102.58,C
2035-05-16,77.59,A
2035-05-17,129.7,B
2035-05-18,93.49,B
2035-05-19,115.8,A
2035-05-20,85.68,C
2035-05-21,80.51,B
2035-05-22,102.53,C
2035-05-23,100.38,B
2035-05-24,114.39,B
2035-05-25,91.94,A
2035-05-26,44.91,A
2035-05-27,87.02,C
2035-05-28,123.81,B
2035-05-29,91.08,C
2035-05-30,92.61,A
2035-05-31,129.59,A
2035-06-01,78.64,A
2035-06-02,53.72,A
2035-06-03,95.18,A
2035-06-04,85.47,C
2035-06-05,71.01,C
2035-06-06,94.74,A
2035-06-07,114.75,B
2035-06-08,99.72,B
2035-06-09,109.21,B
2035-06-10,154.4,B
2035-06-11,89.71,B
2035-06-12,108.35,A
2035-06-13,84.72,C
2035-06-14,74.61,A
2035-06-15,82.45,B
2035-06-16,85.64,B
2035-06-17,110.78,C
2035-06-18,170.47,A
2035-06-19,78.52,C
2035-06-20,87.5,B
2035-06-21,98.11,B
2035-06-22,111.87,C
2035-06-23,107.91,B
2035-06-24,138.52,A
2035-06-25,27.21,B
2035-06-26,28.39,B
2035-06-27,85.12,A
2035-06-28,132.92,B
2035-06-29,53.03,C
2035-06-30,9.77,C
2035-07-01,117.14,B
2035-07-02,107.31,A
2035-07-03,67.29,A
2035-07-04,70.43,C
2035-07-05,70.91,B
2035-07-06,103.32,A
2035-07-07,81.21,A
2035-07-08,114.82,C
2035-07-09,14.36,C
2035-07-10,74.29,A
2035-07-11,66.22,C
2035-07-12,92.31,A
2035-07-13,83.86,B
2035-07-14,123.94,C
2035-07-15,44.22,C
2035-07-16,101.36,B
2035-07-17,116.75,A
2035-07-18,155.7,A
2035-07-19,85.55,C
2035-07-20,109.73,A
2035-07-21,45.88,A
2035-07-22,28.24,B
2035-07-23,125.34,B
2035-07-24,99.83,B
2035-07-25,135.36,C
2035-07-26,58.8,B
2035-07-27,115.6,B
2035-07-28,156.06,A
2035-07-29,112.59,B
2035-07-30,108.3,C
2035-07-31,100.26,B
2035-08-01,52.87,C
2035-08-02,140.67,A
2035-08-03,81.67,A
2035-08-04,115.13,B
2035-08-05,53.11,A
2035-08-06,64.62,B
2035-08-07,143.05,A
2035-08-08,89.58,C
2035-08-09,143.6,A
2035-08-10,97.65,B
2035-08-11,44.74,B
2035-08-12,126.76,B
2035-08-13,82.3,C
2035-08-14,120.96,A
2035-08-15,100.76,B
2035-08-16,53.71,C
2035-08-17,104.93,C
2035-08-18,165.89,C
2035-08-19,114.79,A
2035-08-20,98.27,A
2035-08-21,132.26,A
2035-08-22,106.32,C
2035-08-23,106.29,A
2035-08-24,67.25,B
2035-08-25,107.02,C
2035-08-26,131.06,A
2035-08-27,74.92,C
2035-08-28,142.73,C
2035-08-29,97.25,A
2035-08-30,73.37,C
2035-08-31,129.93,A
2035-09-01,91.4,A
2035-09-02,122.79,A
2035-09-03,91.25,C
2035-09-04,98.21,C
2035-09-05,114.81,A
2035-09-06,108.73,C
2035-09-07,40.13,A
2035-09-08,66.36,B
2035-09-09,127.88,C
2035-09-10,96.04,C
2035-09-11,84.29,B
2035-09-12,59.24,B
2035-09-13,67.61,B
2035-09-14,39.35,A
2035-09-15,96.96,A
2035-09-16,82.05,B
2035-09-17,71.0,A
2035-09-18,97.96,C
2035-09-19,110.92,B
2035-09-20,162.8,C
2035-09-21,54.64,A
2035-09-22,134.78,B
2035-09-23,129.19,A
2035-09-24,76.63,C
2035-09-25,106.1,A
2035-09-26,86.27,C
2035-09-27,105.57,A
2035-09-28,145.27,C
2035-09-29,118.17,A
2035-09-30,115.26,B
2035-10-01,87.09,B
2035-10-02,55.48,A
2035-10-03,93.75,C
2035-10-04,136.2,A
2035-10-05,54.18,B
2035-10-06,68.35,C
2035-10-07,103.79,A
2035-10-08,70.46,A
2035-10-09,76.22,C
2035-10-10,87.29,A
2035-10-11,59.03,B
2035-10-12,70.78,B
2035-10-13,121.1,C
2035-10-14,60.78,A
2035-10-15,52.06,A
2035-10-16,81.1,B
2035-10-17,121.96,C
2035-10-18,94.09,C
2035-10-19,86.06,C
2035-10-20,101.33,B
2035-10-21,47.63,B
2035-10-22,114.84,C
2035-10-23,111.5,C
2035-10-24,87.72,A
2035-10-25,130.18,A
2035-10-26,66.18,C
2035-10-27,133.84,B
2035-10-28,104.63,B
2035-10-29,89.9,A
2035-10-30,99.4,C
2035-10-31,96.78,B
2035-11-01,136.61,B
2035-11-02,67.7,B
2035-11-03,77.12,B
2035-11-04,131.09,A
2035-11-05,82.13,B
2035-11-06,127.02,B
2035-11-07,150.98,C
2035-11-08,87.95,C
2035-11-09,120.44,A
2035-11-10,65.39,A
2035-11-11,122.64,A
2035-11-12,105.98,A
2035-11-13,114.19,B
2035-11-14,80.63,C
2035-11-15,50.17,B
2035-11-16,115.1,B
2035-11-17,38.1,B
2035-11-18,90.48,C
2035-11-19,128.24,A
2035-11-20,105.76,A
2035-11-21,158.45,C
2035-11-22,130.75,C
2035-11-23,80.38,A
2035-11-24,100.82,B
2035-11-25,98.58,A
2035-11-26,52.53,C
2035-11-27,71.79,A
2035-11-28,83.18,A
2035-11-29,38.91,A
2035-11-30,101.42,C
2035-12-01,92.53,C
2035-12-02,49.81,A
2035-12-03,112.13,A
2035-12-04,121.48,B
2035-12-05,106.81,B
2035-12-06,90.28,A
2035-12-07,117.74,C
2035-12-08,120.41,B
2035-12-09,109.22,A
2035-12-10,75.26,B
2035-12-11,108.3,B
2035-12-12,135.86,B
2035-12-13,82.1,C
2035-12-14,162.94,A
2035-12-15,84.23,C
2035-12-16,100.97,C
2035-12-17,79.83,B
2035-12-18,106.7,A
2035-12-19,47.73,B
2035-12-20,83.62,C
2035-12-21,67.24,C
2035-12-22,104.06,A
2035-12-23,129.83,C
2035-12-24,119.38,C
2035-12-25,28.85,A
2035-12-26,161.26,B
2035-12-27,108.86,A
2035-12-28,76.71,C
2035-12-29,104.71,A
2035-12-30,105.29,B
2035-12-31,79.99,C
2036-01-01,105.94,B
2036-01-02,81.78,B
2036-01-03,87.87,A
2036-01-04,79.41,B
2036-01-05,57.02,A
2036-01-06,104.38,A
2036-01-07,117.56,B
2036-01-08,115.45,A
2036-01-09,124.6,A
2036-01-10,109.73,B
2036-01-11,84.02,C
2036-01-12,125.19,A
2036-01-13,59.27,A
2036-01-14,72.98,B
2036-01-15,71.07,A
2036-01-16,132.4,B
2036-01-17,128.28,C
2036-01-18,114.62,C
2036-01-19,132.16,B
2036-01-20,73.62,C
2036-01-21,110.46,B
2036-01-22,96.24,A
2036-01-23,125.41,C
2036-01-24,96.54,A
2036-01-25,88.45,A
2036-01-26,116.23,B
2036-01-27,126.29,C
2036-01-28,79.95,C
2036-01-29,98.17,A
2036-01-30,68.25,A
2036-01-31,25.06,B
2036-02-01,106.01,C
2036-02-02,118.27,B
2036-02-03,107.86,B
2036-02-04,41.51,B
2036-02-05,89.47,C
2036-02-06,33.45,B
2036-02-07,141.09,C
2036-02-08,67.42,B
2036-02-09,137.66,A
2036-02-10,128.38,B
2036-02-11,72.98,B
2036-02-12,66.02,C
2036-02-13,110.51,A
2036-02-14,74.67,A
2036-02-15,103.07,C
2036-02-16,85.52,C
2036-02-17,46.57,A
2036-02-18,33.09,C
2036-02-19,106.8,A
2036-02-20,83.15,B
2036-02-21,83.41,B
2036-02-22,120.88,A
2036-02-23,135.2,A
2036-02-24,91.82,B
2036-02-25,89.78,B
2036-02-26,115.0,A
2036-02-27,59.29,C
2036-02-28,106.69,C
2036-02-29,116.56,A
2036-03-01,120.41,C
2036-03-02,41.9,B
2036-03-03,77.16,B
2036-03-04,73.65,C
2036-03-05,103.0,A
2036-03-06,114.42,B
2036-03-07,144.9,B
2036-03-08,85.15,B
2036-03-09,85.07,C
2036-03-10,134.49,A
2036-03-11,108.9,C
2036-03-12,138.03,A
2036-03-13,108.66,A
2036-03-14,65.29,C
2036-03-15,60.01,B
2036-03-16,53.45,A
2036-03-17,119.86,C
2036-03-18,135.66,B
2036-03-19,155.48,A
2036-03-20,125.29,A
2036-03-21,134.89,B
2036-03-22,82.6,A
2036-03-23,65.31,C
2036-03-24,125.5,A
2036-03-25,112.53,A
2036-03-26,103.22,B
2036-03-27,111.62,C
2036-03-28,109.7,B
2036-03-29,138.51,C
2036-03-30,138.85,B
2036-03-31,116.33,A
2036-04-01,125.37,C
2036-04-02,115.42,C
2036-04-03,86.16,A
2036-04-04,122.7,B
2036-04-05,58.17,C
2036-04-06,131.62,A
2036-04-07,64.16,A
2036-04-08,108.57,C
2036-04-09,74.65,C
2036-04-10,116.6,C
2036-04-11,98.98,A
2036-04-12,52.16,B
2036-04-13,112.62,B
2036-04-14,82.66,C
2036-04-15,126.43,A
2036-04-16,100.04,A
2036-04-17,111.86,C
2036-04-18,71.27,A
2036-04-19,82.32,A
2036-04-20,86.24,A
2036-04-21,125.7,C
2036-04-22,150.32,C
2036-04-23,75.84,C
2036-04-24,132.31,B
2036-04-25,34.91,B
2036-04-26,64.17,C
2036-04-27,71.24,A
2036-04-28,59.42,A
2036-04-29,52.49,C
2036-04-30,112.39,A
2036-05-01,93.58,A
2036-05-02,111.38,B
2036-05-03,120.71,A
2036-05-04,107.97,A
2036-05-05,119.73,C
2036-05-06,97.91,B
2036-05-07,64.39,B
2036-05-08,131.73,A
2036-05-09,82.33,C
2036-05-10,71.93,B
2036-05-11,94.66,B
2036-05-12,72.62,A
2036-05-13,87.48,B
2036-05-14,110.32,C
2036-05-15,110.45,C
2036-05-16,73.07,B
2036-05-17,85.37,C
2036-05-18,132.23,C
2036-05-19,114.89,A
2036-05-20,162.25,B
2036-05-21,135.4,B
2036-05-22,134.33,A
2036-05-23,58.36,A
2036-05-24,122.92,A
2036-05-25,98.41,A
2036-05-26,83.47,C
2036-05-27,105.04,C
2036-05-28,128.36,A
2036-05-29,115.02,B
2036-05-30,73.65,B
2036-05-31,122.27,B
2036-06-01,80.87,A
2036-06-02,106.5,C
2036-06-03,90.9,C
2036-06-04,93.43,C
2036-06-05,122.99,C
2036-06-06,68.79,B
2036-06-07,158.36,B
2036-06-08,45.37,C
2036-06-09,82.07,A
2036-06-10,111.11,C
2036-06-11,117.53,C
2036-06-12,96.99,A
2036-06-13,98.28,C
2036-06-14,112.32,B
2036-06-15,93.07,A
2036-06-16,141.8,A
2036-06-17,125.18,C
2036-06-18,126.18,B
2036-06-19,71.02,B
2036-06-20,97.62,B
2036-06-21,90.99,C
2036-06-22,69.83,C
2036-06-23,110.8,B
2036-06-24,83.87,C
2036-06-25,119.66,B
2036-06-26,67.89,C
2036-06-27,151.57,A
2036-06-28,73.11,A
2036-06-29,106.58,C
2036-06-30,123.76,C
2036-07-01,82.88,B
2036-07-02,125.57,A
2036-07-03,63.12,B
2036-07-04,143.56,C
2036-07-05,100.42,B
2036-07-06,82.11,B
2036-07-07,91.02,C
2036-07-08,124.27,A
2036-07-09,82.47,B
2036-07-10,77.52,C
2036-07-11,93.35,B
2036-07-12,64.69,C
2036-07-13,154.62,C
2036-07-14,83.01,C
2036-07-15,124.95,A
2036-07-16,106.28,A
2036-07-17,70.46,C
2036-07-18,99.82,A
2036-07-19,123.52,B
2036-07-20,94.35,A
2036-07-21,135.79,A
2036-07-22,130.65,B
2036-07-23,127.57,B
2036-07-24,86.21,B
2036-07-25,67.6,C
2036-07-26,81.59,B
2036-07-27,105.89,B
2036-07-28,116.37,B
2036-07-29,34.85,C
2036-07-30,99.76,A
2036-07-31,104.74,C
2036-08-01,91.67,A
2036-08-02,138.74,B
2036-08-03,112.65,C
2036-08-04,139.31,C
2036-08-05,93.52,A
2036-08-06,128.12,C
2036-08-07,149.05,C
2036-08-08,139.35,A
2036-08-09,67.24,C
2036-08-10,92.39,C
2036-08-11,89.65,B
2036-08-12,114.68,B
2036-08-13,120.59,B
2036-08-14,74.83,B
2036-08-15,102.04,B
2036-08-16,39.4,C
2036-08-17,143.26,C
2036-08-18,84.63,B
2036-08-19,101.68,A
2036-08-20,90.71,B
2036-08-21,134.38,C
2036-08-22,14.87,A
2036-08-23,99.5,B
2036-08-24,96.83,A
2036-08-25,86.39,B
2036-08-26,151.79,C
2036-08-27,73.38,B
2036-08-28,123.15,A
2036-08-29,110.69,C
2036-08-30,137.85,B
2036-08-31,79.98,B
2036-09-01,78.01,B
2036-09-02,132.88,B
2036-09-03,112.76,C
2036-09-04,53.65,C
2036-09-05,83.82,B
2036-09-06,111.59,C
2036-09-07,125.13,A
2036-09-08,79.62,C
2036-09-09,12.57,A
2036-09-10,115.7,A
2036-09-11,63.39,C
2036-09-12,39.48,B
2036-09-13,15.03,C
2036-09-14,124.42,A
2036-09-15,103.86,A
2036-09-16,93.23,C
2036-09-17,141.67,A
2036-09-18,119.12,C
2036-09-19,148.49,C
2036-09-20,58.22,B
2036-09-21,98.96,A
2036-09-22,51.5,C
2036-09-23,93.12,A
2036-09-24,92.54,C
2036-09-25,118.8,C
2036-09-26,101.37,B
2036-09-27,118.7,C
2036-09-28,121.1,B
2036-09-29,164.65,A
2036-09-30,90.54,B
2036-10-01,71.77,B
2036-10-02,107.38,A
2036-10-03,75.93,B
2036-10-04,144.46,B
2036-10-05,91.5,C
2036-10-06,83.03,B
2036-10-07,85.86,C
2036-10-08,121.55,B
2036-10-09,95.27,A
2036-10-10,80.75,A
2036-10-11,149.96,B
2036-10-12,74.31,C
2036-10-13,101.07,B
2036-10-14,119.57,C
2036-10-15,51.06,B
2036-10-16,114.41,A
2036-10-17,68.35,A
2036-10-18,124.68,C
2036-10-19,71.12,B
2036-10-20,120.78,C
2036-10-21,133.17,A
2036-10-22,125.84,C
2036-10-23,107.54,B
2036-10-24,93.06,C
2036-10-25,82.71,B
2036-10-26,144.13,B
2036-10-27,79.02,A
2036-10-28,68.89,B
2036-10-29,119.96,B
2036-10-30,76.12,B
2036-10-31,71.5,C
2036-11-01,56.69,A
2036-11-02,85.51,C
2036-11-03,97.52,C
2036-11-04,107.29,A
2036-11-05,128.54,C
2036-11-06,115.81,C
2036-11-07,94.27,B
2036-11-08,94.05,C
2036-11-09,115.3,A
2036-11-10,138.18,C
2036-11-11,103.79,B
2036-11-12,85.93,C
2036-11-13,70.21,A
2036-11-14,56.64,B
2036-11-15,119.99,C
2036-11-16,117.34,A
2036-11-17,117.32,A
2036-11-18,96.14,C
2036-11-19,142.21,C
2036-11-20,85.48,B
2036-11-21,128.66,C
2036-11-22,98.84,B
2036-11-23,81.75,C
2036-11-24,117.78,A
2036-11-25,112.29,A
2036-11-26,110.71,A
2036-11-27,102.44,A
2036-11-28,98.86,B
2036-11-29,171.65,A
2036-11-30,135.53,B
2036-12-01,121.68,B
2036-12-02,103.35,B
2036-12-03,130.21,C
2036-12-04,115.54,B
2036-12-05,100.79,C
2036-12-06,90.89,B
2036-12-07,97.57,C
2036-12-08,4.89,C
2036-12-09,106.32,A
2036-12-10,85.35,B
2036-12-11,147.78,C
2036-12-12,100.58,A
2036-12-13,64.33,B
2036-12-14,98.5,A
2036-12-15,68.62,C
2036-12-16,114.2,A
2036-12-17,108.73,A
2036-12-18,128.74,B
2036-12-19,76.27,C
2036-12-20,101.02,B
2036-12-21,59.05,B
2036-12-22,121.04,A
2036-12-23,115.57,C
2036-12-24,114.87,B
2036-12-25,100.05,C
2036-12-26,112.44,B
2036-12-27,148.13,A
2036-12-28,91.45,B
2036-12-29,104.16,B
2036-12-30,67.42,B
2036-12-31,73.95,C
2037-01-01,102.93,A
2037-01-02,133.12,C
2037-01-03,92.31,B
2037-01-04,92.92,A
2037-01-05,87.79,C
2037-01-06,83.96,B
2037-01-07,108.85,A
2037-01-08,55.08,A
2037-01-09,138.43,B
2037-01-10,80.04,A
2037-01-11,121.66,A
2037-01-12,61.9,C
2037-01-13,120.07,A
2037-01-14,120.16,C
2037-01-15,111.53,C
2037-01-16,70.17,C
2037-01-17,107.49,C
2037-01-18,64.51,A
2037-01-19,92.42,A
2037-01-20,130.88,C
2037-01-21,123.9,A
2037-01-22,100.46,C
2037-01-23,70.76,C
2037-01-24,119.37,C
2037-01-25,100.58,B
2037-01-26,30.39,A
2037-01-27,123.48,A
2037-01-28,99.27,C
2037-01-29,136.07,A
2037-01-30,67.14,C
2037-01-31,4.7,A
2037-02-01,108.37,C
2037-02-02,108.75,C
2037-02-03,49.28,B
2037-02-04,70.65,C
2037-02-05,182.66,B
2037-02-06,90.83,A
2037-02-07,121.93,A
2037-02-08,80.48,B
2037-02-09,37.29,A
2037-02-10,94.89,B
2037-02-11,92.03,A
2037-02-12,88.0,B
2037-02-13,96.56,B
2037-02-14,107.93,C
2037-02-15,95.5,A
2037-02-16,62.08,C
2037-02-17,114.59,B
2037-02-18,41.52,C
2037-02-19,38.49,B
2037-02-20,122.18,A
2037-02-21,115.21,A
2037-02-22,98.28,A
2037-02-23,58.14,B
2037-02-24,78.76,C
2037-02-25,92.12,A
2037-02-26,52.12,B
2037-02-27,77.83,C
2037-02-28,119.57,B
2037-03-01,142.82,B
2037-03-02,87.42,A
2037-03-03,122.69,B
2037-03-04,42.58,B
2037-03-05,119.78,C
2037-03-06,75.88,A
2037-03-07,109.22,B
2037-03-08,92.57,B
2037-03-09,139.61,C
2037-03-10,73.77,B
2037-03-11,118.83,B
2037-03-12,91.61,B
2037-03-13,95.25,B
2037-03-14,94.48,B
2037-03-15,74.63,C
2037-03-16,99.58,B
2037-03-17,98.12,B
2037-03-18,107.24,B
2037-03-19,102.54,B
2037-03-20,123.94,A
2037-03-21,127.71,A
2037-03-22,68.48,A
2037-03-23,126.06,A
2037-03-24,64.41,C
2037-03-25,117.64,A
2037-03-26,118.59,C
2037-03-27,42.66,A
2037-03-28,102.79,C
2037-03-29,85.95,A
2037-03-30,77.24,B
2037-03-31,140.53,A
2037-04-01,80.39,A
2037-04-02,75.81,A
2037-04-03,102.27,B
2037-04-04,120.57,C
2037-04-05,95.75,C
2037-04-06,134.99,B
2037-04-07,115.93,A
2037-04-08,34.65,A
2037-04-09,84.12,A
2037-04-10,83.14,C
2037-04-11,114.38,C
2037-04-12,21.26,B
2037-04-13,176.47,B
2037-04-14,115.93,C
2037-04-15,104.47,A
2037-04-16,135.45,A
2037-04-17,170.31,A
2037-04-18,126.12,A
2037-04-19,128.46,B
2037-04-20,103.92,B
2037-04-21,95.75,C
2037-04-22,85.73,B
2037-04-23,75.22,A
2037-04-24,79.39,C
2037-04-25,59.48,B
2037-04-26,97.72,B
2037-04-27,49.46,B
2037-04-28,105.73,B
2037-04-29,98.91,B
2037-04-30,104.41,A
2037-05-01,84.34,A
2037-05-02,192.31,A
2037-05-03,140.8,B
2037-05-04,62.66,C
2037-05-05,106.99,A
2037-05-06,81.76,A
2037-05-07,69.57,B
2037-05-08,59.8,C
2037-05-09,112.97,A
2037-05-10,79.56,A
2037-05-11,129.03,C
2037-05-12,129.81,B
2037-05-13,141.22,C
2037-05-14,74.43,B
2037-05-15,114.26,A
2037-05-16,118.97,C
2037-05-17,85.77,C
2037-05-18,76.85,A
2037-05-19,148.29,A
2037-05-20,106.82,B
2037-05-21,119.08,A
2037-05-22,75.34,A
2037-05-23,63.42,C
2037-05-24,82.92,B
2037-05-25,96.28,A
2037-05-26,68.07,C
2037-05-27,99.7,A
2037-05-28,49.34,A
2037-05-29,66.4,A
2037-05-30,120.13,A
2037-05-31,118.11,B
2037-06-01,88.24,C
2037-06-02,69.47,C
2037-06-03,69.18,A
2037-06-04,88.8,C
2037-06-05,119.34,A
2037-06-06,127.85,C
2037-06-07,85.1,B
2037-06-08,65.4,B
2037-06-09,108.04,C
2037-06-10,75.29,A
2037-06-11,84.45,A
2037-06-12,139.76,A
2037-06-13,124.81,A
2037-06-14,104.74,B
2037-06-15,61.84,A
2037-06-16,106.34,A
2037-06-17,131.88,A
2037-06-18,88.73,A
2037-06-19,140.53,B
2037-06-20,138.78,C
2037-06-21,149.91,C
2037-06-22,109.16,A
2037-06-23,117.62,A
2037-06-24,142.43,C
2037-06-25,113.42,B
2037-06-26,84.96,B
2037-06-27,86.54,B
2037-06-28,107.16,B
2037-06-29,61.28,B
2037-06-30,95.7,B
2037-07-01,132.9,B
2037-07-02,34.26,B
2037-07-03,146.03,B
2037-07-04,133.67,A
2037-07-05,86.21,B
2037-07-06,116.3,A
2037-07-07,100.09,C
2037-07-08,136.9,A
2037-07-09,129.48,C
2037-07-10,58.0,A
2037-07-11,117.62,C
2037-07-12,93.83,A
2037-07-13,102.48,A
2037-07-14,78.15,C
2037-07-15,152.44,B
2037-07-16,131.2,B
2037-07-17,77.81,B
2037-07-18,59.96,B
2037-07-19,88.43,B
2037-07-20,113.04,A
2037-07-21,107.21,B
2037-07-22,137.59,A
2037-07-23,123.7,C
2037-07-24,129.09,B
2037-07-25,109.81,B
2037-07-26,58.32,B
2037-07-27,94.13,B
2037-07-28,131.8,A
2037-07-29,98.45,A
2037-07-30,115.09,A
2037-07-31,90.44,C
2037-08-01,69.11,A
2037-08-02,96.3,B
2037-08-03,109.27,C
2037-08-04,80.33,C
2037-08-05,92.93,B
2037-08-06,71.82,B
2037-08-07,133.58,B
2037-08-08,91.17,B
2037-08-09,143.58,A
2037-08-10,145.86,C
2037-08-11,97.57,B
2037-08-12,69.75,A
2037-08-13,37.42,B
2037-08-14,55.84,A
2037-08-15,58.78,B
2037-08-16,141.35,B
2037-08-17,103.47,B
2037-08-18,111.69,A
2037-08-19,33.39,B
2037-08-20,64.06,A
2037-08-21,126.61,A
2037-08-22,108.6,C
2037-08-23,95.58,A
2037-08-24,116.95,A
2037-08-25,149.07,A
2037-08-26,93.37,A
2037-08-27,102.08,A
2037-08-28,105.78,C
2037-08-29,171.76,B
2037-08-30,37.02,C
2037-08-31,120.5,C
2037-09-01,96.56,B
2037-09-02,117.0,A
2037-09-03,80.28,B
2037-09-04,98.53,A
2037-09-05,121.34,B
2037-09-06,193.39,C
2037-09-07,124.24,A
2037-09-08,74.56,C
2037-09-09,87.29,A
2037-09-10,86.4,B
2037-09-11,46.13,B
2037-09-12,90.1,B
2037-09-13,121.98,B
2037-09-14,61.77,B
2037-09-15,131.45,C
2037-09-16,114.63,C
2037-09-17,77.97,A
2037-09-18,95.75,C
2037-09-19,147.95,A
2037-09-20,122.01,A
2037-09-21,100.26,C
2037-09-22,92.87,C
2037-09-23,102.3,A
2037-09-24,84.65,C
2037-09-25,36.86,C
2037-09-26,161.27,C
2037-09-27,104.66,B
2037-09-28,111.94,B
2037-09-29,98.26,A
2037-09-30,77.95,C
2037-10-01,94.42,A
2037-10-02,52.02,C
2037-10-03,168.31,B
2037-10-04,62.96,C
2037-10-05,69.04,C
2037-10-06,86.79,C
2037-10-07,138.64,B
2037-10-08,100.44,B
2037-10-09,62.92,B
2037-10-10,137.63,A
2037-10-11,121.93,C
2037-10-12,116.15,B
2037-10-13,121.47,A
2037-10-14,171.83,B
2037-10-15,165.55,C
2037-10-16,79.4,A
2037-10-17,148.72,B
2037-10-18,111.5,B
2037-10-19,114.97,A
2037-10-20,49.23,A
2037-10-21,94.89,A
2037-10-22,75.62,A
2037-10-23,124.84,A
2037-10-24,132.13,B
2037-10-25,37.77,A
2037-10-26,99.34,A
2037-10-27,133.66,C
2037-10-28,94.4,B
2037-10-29,121.87,C
2037-10-30,110.53,B
2037-10-31,110.25,B
2037-11-01,91.86,B
2037-11-02,63.5,A
2037-11-03,66.35,C
2037-11-04,121.28,B
2037-11-05,100.58,B
2037-11-06,95.84,B
2037-11-07,87.28,C
2037-11-08,90.84,B
2037-11-09,131.41,C
2037-11-10,89.94,C
2037-11-11,59.77,A
2037-11-12,122.31,B
2037-11-13,93.85,B
2037-11-14,100.94,C
2037-11-15,148.23,C
2037-11-16,99.38,A
2037-11-17,108.68,B
2037-11-18,64.63,C
2037-11-19,93.41,B
2037-11-20,118.29,B
2037-11-21,53.34,B
2037-11-22,106.03,C
2037-11-23,103.02,B
2037-11-24,101.24,C
2037-11-25,131.11,A
2037-11-26,110.03,A
2037-11-27,144.34,C
2037-11-28,70.19,B
2037-11-29,104.08,A
2037-11-30,102.53,A
2037-12-01,67.92,C
2037-12-02,128.69,C
2037-12-03,169.74,C
2037-12-04,99.49,C
2037-12-05,115.15,C
2037-12-06,102.54,C
2037-12-07,78.59,C
2037-12-08,45.38,B
2037-12-09,78.83,B
2037-12-10,58.24,B
2037-12-11,151.12,B
2037-12-12,96.55,A
2037-12-13,89.62,B
2037-12-14,126.67,A
2037-12-15,66.35,A
2037-12-16,76.99,A
2037-12-17,102.04,C
2037-12-18,130.86,A
2037-12-19,115.05,A
2037-12-20,107.45,A
2037-12-21,102.34,C
2037-12-22,112.01,B
2037-12-23,106.05,B
2037-12-24,69.1,A
2037-12-25,101.38,A
2037-12-26,67.79,B
2037-12-27,102.43,B
2037-12-28,85.58,B
2037-12-29,176.18,B
2037-12-30,98.8,A
2037-12-31,57.87,A
2038-01-01,95.97,A
2038-01-02,115.58,B
2038-01-03,150.2,C
2038-01-04,82.32,B
2038-01-05,52.53,B
2038-01-06,124.51,C
2038-01-07,58.58,C
2038-01-08,86.92,B
2038-01-09,92.27,C
2038-01-10,97.91,C
2038-01-11,120.67,C
2038-01-12,79.49,B
2038-01-13,91.79,B
2038-01-14,99.57,C
2038-01-15,104.49,A
2038-01-16,102.93,C
2038-01-17,113.08,C
2038-01-18,125.32,B
2038-01-19,111.78,A
2038-01-20,113.61,C
2038-01-21,52.53,A
2038-01-22,101.51,B
2038-01-23,114.39,A
2038-01-24,121.87,B
2038-01-25,79.23,C
2038-01-26,80.23,B
2038-01-27,82.78,A
2038-01-28,116.09,A
2038-01-29,131.02,A
2038-01-30,87.78,C
2038-01-31,127.24,A
2038-02-01,103.7,A
2038-02-02,96.27,A
2038-02-03,93.66,B
2038-02-04,122.4,A
2038-02-05,109.77,B
2038-02-06,168.27,A
2038-02-07,74.43,B
2038-02-08,115.54,B
2038-02-09,84.12,C
2038-02-10,73.92,C
2038-02-11,99.67,B
2038-02-12,104.12,B
2038-02-13,187.55,C
2038-02-14,94.48,B
2038-02-15,66.09,C
2038-02-16,108.19,C
2038-02-17,37.76,A
2038-02-18,97.24,A
2038-02-19,100.48,C
2038-02-20,123.84,C
2038-02-21,129.21,B
2038-02-22,84.89,C
2038-02-23,95.48,C
2038-02-24,77.02,A
2038-02-25,66.16,C
2038-02-26,109.3,B
2038-02-27,61.79,C
2038-02-28,87.56,A
2038-03-01,70.14,A
2038-03-02,116.49,A
2038-03-03,154.04,A
2038-03-04,60.95,C
2038-03-05,44.41,B
2038-03-06,96.25,B
2038-03-07,100.47,B
2038-03-08,106.56,C
2038-03-09,86.61,C
2038-03-10,93.98,C
2038-03-11,111.03,B
2038-03-12,131.21,C
2038-03-13,41.35,C
2038-03-14,88.93,A
2038-03-15,106.41,A
2038-03-16,72.46,B
2038-03-17,151.94,A
2038-03-18,80.94,A
2038-03-19,98.76,A
2038-03-20,116.68,C
2038-03-21,82.26,A
2038-03-22,44.6,A
2038-03-23,87.14,A
2038-03-24,130.88,C
2038-03-25,89.89,B
2038-03-26,74.61,C
2038-03-27,127.77,C
2038-03-28,90.08,A
2038-03-29,84.7,B
2038-03-30,104.48,A
2038-03-31,123.12,A
2038-04-01,171.45,B
2038-04-02,124.82,B
2038-04-03,134.8,C
2038-04-04,84.82,B
2038-04-05,69.22,C
2038-04-06,101.87,C
2038-04-07,103.22,A
2038-04-08,67.64,A
2038-04-09,94.74,A
2038-04-10,111.98,B
2038-04-11,110.81,B
2038-04-12,115.13,C
2038-04-13,108.73,C
2038-04-14,84.79,C
2038-04-15,84.58,B
2038-04-16,64.03,C
2038-04-17,153.54,C
2038-04-18,100.4,B
2038-04-19,93.25,A
2038-04-20,72.14,B
2038-04-21,188.07,C
2038-04-22,67.8,A
2038-04-23,87.13,B
2038-04-24,112.48,A
2038-04-25,118.01,A
2038-04-26,128.74,C
2038-04-27,154.23,B
2038-04-28,115.18,C
2038-04-29,68.32,B
2038-04-30,92.29,B
2038-05-01,109.23,B
2038-05-02,162.05,A
2038-05-03,58.07,A
2038-05-04,136.08,C
2038-05-05,105.4,B
2038-05-06,125.13,B
2038-05-07,96.94,A
2038-05-08,74.73,B
2038-05-09,122.53,C
2038-05-10,119.63,B
2038-05-11,129.16,B
2038-05-12,99.6,A
2038-05-13,82.01,B
2038-05-14,53.24,C
2038-05-15,124.58,C
2038-05-16,112.72,C
2038-05-17,71.03,C
2038-05-18,83.84,A
2038-05-19,86.0,B
2038-05-20,55.51,A
2038-05-21,75.47,C
2038-05-22,65.99,B
2038-05-23,87.63,A
2038-05-24,77.84,B
2038-05-25,51.19,C
2038-05-26,160.68,C
2038-05-27,137.58,B
2038-05-28,76.63,B
2038-05-29,108.17,C
2038-05-30,112.52,A
2038-05-31,135.73,C
2038-06-01,75.44,B
2038-06-02,132.76,A
2038-06-03,83.56,C
2038-06-04,59.92,B
2038-06-05,121.72,B
2038-06-06,138.72,B
2038-06-07,177.64,A
2038-06-08,64.01,A
2038-06-09,120.97,A
2038-06-10,87.71,A
2038-06-11,123.5,B
2038-06-12,55.89,B
2038-06-13,62.12,A
2038-06-14,76.72,A
2038-06-15,102.37,B
2038-06-16,80.77,C
2038-06-17,119.77,C
2038-06-18,130.81,B
2038-06-19,117.99,A
2038-06-20,148.56,A
2038-06-21,112.47,B
2038-06-22,63.49,C
2038-06-23,140.9,B
2038-06-24,81.85,B
2038-06-25,127.31,B
2038-06-26,147.21,A
2038-06-27,113.7,A
2038-06-28,55.12,A
2038-06-29,72.45,C
2038-06-30,139.68,B
2038-07-01,94.93,B
2038-07-02,102.39,C
2038-07-03,122.76,A
2038-07-04,120.89,B
2038-07-05,108.36,C
2038-07-06,93.74,A
2038-07-07,90.62,A
2038-07-08,125.74,B
2038-07-09,116.61,A
2038-07-10,92.28,C
2038-07-11,132.46,A
2038-07-12,96.13,B
2038-07-13,74.45,C
2038-07-14,77.23,A
2038-07-15,70.64,A
2038-07-16,91.83,A
2038-07-17,137.01,C
2038-07-18,75.85,A
2038-07-19,105.65,B
2038-07-20,80.3,C
2038-07-21,92.19,C
2038-07-22,54.7,A
2038-07-23,93.38,C
2038-07-24,94.19,B
2038-07-25,116.21,C
2038-07-26,106.7,B
2038-07-27,64.05,A
2038-07-28,132.53,C
2038-07-29,106.31,B
2038-07-30,105.95,A
2038-07-31,99.72,B
2038-08-01,84.47,C
2038-08-02,106.53,C
2038-08-03,114.27,B
2038-08-04,47.59,A
2038-08-05,83.84,A
2038-08-06,92.35,A
2038-08-07,107.69,A
2038-08-08,135.8,B
2038-08-09,76.85,C
2038-08-10,85.06,B
2038-08-11,84.62,C
2038-08-12,75.95,C
2038-08-13,89.58,B
2038-08-14,123.12,B
2038-08-15,68.38,A
2038-08-16,112.83,C
2038-08-17,48.94,A
2038-08-18,83.16,A
2038-08-19,90.12,B
2038-08-20,150.25,A
2038-08-21,91.76,A
2038-08-22,33.96,A
2038-08-23,40.03,B
2038-08-24,101.02,A
2038-08-25,118.41,A
2038-08-26,111.26,B
2038-08-27,105.03,B
2038-08-28,134.87,B
2038-08-29,83.81,C
2038-08-30,126.85,B
2038-08-31,51.04,C
2038-09-01,72.85,B
2038-09-02,45.79,B
2038-09-03,91.75,B
2038-09-04,98.61,C
2038-09-05,103.42,A
2038-09-06,101.86,B
2038-09-07,72.17,C
2038-09-08,94.29,B
2038-09-09,55.08,A
2038-09-10,140.9,C
2038-09-11,90.89,B
2038-09-12,85.33,B
2038-09-13,147.67,B
2038-09-14,62.73,A
2038-09-15,66.23,C
2038-09-16,67.11,B
2038-09-17,97.96,B
2038-09-18,112.95,C
2038-09-19,122.19,A
2038-09-20,177.16,A
2038-09-21,32.52,A
2038-09-22,118.26,A
2038-09-23,83.29,A
2038-09-24,82.41,B
2038-09-25,81.41,B
2038-09-26,72.74,B
2038-09-27,150.54,C
2038-09-28,125.27,A
2038-09-29,100.7,C
2038-09-30,140.53,A
2038-10-01,54.62,A
2038-10-02,60.08,C
2038-10-03,34.66,B
2038-10-04,79.02,C
2038-10-05,104.85,B
2038-10-06,103.09,A
2038-10-07,104.48,B
2038-10-08,57.67,A
2038-10-09,124.09,A
2038-10-10,107.19,B
2038-10-11,121.99,A
2038-10-12,43.22,B
2038-10-13,109.34,C
2038-10-14,64.37,A
2038-10-15,88.08,A
2038-10-16,116.03,C
2038-10-17,102.73,A
2038-10-18,74.45,C
2038-10-19,100.19,C
2038-10-20,54.05,A
2038-10-21,134.51,C
2038-10-22,93.84,C
2038-10-23,133.56,C
2038-10-24,84.2,A
2038-10-25,84.29,C
2038-10-26,77.07,B
2038-10-27,126.29,B
2038-10-28,78.72,C
2038-10-29,119.34,B
2038-10-30,88.53,A
2038-10-31,40.72,C
2038-11-01,82.89,A
2038-11-02,64.63,B
2038-11-03,101.02,B
2038-11-04,64.29,B
2038-11-05,127.78,B
2038-11-06,97.27,A
2038-11-07,94.67,C
2038-11-08,119.68,A
2038-11-09,128.92,C
2038-11-10,99.4,B
2038-11-11,76.86,B
2038-11-12,46.78,B
2038-11-13,79.42,B
2038-11-14,81.48,A
2038-11-15,103.21,C
2038-11-16,89.65,C
2038-11-17,120.64,B
2038-11-18,81.61,A
2038-11-19,98.29,B
2038-11-20,135.52,C
2038-11-21,110.25,C
2038-11-22,98.2,A
2038-11-23,81.94,C
2038-11-24,81.0,A
2038-11-25,74.48,B
2038-11-26,83.77,B
2038-11-27,122.67,A
2038-11-28,93.34,B
2038-11-29,59.51,A
2038-11-30,153.05,B
2038-12-01,109.6,B
2038-12-02,57.79,C
2038-12-03,86.74,C
2038-12-04,103.33,A
2038-12-05,151.14,B
2038-12-06,66.76,B
2038-12-07,129.28,B
2038-12-08,141.98,C
2038-12-09,81.14,A
2038-12-10,91.67,A
2038-12-11,68.91,A
2038-12-12,93.9,B
2038-12-13,98.9,A
2038-12-14,119.79,C
2038-12-15,132.71,C
2038-12-16,77.56,A
2038-12-17,40.98,B
2038-12-18,99.36,C
2038-12-19,108.81,C
2038-12-20,107.48,B
2038-12-21,90.2,B
2038-12-22,103.74,A
2038-12-23,145.46,C
2038-12-24,111.45,C
2038-12-25,88.8,A
2038-12-26,84.99,A
2038-12-27,94.9,C
2038-12-28,149.1,C
2038-12-29,94.5,B
2038-12-30,99.41,B
2038-12-31,85.53,C
2039-01-01,132.97,C
2039-01-02,158.8,B
2039-01-03,113.52,A
2039-01-04,102.48,A
2039-01-05,101.1,C
2039-01-06,28.47,C
2039-01-07,122.35,B
2039-01-08,104.02,C
2039-01-09,112.85,A
2039-01-10,78.66,B
2039-01-11,88.47,B
2039-01-12,87.67,A
2039-01-13,130.65,A
2039-01-14,88.64,B
2039-01-15,162.48,C
2039-01-16,112.93,A
2039-01-17,61.73,B
2039-01-18,96.14,C
2039-01-19,80.45,C
2039-01-20,34.17,B
2039-01-21,147.98,A
2039-01-22,121.09,C
2039-01-23,116.0,A
2039-01-24,104.96,B
2039-01-25,58.54,A
2039-01-26,85.49,C
2039-01-27,125.25,B
2039-01-28,68.46,C
2039-01-29,24.05,C
2039-01-30,93.19,B
2039-01-31,95.75,B
2039-02-01,104.07,A
2039-02-02,121.86,B
2039-02-03,119.13,A
2039-02-04,107.76,B
2039-02-05,135.87,C
2039-02-06,54.43,A
2039-02-07,115.14,B
2039-02-08,118.8,A
2039-02-09,62.81,A
2039-02-10,54.91,A
2039-02-11,77.62,C
2039-02-12,80.15,B
2039-02-13,122.6,B
2039-02-14,81.56,A
2039-02-15,74.31,B
2039-02-16,81.65,C
2039-02-17,133.56,A
2039-02-18,100.39,C
2039-02-19,94.62,C
2039-02-20,96.7,B
2039-02-21,138.76,B
2039-02-22,77.52,A
2039-02-23,139.47,C
2039-02-24,106.31,B
2039-02-25,95.19,A
2039-02-26,175.03,A
2039-02-27,68.33,B
2039-02-28,89.39,C
2039-03-01,63.03,A
2039-03-02,128.57,B
2039-03-03,150.68,A
2039-03-04,150.1,B
2039-03-05,136.72,A
2039-03-06,94.07,C
2039-03-07,89.04,A
2039-03-08,49.86,B
2039-03-09,161.08,B
2039-03-10,114.07,B
2039-03-11,85.24,C
2039-03-12,139.84,B
2039-03-13,70.82,B
2039-03-14,112.2,C
2039-03-15,94.82,B
2039-03-16,119.16,B
2039-03-17,98.09,C
2039-03-18,56.47,A
2039-03-19,102.27,B
2039-03-20,131.47,A
2039-03-21,99.12,C
2039-03-22,123.25,B
2039-03-23,68.63,B
2039-03-24,99.21,C
2039-03-25,81.26,B
2039-03-26,122.41,A
2039-03-27,100.94,A
2039-03-28,120.7,C
2039-03-29,132.63,B
2039-03-30,120.75,B
2039-03-31,108.19,C
2039-04-01,70.05,A
2039-04-02,99.92,A
2039-04-03,84.01,C
2039-04-04,89.68,B
2039-04-05,88.76,A
2039-04-06,70.6,B
2039-04-07,41.28,A
2039-04-08,79.96,B
2039-04-09,105.8,A
2039-04-10,59.55,B
2039-04-11,100.47,C
2039-04-12,157.01,A
2039-04-13,130.69,C
2039-04-14,149.31,A
2039-04-15,111.73,A
2039-04-16,135.03,A
2039-04-17,78.28,B
2039-04-18,80.67,A
2039-04-19,17.83,C
2039-04-20,135.58,B
2039-04-21,146.21,B
2039-04-22,83.4,A
2039-04-23,92.38,B
2039-04-24,77.38,A
2039-04-25,111.48,C
2039-04-26,133.75,B
2039-04-27,47.72,B
2039-04-28,112.28,C
2039-04-29,91.7,C
2039-04-30,103.4,A
2039-05-01,113.77,B
2039-05-02,94.35,C
2039-05-03,96.89,B
2039-05-04,93.58,C
2039-05-05,129.11,B
2039-05-06,112.56,C
2039-05-07,106.62,B
2039-05-08,61.87,A
2039-05-09,88.64,B
2039-05-10,126.96,A
2039-05-11,101.13,A
2039-05-12,119.79,A
2039-05-13,153.0,B
2039-05-14,131.16,B
2039-05-15,102.79,A
2039-05-16,84.67,C
2039-05-17,23.39,B
2039-05-18,94.95,B
2039-05-19,90.22,B
2039-05-20,111.71,B
2039-05-21,95.38,A
2039-05-22,90.39,C
2039-05-23,46.62,C
2039-05-24,71.8,C
2039-05-25,114.3,C
2039-05-26,151.48,A
2039-05-27,97.07,B
2039-05-28,109.83,B
2039-05-29,128.57,A
2039-05-30,75.67,B
2039-05-31,111.0,A
2039-06-01,77.79,C
2039-06-02,125.94,A
2039-06-03,104.54,B
2039-06-04,108.95,B
2039-06-05,76.52,B
2039-06-06,121.21,C
2039-06-07,162.54,A
2039-06-08,69.76,A
2039-06-09,89.81,B
2039-06-10,106.95,A
2039-06-11,123.59,B
2039-06-12,83.21,A
2039-06-13,15.01,C
2039-06-14,140.78,C
2039-06-15,122.82,A
2039-06-16,47.71,C
2039-06-17,81.77,C
2039-06-18,147.05,C
2039-06-19,143.43,B
2039-06-20,128.16,A
2039-06-21,94.72,A
2039-06-22,102.49,B
2039-06-23,96.07,B
2039-06-24,133.85,C
2039-06-25,73.72,A
2039-06-26,75.83,B
2039-06-27,130.31,C
2039-06-28,98.67,A
2039-06-29,141.35,C
2039-06-30,79.85,A
2039-07-01,98.36,C
2039-07-02,72.33,A
2039-07-03,100.22,A
2039-07-04,55.64,A
2039-07-05,85.37,A
2039-07-06,56.91,C
2039-07-07,120.82,B
2039-07-08,121.5,A
2039-07-09,48.09,A
2039-07-10,140.02,A
2039-07-11,174.92,A
2039-07-12,101.38,C
2039-07-13,80.77,B
2039-07-14,184.73,B
2039-07-15,60.43,B
2039-07-16,100.6,C
2039-07-17,105.39,B
2039-07-18,140.35,C
2039-07-19,110.79,C
2039-07-20,85.04,A
2039-07-21,71.46,C
2039-07-22,89.75,B
2039-07-23,69.79,B
2039-07-24,101.43,B
2039-07-25,83.07,A
2039-07-26,55.18,A
2039-07-27,137.65,A
2039-07-28,85.07,C
2039-07-29,61.96,B
2039-07-30,94.95,C
2039-07-31,102.05,C
2039-08-01,47.61,B
2039-08-02,109.95,A
2039-08-03,68.59,B
2039-08-04,151.33,C
2039-08-05,115.72,C
2039-08-06,88.0,A
2039-08-07,77.64,C
2039-08-08,96.85,C
2039-08-09,118.38,B
2039-08-10,99.24,A
2039-08-11,93.49,B
2039-08-12,101.75,B
2039-08-13,81.88,B
2039-08-14,89.7,C
2039-08-15,37.43,C
2039-08-16,129.21,A
2039-08-17,108.96,C
2039-08-18,76.8,A
2039-08-19,156.96,B
2039-08-20,102.61,B
2039-08-21,68.92,C
2039-08-22,125.7,C
2039-08-23,111.94,A
2039-08-24,111.84,C
2039-08-25,87.23,C
2039-08-26,113.51,C
2039-08-27,92.15,B
2039-08-28,116.68,C
2039-08-29,42.85,C
2039-08-30,86.98,B
2039-08-31,151.66,B
2039-09-01,76.99,B
2039-09-02,81.88,B
2039-09-03,86.56,C
2039-09-04,63.56,C
2039-09-05,33.75,C
2039-09-06,93.08,A
2039-09-07,104.08,B
2039-09-08,98.42,C
2039-09-09,95.65,C
2039-09-10,101.29,C
2039-09-11,130.69,B
2039-09-12,94.29,B
2039-09-13,82.78,B
2039-09-14,91.57,C
2039-09-15,117.38,B
2039-09-16,142.36,C
2039-09-17,81.16,A
2039-09-18,102.97,B
2039-09-19,110.15,B
2039-09-20,70.22,C
2039-09-21,100.61,B
2039-09-22,56.03,B
2039-09-23,46.93,C
2039-09-24,111.0,C
2039-09-25,88.48,C
2039-09-26,106.07,A
2039-09-27,99.46,C
2039-09-28,70.71,B
2039-09-29,98.64,A
2039-09-30,63.01,C
2039-10-01,141.73,A
2039-10-02,59.32,A
2039-10-03,94.13,B
2039-10-04,68.73,A
2039-10-05,117.11,C
2039-10-06,90.06,A
2039-10-07,84.44,A
2039-10-08,125.6,B
2039-10-09,100.88,B
2039-10-10,115.5,C
2039-10-11,70.56,B
2039-10-12,57.86,C
2039-10-13,103.22,C
2039-10-14,90.43,B
2039-10-15,118.79,A
2039-10-16,97.6,B
2039-10-17,106.15,A
2039-10-18,51.3,A
2039-10-19,156.65,C
2039-10-20,95.07,C
2039-10-21,120.93,B
2039-10-22,74.92,A
2039-10-23,109.24,B
2039-10-24,60.91,C
2039-10-25,113.56,A
2039-10-26,89.32,B
2039-10-27,100.88,B
2039-10-28,120.94,C
2039-10-29,63.02,C
2039-10-30,79.47,C
2039-10-31,113.72,C
2039-11-01,162.95,B
2039-11-02,95.86,C
2039-11-03,117.73,C
2039-11-04,96.79,B
2039-11-05,158.27,B
2039-11-06,47.58,A
2039-11-07,102.84,B
2039-11-08,51.23,A
2039-11-09,24.8,C
2039-11-10,108.92,A
2039-11-11,104.18,A
2039-11-12,91.45,C
2039-11-13,96.81,C
2039-11-14,192.95,C
2039-11-15,92.25,A
2039-11-16,115.3,B
2039-11-17,80.52,B
2039-11-18,108.86,B
2039-11-19,92.85,C
2039-11-20,87.68,C
2039-11-21,154.57,B
2039-11-22,135.32,C
2039-11-23,104.1,A
2039-11-24,30.87,A
2039-11-25,89.17,C
2039-11-26,181.09,B
2039-11-27,93.08,B
2039-11-28,133.1,A
2039-11-29,143.26,C
2039-11-30,106.57,B
2039-12-01,74.75,B
2039-12-02,26.94,C
2039-12-03,120.52,C
2039-12-04,75.56,B
2039-12-05,105.28,C
2039-12-06,62.51,C
2039-12-07,92.04,C
2039-12-08,101.01,B
2039-12-09,108.06,A
2039-12-10,66.56,C
2039-12-11,135.91,B
2039-12-12,82.09,A
2039-12-13,49.77,B
2039-12-14,96.73,B
2039-12-15,101.03,B
2039-12-16,104.82,A
2039-12-17,140.86,B
2039-12-18,92.91,A
2039-12-19,96.41,C
2039-12-20,66.9,A
2039-12-21,124.62,B
2039-12-22,105.53,A
2039-12-23,87.4,B
2039-12-24,42.86,C
2039-12-25,143.33,A
2039-12-26,63.17,C
2039-12-27,82.26,A
2039-12-28,110.75,B
2039-12-29,64.1,A
2039-12-30,55.19,C
2039-12-31,36.28,A
2040-01-01,121.63,C
2040-01-02,83.64,C
2040-01-03,187.82,B
2040-01-04,70.17,A
2040-01-05,72.36,C
2040-01-06,83.04,B
2040-01-07,107.29,B
2040-01-08,85.89,B
2040-01-09,118.37,C
2040-01-10,70.64,A
2040-01-11,58.55,C
2040-01-12,46.05,B
2040-01-13,33.29,C
2040-01-14,84.46,C
2040-01-15,90.97,B
2040-01-16,81.06,A
2040-01-17,137.3,B
2040-01-18,136.84,B
2040-01-19,89.96,C
2040-01-20,46.18,B
2040-01-21,83.0,C
2040-01-22,82.01,B
2040-01-23,80.29,B
2040-01-24,79.07,A
2040-01-25,80.4,B
2040-01-26,88.4,A
2040-01-27,80.93,B
2040-01-28,106.25,B
2040-01-29,110.81,B
2040-01-30,72.68,A
2040-01-31,138.13,C
2040-02-01,70.6,A
2040-02-02,51.47,B
2040-02-03,111.42,C
2040-02-04,75.48,B
2040-02-05,109.64,A
2040-02-06,27.89,A
2040-02-07,141.77,C
2040-02-08,120.17,A
2040-02-09,54.99,B
2040-02-10,55.64,C
2040-02-11,150.17,B
2040-02-12,77.88,C
2040-02-13,74.97,C
2040-02-14,32.02,B
2040-02-15,89.48,A
2040-02-16,93.17,B
2040-02-17,121.46,A
2040-02-18,58.03,A
2040-02-19,54.39,B
2040-02-20,113.53,C
2040-02-21,130.2,B
2040-02-22,52.87,C
2040-02-23,132.45,A
2040-02-24,101.13,C
2040-02-25,132.75,B
2040-02-26,97.89,C
2040-02-27,24.33,A
2040-02-28,95.44,A
2040-02-29,131.8,C
2040-03-01,73.19,A
2040-03-02,88.36,A
2040-03-03,94.72,C
2040-03-04,13.01,A
2040-03-05,150.14,C
2040-03-06,72.96,C
2040-03-07,115.17,A
2040-03-08,97.4,C
2040-03-09,137.73,A
2040-03-10,112.18,A
2040-03-11,92.95,A
2040-03-12,73.19,B
2040-03-13,123.94,A
2040-03-14,43.8,C
2040-03-15,119.38,C
2040-03-16,67.03,C
2040-03-17,137.57,B
2040-03-18,104.84,B
2040-03-19,119.32,C
2040-03-20,102.46,C
2040-03-21,47.43,B
2040-03-22,74.18,C
2040-03-23,120.17,A
2040-03-24,105.0,A
2040-03-25,135.43,C
2040-03-26,112.1,A
2040-03-27,71.0,C
2040-03-28,76.12,B
2040-03-29,103.09,C
2040-03-30,30.58,B
2040-03-31,150.2,A
2040-04-01,113.25,A
2040-04-02,136.81,A
2040-04-03,52.58,C
2040-04-04,137.34,B
2040-04-05,166.94,B
2040-04-06,60.37,A
2040-04-07,123.14,A
2040-04-08,100.03,B
2040-04-09,53.67,C
2040-04-10,114.79,A
2040-04-11,125.01,C
2040-04-12,81.97,A
2040-04-13,134.98,A
2040-04-14,50.0,C
2040-04-15,55.16,C
2040-04-16,104.1,C
2040-04-17,100.76,C
2040-04-18,78.5,B
2040-04-19,124.29,B
2040-04-20,115.93,A
2040-04-21,125.74,A
2040-04-22,96.31,A
2040-04-23,118.08,B
2040-04-24,86.63,C
2040-04-25,161.61,A
2040-04-26,75.62,B
2040-04-27,163.16,A
2040-04-28,101.05,B
2040-04-29,58.73,C
2040-04-30,141.04,B
2040-05-01,103.78,C
2040-05-02,125.53,A
2040-05-03,136.72,C
2040-05-04,110.14,B
2040-05-05,82.23,A
2040-05-06,129.27,A
2040-05-07,100.02,B
2040-05-08,120.58,C
2040-05-09,35.39,B
2040-05-10,113.41,C
2040-05-11,80.84,C
2040-05-12,149.6,A
2040-05-13,118.69,C
2040-05-14,107.2,A
2040-05-15,114.59,B
2040-05-16,70.05,B
2040-05-17,148.7,A
2040-05-18,159.74,A
2040-05-19,111.79,C
2040-05-20,147.71,A
2040-05-21,82.93,B
2040-05-22,76.09,A
2040-05-23,101.23,A
2040-05-24,113.05,A
2040-05-25,88.18,C
2040-05-26,116.13,A
2040-05-27,109.19,C
2040-05-28,70.05,C
2040-05-29,115.56,A
2040-05-30,125.91,C
2040-05-31,105.14,A
2040-06-01,134.58,B
2040-06-02,63.48,B
2040-06-03,114.04,C
2040-06-04,64.89,B
2040-06-05,66.58,A
2040-06-06,81.07,C
2040-06-07,71.74,A
2040-06-08,83.56,C
2040-06-09,93.58,C
2040-06-10,125.11,C
2040-06-11,90.37,C
2040-06-12,52.43,B
2040-06-13,134.2,B
2040-06-14,74.89,B
2040-06-15,98.24,C
2040-06-16,113.4,B
2040-06-17,105.99,C
2040-06-18,132.83,B
2040-06-19,114.37,C
2040-06-20,74.16,B
2040-06-21,165.36,B
2040-06-22,105.83,B
2040-06-23,95.58,C
2040-06-24,128.92,C
2040-06-25,107.48,B
2040-06-26,111.98,C
2040-06-27,174.77,A
2040-06-28,91.75,B
2040-06-29,90.47,B
2040-06-30,103.5,A
2040-07-01,132.15,C
2040-07-02,54.8,C
2040-07-03,41.03,C
2040-07-04,82.39,B
2040-07-05,50.98,B
2040-07-06,87.26,B
2040-07-07,115.07,B
2040-07-08,73.28,B
2040-07-09,57.2,C
2040-07-10,102.08,A
2040-07-11,125.88,C
2040-07-12,49.15,C
2040-07-13,124.61,B
2040-07-14,124.52,A
2040-07-15,128.62,A
2040-07-16,136.55,C
2040-07-17,166.06,A
2040-07-18,89.32,C
2040-07-19,103.29,C
2040-07-20,87.51,C
2040-07-21,100.06,C
2040-07-22,145.99,B
2040-07-23,59.65,A
2040-07-24,74.17,B
2040-07-25,141.32,B
2040-07-26,130.6,B
2040-07-27,89.11,C
2040-07-28,125.63,C
2040-07-29,110.24,B
2040-07-30,145.01,B
2040-07-31,102.24,A
2040-08-01,130.77,B
2040-08-02,179.26,C
2040-08-03,117.94,B
2040-08-04,101.48,A
2040-08-05,121.67,A
2040-08-06,89.04,B
2040-08-07,171.64,A
2040-08-08,105.4,A
2040-08-09,91.81,C
2040-08-10,74.93,A
2040-08-11,132.92,A
2040-08-12,126.02,B
2040-08-13,85.08,C
2040-08-14,117.29,A
2040-08-15,54.74,B
2040-08-16,105.55,B
2040-08-17,113.58,B
2040-08-18,151.22,B
2040-08-19,76.88,B
2040-08-20,61.65,B
2040-08-21,73.14,A
2040-08-22,59.9,C
2040-08-23,140.1,C
2040-08-24,146.46,B
2040-08-25,109.61,B
2040-08-26,136.64,C
2040-08-27,104.02,A
2040-08-28,97.15,B
2040-08-29,90.01,C
2040-08-30,90.69,C
2040-08-31,53.57,B
2040-09-01,107.93,C
2040-09-02,98.79,A
2040-09-03,122.36,C
2040-09-04,102.81,B
2040-09-05,95.12,B
2040-09-06,73.86,C
2040-09-07,102.16,C
2040-09-08,122.15,B
2040-09-09,65.7,A
2040-09-10,102.62,C
2040-09-11,106.99,A
2040-09-12,78.46,A
2040-09-13,82.36,B
2040-09-14,106.92,B
2040-09-15,66.0,B
2040-09-16,110.5,A
2040-09-17,73.14,B
2040-09-18,131.89,B
2040-09-19,170.13,A
2040-09-20,74.18,A
2040-09-21,116.03,A
2040-09-22,78.2,A
2040-09-23,131.23,C
2040-09-24,62.04,B
2040-09-25,114.42,C
2040-09-26,73.81,A
2040-09-27,93.79,A
2040-09-28,69.07,A
2040-09-29,124.67,A
2040-09-30,74.89,A
2040-10-01,116.56,B
2040-10-02,89.68,C
2040-10-03,96.57,C
2040-10-04,104.43,B
2040-10-05,77.15,B
2040-10-06,106.31,B
2040-10-07,145.91,C
2040-10-08,76.66,C
2040-10-09,135.15,A
2040-10-10,144.94,C
2040-10-11,75.52,C
2040-10-12,56.35,B
2040-10-13,109.06,C
2040-10-14,83.58,A
2040-10-15,156.06,C
2040-10-16,101.04,B
2040-10-17,75.93,B
2040-10-18,65.59,A
2040-10-19,132.7,A
2040-10-20,82.86,C
2040-10-21,101.31,A
2040-10-22,128.67,A
2040-10-23,85.47,B
2040-10-24,98.62,B
2040-10-25,99.5,C
2040-10-26,120.5,C
2040-10-27,117.72,C
2040-10-28,93.4,C
2040-10-29,101.91,B
2040-10-30,88.85,C
2040-10-31,55.67,C
2040-11-01,107.73,A
2040-11-02,156.77,C
2040-11-03,73.49,A
2040-11-04,76.81,B
2040-11-05,73.88,C
2040-11-06,121.74,A
2040-11-07,113.45,C
2040-11-08,95.24,A
2040-11-09,72.46,C
2040-11-10,102.77,B
2040-11-11,53.06,B
2040-11-12,104.3,A
2040-11-13,126.15,A
2040-11-14,62.46,B
2040-11-15,54.02,C
2040-11-16,147.38,A
2040-11-17,81.65,B
2040-11-18,134.08,A
2040-11-19,95.38,B
2040-11-20,95.14,B
2040-11-21,113.15,A
2040-11-22,73.48,A
2040-11-23,103.97,B
2040-11-24,88.56,C
2040-11-25,60.39,B
2040-11-26,110.12,C
2040-11-27,92.1,C
2040-11-28,95.29,C
2040-11-29,63.7,A
2040-11-30,147.74,B
2040-12-01,126.3,B
2040-12-02,128.92,C
2040-12-03,90.84,C
2040-12-04,48.12,C
2040-12-05,158.59,B
2040-12-06,98.17,C
2040-12-07,77.54,B
2040-12-08,112.5,B
2040-12-09,135.01,A
2040-12-10,99.31,C
2040-12-11,138.36,C
2040-12-12,132.49,C
2040-12-13,67.71,A
2040-12-14,79.79,C
2040-12-15,128.73,B
2040-12-16,18.62,C
2040-12-17,95.52,B
2040-12-18,57.08,B
2040-12-19,85.7,B
2040-12-20,91.04,B
2040-12-21,89.25,C
2040-12-22,83.43,A
2040-12-23,67.09,A
2040-12-24,168.76,B
2040-12-25,156.02,C
2040-12-26,65.14,A
2040-12-27,147.46,C
2040-12-28,88.02,A
2040-12-29,103.88,B
2040-12-30,98.1,B
2040-12-31,80.21,B
2041-01-01,129.52,A
2041-01-02,103.78,C
2041-01-03,85.7,B
2041-01-04,60.86,C
2041-01-05,73.19,C
2041-01-06,50.76,B
2041-01-07,100.37,A
2041-01-08,64.1,B
2041-01-09,92.03,B
2041-01-10,141.1,A
2041-01-11,135.98,C
2041-01-12,99.03,A
2041-01-13,97.39,A
2041-01-14,156.4,B
2041-01-15,115.62,A
2041-01-16,90.17,A
2041-01-17,57.22,A
2041-01-18,111.82,C
2041-01-19,124.88,C
2041-01-20,81.05,B
2041-01-21,108.74,B
2041-01-22,89.09,C
2041-01-23,94.48,A
2041-01-24,25.37,B
2041-01-25,93.35,B
2041-01-26,129.08,A
2041-01-27,81.06,A
2041-01-28,101.58,A
2041-01-29,77.32,A
2041-01-30,110.81,A
2041-01-31,93.5,C
2041-02-01,71.82,A
2041-02-02,139.98,B
2041-02-03,127.87,C
2041-02-04,81.85,B
2041-02-05,114.93,A
2041-02-06,116.12,B
2041-02-07,119.27,A
2041-02-08,49.17,B
2041-02-09,130.2,B
2041-02-10,82.91,A
2041-02-11,89.27,B
2041-02-12,179.14,C
2041-02-13,109.06,B
2041-02-14,88.41,C
2041-02-15,33.84,C
2041-02-16,141.69,B
2041-02-17,59.64,C
2041-02-18,69.9,C
2041-02-19,90.06,B
2041-02-20,142.49,A
2041-02-21,123.2,A
2041-02-22,95.26,C
2041-02-23,109.25,C
2041-02-24,81.99,A
2041-02-25,104.19,C
2041-02-26,102.2,A
2041-02-27,129.06,B
2041-02-28,129.02,C
2041-03-01,114.82,B
2041-03-02,89.13,A
2041-03-03,122.25,A
2041-03-04,97.24,B
2041-03-05,67.54,C
2041-03-06,87.9,B
2041-03-07,102.54,B
2041-03-08,127.31,C
2041-03-09,94.49,A
2041-03-10,63.72,B
2041-03-11,101.7,B
2041-03-12,103.4,A
2041-03-13,105.58,B
2041-03-14,127.6,A
2041-03-15,130.57,C
2041-03-16,82.17,A
2041-03-17,81.52,A
2041-03-18,68.79,B
2041-03-19,120.96,A
2041-03-20,143.46,A
2041-03-21,90.55,B
2041-03-22,92.76,A
2041-03-23,144.0,B
2041-03-24,115.17,C
2041-03-25,113.09,A
2041-03-26,64.27,C
2041-03-27,120.36,A
2041-03-28,72.13,A
2041-03-29,65.98,C
2041-03-30,89.6,C
2041-03-31,82.43,B
2041-04-01,93.35,B
2041-04-02,56.77,C
2041-04-03,128.76,A
2041-04-04,133.83,B
2041-04-05,41.58,A
2041-04-06,129.35,B
2041-04-07,75.68,B
2041-04-08,125.71,B
2041-04-09,97.95,C
2041-04-10,100.86,A
2041-04-11,94.45,C
2041-04-12,136.36,B
2041-04-13,62.81,C
2041-04-14,118.24,C
2041-04-15,133.04,C
2041-04-16,121.49,A
2041-04-17,169.74,C
2041-04-18,84.13,C
2041-04-19,128.72,C
2041-04-20,96.82,C
2041-04-21,137.0,C
2041-04-22,99.77,C
2041-04-23,88.36,C
2041-04-24,139.18,B
2041-04-25,85.25,C
2041-04-26,57.62,C
2041-04-27,84.49,A
2041-04-28,118.57,B
2041-04-29,141.76,C
2041-04-30,103.33,A
2041-05-01,139.55,C
2041-05-02,59.25,C
2041-05-03,86.7,C
2041-05-04,91.34,C
2041-05-05,96.56,C
2041-05-06,102.52,A
2041-05-07,120.73,C
2041-05-08,86.66,A
2041-05-09,104.17,C
2041-05-10,98.65,A
2041-05-11,72.22,B
2041-05-12,96.59,C
2041-05-13,95.69,B
2041-05-14,95.05,B
2041-05-15,100.95,B
2041-05-16,102.27,C
2041-05-17,87.03,B
2041-05-18,138.18,A
2041-05-19,64.92,C
2041-05-20,78.37,B
2041-05-21,183.4,C
2041-05-22,68.86,B
2041-05-23,59.39,A
2041-05-24,122.03,C
2041-05-25,37.69,C
2041-05-26,92.94,B
2041-05-27,164.37,A
2041-05-28,53.65,C
2041-05-29,40.54,B
2041-05-30,55.02,A
2041-05-31,126.95,A
2041-06-01,133.74,A
2041-06-02,84.62,A
2041-06-03,120.5,C
2041-06-04,141.71,C
2041-06-05,153.62,A
2041-06-06,92.61,B
2041-06-07,30.96,B
2041-06-08,75.37,A
2041-06-09,87.2,A
2041-06-10,92.23,B
2041-06-11,94.36,A
2041-06-12,133.57,A
2041-06-13,138.23,C
2041-06-14,94.85,C
2041-06-15,132.95,A
2041-06-16,110.73,A
2041-06-17,101.94,C
2041-06-18,127.2,A
2041-06-19,102.17,C
2041-06-20,161.73,C
2041-06-21,56.85,B
2041-06-22,84.54,C
2041-06-23,96.2,B
2041-06-24,113.18,B
2041-06-25,138.04,C
2041-06-26,99.27,A
2041-06-27,57.33,C
2041-06-28,125.28,B
2041-06-29,122.65,C
2041-06-30,41.74,C
2041-07-01,99.89,B
2041-07-02,52.3,C
2041-07-03,80.06,C
2041-07-04,72.92,B
2041-07-05,124.28,A
2041-07-06,103.5,B
2041-07-07,61.43,B
2041-07-08,126.4,C
2041-07-09,56.99,B
2041-07-10,109.9,C
2041-07-11,75.58,C
2041-07-12,89.03,C
2041-07-13,115.13,B
2041-07-14,115.64,B
2041-07-15,140.11,C
2041-07-16,97.53,A
2041-07-17,109.19,A
2041-07-18,86.87,B
2041-07-19,105.46,C
2041-07-20,113.18,C
2041-07-21,97.05,B
2041-07-22,45.89,A
2041-07-23,109.94,B
2041-07-24,110.94,B
2041-07-25,79.59,C
2041-07-26,61.47,B
2041-07-27,144.28,A
2041-07-28,114.74,B
2041-07-29,93.13,C
2041-07-30,65.89,A
2041-07-31,75.35,C
2041-08-01,139.08,B
2041-08-02,104.13,B
2041-08-03,83.48,B
2041-08-04,115.29,C
2041-08-05,75.57,B
2041-08-06,145.03,A
2041-08-07,140.58,A
2041-08-08,86.0,C
2041-08-09,81.14,C
2041-08-10,21.46,C
2041-08-11,55.55,B
2041-08-12,59.82,A
2041-08-13,104.06,A
2041-08-14,90.21,A
2041-08-15,59.64,C
2041-08-16,91.57,C
2041-08-17,115.16,B
2041-08-18,115.84,C
2041-08-19,43.01,A
2041-08-20,96.15,B
2041-08-21,58.5,C
2041-08-22,127.05,C
2041-08-23,123.52,A
2041-08-24,73.06,B
2041-08-25,77.02,C
2041-08-26,86.16,C
2041-08-27,32.42,B
2041-08-28,110.08,B
2041-08-29,91.81,C
2041-08-30,129.11,C
2041-08-31,67.05,A
2041-09-01,81.43,B
2041-09-02,54.8,C
2041-09-03,69.87,C
2041-09-04,64.54,C
2041-09-05,129.88,C
2041-09-06,79.05,A
2041-09-07,103.73,B
2041-09-08,88.58,C
2041-09-09,61.92,B
2041-09-10,94.95,B
2041-09-11,117.97,C
2041-09-12,63.77,C
2041-09-13,124.27,C
2041-09-14,74.51,A
2041-09-15,85.51,C
2041-09-16,115.92,B
2041-09-17,80.1,B
2041-09-18,101.13,A
2041-09-19,90.56,C
2041-09-20,103.88,C
2041-09-21,28.58,A
2041-09-22,45.11,A
2041-09-23,118.48,C
2041-09-24,131.12,C
2041-09-25,72.69,B
2041-09-26,92.68,C
2041-09-27,108.84,C
2041-09-28,120.8,B
2041-09-29,39.17,B
2041-09-30,73.42,B
2041-10-01,90.02,B
2041-10-02,121.09,A
2041-10-03,174.95,B
2041-10-04,93.16,A
2041-10-05,57.79,B
2041-10-06,112.07,B
2041-10-07,139.63,B
2041-10-08,103.24,C
2041-10-09,72.8,A
2041-10-10,128.74,B
2041-10-11,119.91,C
2041-10-12,69.22,A
2041-10-13,111.44,A
2041-10-14,129.92,C
2041-10-15,135.46,A
2041-10-16,98.01,C
2041-10-17,89.26,B
2041-10-18,136.82,C
2041-10-19,63.7,A
2041-10-20,113.16,A
2041-10-21,58.9,B
2041-10-22,131.96,C
2041-10-23,92.26,B
2041-10-24,138.06,C
2041-10-25,114.74,C
2041-10-26,63.3,C
2041-10-27,142.74,B
2041-10-28,146.54,B
2041-10-29,126.47,A
2041-10-30,77.15,B
2041-10-31,90.82,C
2041-11-01,104.36,C
2041-11-02,78.89,C
2041-11-03,150.77,B
2041-11-04,104.33,A
2041-11-05,18.9,B
2041-11-06,110.18,A
2041-11-07,89.56,B
2041-11-08,97.75,C
2041-11-09,88.94,A
2041-11-10,124.21,B
2041-11-11,133.59,C
2041-11-12,108.49,A
2041-11-13,89.82,A
2041-11-14,98.76,C
2041-11-15,100.59,C
2041-11-16,75.93,B
2041-11-17,117.37,B
2041-11-18,113.8,C
2041-11-19,146.21,B
2041-11-20,137.66,C
2041-11-21,79.43,C
2041-11-22,124.61,B
2041-11-23,71.47,B
2041-11-24,120.88,A
2041-11-25,84.5,A
2041-11-26,69.3,B
2041-11-27,108.4,B
2041-11-28,19.76,B
2041-11-29,47.47,A
2041-11-30,94.68,B
2041-12-01,102.47,A
2041-12-02,88.92,A
2041-12-03,52.62,A
2041-12-04,75.46,A
2041-12-05,111.74,C
2041-12-06,117.45,B
2041-12-07,99.51,B
2041-12-08,134.71,C
2041-12-09,111.92,B
2041-12-10,77.58,A
2041-12-11,91.58,C
2041-12-12,57.91,A
2041-12-13,96.76,C
2041-12-14,133.36,A
2041-12-15,120.56,B
2041-12-16,142.06,B
2041-12-17,90.91,B
2041-12-18,140.38,C
2041-12-19,94.2,B
2041-12-20,144.15,C
2041-12-21,99.34,A
2041-12-22,121.22,B
2041-12-23,144.25,A
2041-12-24,94.2,B
2041-12-25,106.55,A
2041-12-26,138.67,C
2041-12-27,96.01,B
2041-12-28,60.37,C
2041-12-29,164.29,C
2041-12-30,85.91,A
2041-12-31,104.76,B
2042-01-01,134.68,C
2042-01-02,81.75,A
2042-01-03,69.76,A
2042-01-04,107.61,A
2042-01-05,46.21,B
2042-01-06,91.36,B
2042-01-07,107.35,A
2042-01-08,91.9,B
2042-01-09,53.7,A
2042-01-10,147.83,B
2042-01-11,103.11,B
2042-01-12,142.93,A
2042-01-13,92.2,A
2042-01-14,73.6,B
2042-01-15,108.08,A
2042-01-16,93.53,A
2042-01-17,64.67,A
2042-01-18,144.09,A
2042-01-19,81.33,B
2042-01-20,44.79,C
2042-01-21,139.84,B
2042-01-22,84.87,A
2042-01-23,81.77,A
2042-01-24,47.04,B
2042-01-25,152.23,A
2042-01-26,61.32,B
2042-01-27,101.75,C
2042-01-28,127.38,A
2042-01-29,112.65,C
2042-01-30,98.21,C
2042-01-31,48.95,A
2042-02-01,75.71,A
2042-02-02,105.63,B
2042-02-03,76.38,B
2042-02-04,106.1,A
2042-02-05,120.45,B
2042-02-06,128.88,C
2042-02-07,63.21,C
2042-02-08,165.9,A
2042-02-09,123.02,B
2042-02-10,99.95,A
2042-02-11,132.69,C
2042-02-12,170.96,A
2042-02-13,56.84,A
2042-02-14,93.24,A
2042-02-15,28.65,A
2042-02-16,105.78,C
2042-02-17,110.96,A
2042-02-18,98.83,A
2042-02-19,80.36,B
2042-02-20,38.51,A
2042-02-21,111.21,B
2042-02-22,143.66,A
2042-02-23,82.27,B
2042-02-24,113.2,A
2042-02-25,52.96,B
2042-02-26,113.72,A
2042-02-27,88.2,A
2042-02-28,77.13,B
2042-03-01,77.19,A
2042-03-02,88.19,A
2042-03-03,111.47,A
2042-03-04,60.64,A
2042-03-05,88.09,C
2042-03-06,98.76,A
2042-03-07,67.03,C
2042-03-08,153.9,C
2042-03-09,74.4,A
2042-03-10,79.92,B
2042-03-11,70.65,B
2042-03-12,153.95,A
2042-03-13,116.59,C
2042-03-14,71.9,B
2042-03-15,133.19,A
2042-03-16,106.02,A
2042-03-17,107.72,C
2042-03-18,89.67,B
2042-03-19,77.52,A
2042-03-20,110.13,B
2042-03-21,83.65,A
2042-03-22,154.5,A
2042-03-23,61.92,A
2042-03-24,146.08,C
2042-03-25,96.66,C
2042-03-26,108.97,C
2042-03-27,124.99,B
2042-03-28,81.09,C
2042-03-29,125.93,C
2042-03-30,18.77,B
2042-03-31,78.4,B
2042-04-01,72.28,B
2042-04-02,62.39,B
2042-04-03,93.55,C
2042-04-04,111.57,C
2042-04-05,121.08,B
2042-04-06,107.23,C
2042-04-07,80.03,A
2042-04-08,111.07,C
2042-04-09,103.73,A
2042-04-10,128.08,B
2042-04-11,123.31,B
2042-04-12,100.91,C
2042-04-13,73.08,B
2042-04-14,80.41,C
2042-04-15,83.51,C
2042-04-16,90.54,C
2042-04-17,108.04,C
2042-04-18,121.46,A
2042-04-19,98.47,A
2042-04-20,100.48,B
2042-04-21,104.3,A
2042-04-22,84.52,B
2042-04-23,72.24,B
2042-04-24,47.34,C
2042-04-25,79.15,C
2042-04-26,114.19,C
2042-04-27,78.91,C
2042-04-28,92.87,C
2042-04-29,113.99,C
2042-04-30,115.62,A
2042-05-01,67.61,B
2042-05-02,129.91,B
2042-05-03,133.82,C
2042-05-04,66.1,A
2042-05-05,75.28,C
2042-05-06,135.73,A
2042-05-07,95.59,A
2042-05-08,33.41,B
2042-05-09,58.01,B
2042-05-10,92.52,A
2042-05-11,65.11,B
2042-05-12,87.6,A
2042-05-13,100.97,A
2042-05-14,143.87,A
2042-05-15,91.29,B
2042-05-16,132.18,C
2042-05-17,142.86,B
2042-05-18,143.03,C
2042-05-19,108.54,B
2042-05-20,41.1,B
2042-05-21,113.82,C
2042-05-22,131.45,A
2042-05-23,97.57,C
2042-05-24,152.34,A
2042-05-25,125.8,A
2042-05-26,117.05,B
2042-05-27,132.59,B
2042-05-28,110.0,B
2042-05-29,25.22,B
2042-05-30,85.03,B
2042-05-31,135.92,C
2042-06-01,75.77,C
2042-06-02,93.3,C
2042-06-03,68.65,C
2042-06-04,153.52,B
2042-06-05,150.44,C
2042-06-06,73.48,C
2042-06-07,104.42,A
2042-06-08,79.59,B
2042-06-09,118.45,C
2042-06-10,115.29,C
2042-06-11,124.35,C
2042-06-12,109.96,B
2042-06-13,188.06,C
2042-06-14,69.16,A
2042-06-15,48.68,B
2042-06-16,88.21,A
2042-06-17,52.96,B
2042-06-18,55.23,C
2042-06-19,69.96,B
2042-06-20,72.33,B
2042-06-21,71.36,B
2042-06-22,115.55,A
2042-06-23,84.81,B
2042-06-24,94.61,C
2042-06-25,126.83,B
2042-06-26,77.01,B
2042-06-27,92.06,C
2042-06-28,110.97,C
2042-06-29,41.75,A
2042-06-30,28.45,C
2042-07-01,110.38,C
2042-07-02,114.76,C
2042-07-03,161.93,B
2042-07-04,104.74,A
2042-07-05,124.59,C
2042-07-06,94.14,C
2042-07-07,108.92,C
2042-07-08,150.02,B
2042-07-09,36.28,B
2042-07-10,112.41,A
2042-07-11,164.07,A
2042-07-12,108.46,B
2042-07-13,149.52,C
2042-07-14,81.12,C
2042-07-15,99.04,A
2042-07-16,101.01,C
2042-07-17,33.47,B
2042-07-18,64.95,A
2042-07-19,67.09,B
2042-07-20,124.66,B
2042-07-21,126.16,C
2042-07-22,109.72,B
2042-07-23,124.72,B
2042-07-24,95.08,C
2042-07-25,136.06,B
2042-07-26,100.26,C
2042-07-27,101.74,B
2042-07-28,92.03,C
2042-07-29,120.53,C
2042-07-30,87.36,A
2042-07-31,129.29,B
2042-08-01,66.04,B
2042-08-02,67.03,A
2042-08-03,120.62,C
2042-08-04,97.34,C
2042-08-05,86.38,A
2042-08-06,126.7,A
2042-08-07,81.78,C
2042-08-08,93.99,C
2042-08-09,141.98,A
2042-08-10,146.17,B
2042-08-11,147.17,C
2042-08-12,91.54,A
2042-08-13,136.92,B
2042-08-14,71.53,B
2042-08-15,72.79,B
2042-08-16,29.49,B
2042-08-17,150.19,A
2042-08-18,74.08,C
2042-08-19,96.2,B
2042-08-20,72.69,B
2042-08-21,120.85,A
2042-08-22,88.73,B
2042-08-23,60.86,A
2042-08-24,113.25,A
2042-08-25,56.99,C
2042-08-26,74.88,B
2042-08-27,36.79,C
2042-08-28,57.28,A
2042-08-29,105.36,B
2042-08-30,41.06,A
2042-08-31,55.57,A
2042-09-01,99.93,A
2042-09-02,83.69,B
2042-09-03,159.76,A
2042-09-04,155.02,A
2042-09-05,73.96,B
2042-09-06,116.71,C
2042-09-07,122.48,A
2042-09-08,82.08,C
2042-09-09,55.13,C
2042-09-10,99.63,C
2042-09-11,94.81,C
2042-09-12,86.93,C
2042-09-13,113.27,C
2042-09-14,77.48,B
2042-09-15,65.44,B
2042-09-16,103.27,C
2042-09-17,111.58,B
2042-09-18,64.7,B
2042-09-19,127.84,C
2042-09-20,46.89,C
2042-09-21,82.66,A
2042-09-22,92.26,B
2042-09-23,80.91,C
2042-09-24,83.05,B
2042-09-25,156.13,B
2042-09-26,74.47,B
2042-09-27,173.24,A
2042-09-28,113.11,C
2042-09-29,102.76,B
2042-09-30,114.29,C
2042-10-01,175.9,B
2042-10-02,70.19,C
2042-10-03,66.39,C
2042-10-04,77.35,C
2042-10-05,100.96,B
2042-10-06,48.23,A
2042-10-07,100.67,C
2042-10-08,150.81,A
2042-10-09,104.54,C
2042-10-10,91.22,A
2042-10-11,81.46,C
2042-10-12,126.96,A
2042-10-13,75.23,C
2042-10-14,155.39,C
2042-10-15,158.0,A
2042-10-16,107.7,C
2042-10-17,99.65,B
2042-10-18,130.92,C
2042-10-19,132.84,C
2042-10-20,75.82,B
2042-10-21,84.2,B
2042-10-22,60.33,B
2042-10-23,112.27,A
2042-10-24,120.0,B
2042-10-25,59.5,A
2042-10-26,86.34,B
2042-10-27,59.58,A
2042-10-28,73.04,B
2042-10-29,116.53,B
2042-10-30,78.0,C
2042-10-31,90.98,B
2042-11-01,51.18,C
2042-11-02,105.61,B
2042-11-03,98.67,B
2042-11-04,78.13,C
2042-11-05,94.33,C
2042-11-06,105.41,A
2042-11-07,75.55,C
2042-11-08,55.71,A
2042-11-09,154.17,B
2042-11-10,81.0,C
2042-11-11,97.1,C
2042-11-12,130.33,B
2042-11-13,205.87,B
2042-11-14,62.67,B
2042-11-15,113.49,B
2042-11-16,78.77,C
2042-11-17,83.84,B
2042-11-18,78.39,B
2042-11-19,66.23,C
2042-11-20,98.76,A
2042-11-21,55.76,C
2042-11-22,128.4,A
2042-11-23,101.65,B
2042-11-24,112.67,C
2042-11-25,86.01,B
2042-11-26,88.98,C
2042-11-27,108.63,A
2042-11-28,65.08,C
2042-11-29,89.26,B
2042-11-30,74.21,C
2042-12-01,73.28,B
2042-12-02,36.7,C
2042-12-03,140.18,A
2042-12-04,79.4,C
2042-12-05,134.9,C
2042-12-06,132.06,C
2042-12-07,61.42,C
2042-12-08,67.35,A
2042-12-09,176.82,B
2042-12-10,152.53,C
2042-12-11,99.05,C
2042-12-12,107.72,C
2042-12-13,81.07,A
2042-12-14,112.97,B
2042-12-15,78.48,B
2042-12-16,39.03,C
2042-12-17,60.35,C
2042-12-18,100.63,B
2042-12-19,66.07,C
2042-12-20,97.98,C
2042-12-21,67.76,C
2042-12-22,76.53,A
2042-12-23,125.59,A
2042-12-24,107.42,B
2042-12-25,99.71,A
2042-12-26,126.37,A
2042-12-27,114.42,B
2042-12-28,143.4,A
2042-12-29,86.2,B
2042-12-30,87.11,C
2042-12-31,60.97,B
2043-01-01,87.13,B
2043-01-02,128.94,B
2043-01-03,139.0,C
2043-01-04,128.81,C
2043-01-05,46.17,C
2043-01-06,132.49,B
2043-01-07,44.84,B
2043-01-08,126.94,B
2043-01-09,88.97,B
2043-01-10,121.08,B
2043-01-11,60.03,B
2043-01-12,135.14,B
2043-01-13,56.05,A
2043-01-14,79.95,C
2043-01-15,70.06,A
2043-01-16,47.37,C
2043-01-17,102.84,B
2043-01-18,154.18,A
2043-01-19,180.01,B
2043-01-20,102.91,A
2043-01-21,133.5,A
2043-01-22,106.96,B
2043-01-23,114.97,A
2043-01-24,89.74,A
2043-01-25,105.85,C
2043-01-26,116.12,B
2043-01-27,82.65,B
2043-01-28,97.68,A
2043-01-29,98.25,B
2043-01-30,124.79,C
2043-01-31,68.02,C
2043-02-01,98.45,B
2043-02-02,157.36,B
2043-02-03,134.92,C
2043-02-04,140.48,B
2043-02-05,100.73,A
2043-02-06,109.63,B
2043-02-07,130.07,A
2043-02-08,146.81,C
2043-02-09,41.45,B
2043-02-10,60.54,C
2043-02-11,139.28,B
2043-02-12,91.69,A
2043-02-13,79.85,B
2043-02-14,62.38,C
2043-02-15,83.35,A
2043-02-16,57.93,B
2043-02-17,162.73,C
2043-02-18,99.33,C
2043-02-19,98.26,B
2043-02-20,117.64,C
2043-02-21,128.87,A
2043-02-22,128.38,B
2043-02-23,132.21,A
2043-02-24,81.07,C
2043-02-25,91.06,C
2043-02-26,123.07,C
2043-02-27,84.18,A
2043-02-28,99.85,C
2043-03-01,111.96,B
2043-03-02,123.56,B
2043-03-03,46.67,C
2043-03-04,121.44,B
2043-03-05,92.99,A
2043-03-06,121.22,A
2043-03-07,123.31,C
2043-03-08,148.24,B
2043-03-09,140.6,B
2043-03-10,140.4,C
2043-03-11,143.31,A
2043-03-12,108.74,C
2043-03-13,100.97,B
2043-03-14,126.18,B
2043-03-15,121.96,C
2043-03-16,90.73,A
2043-03-17,112.14,A
2043-03-18,98.95,C
2043-03-19,74.38,B
2043-03-20,138.71,A
2043-03-21,101.66,C
2043-03-22,161.46,B
2043-03-23,51.24,A
2043-03-24,83.3,A
2043-03-25,100.98,C
2043-03-26,134.12,B
2043-03-27,111.98,A
2043-03-28,109.31,C
2043-03-29,66.18,A
2043-03-30,65.29,B
2043-03-31,109.68,A
2043-04-01,59.08,B
2043-04-02,84.04,A
2043-04-03,115.03,A
2043-04-04,70.53,C
2043-04-05,148.16,A
2043-04-06,136.72,B
2043-04-07,52.86,C
2043-04-08,72.03,B
2043-04-09,31.43,B
2043-04-10,76.52,C
2043-04-11,122.99,B
2043-04-12,87.58,A
2043-04-13,113.82,C
2043-04-14,54.33,A
2043-04-15,123.94,A
2043-04-16,83.35,B
2043-04-17,100.37,B
2043-04-18,97.21,A
2043-04-19,61.85,C
2043-04-20,65.41,A
2043-04-21,129.03,A
2043-04-22,111.97,B
2043-04-23,131.5,B
2043-04-24,109.74,A
2043-04-25,104.29,A
2043-04-26,48.22,B
2043-04-27,91.89,A
2043-04-28,99.18,B
2043-04-29,158.59,A
2043-04-30,120.61,C
2043-05-01,146.08,C
2043-05-02,117.93,A
2043-05-03,78.66,B
2043-05-04,103.51,A
2043-05-05,107.09,A
2043-05-06,71.83,B
2043-05-07,88.47,A
2043-05-08,93.83,B
2043-05-09,148.17,A
2043-05-10,37.58,B
2043-05-11,136.17,A
2043-05-12,71.9,C
2043-05-13,58.84,B
2043-05-14,135.4,C
2043-05-15,121.96,B
2043-05-16,137.64,B
2043-05-17,124.02,B
2043-05-18,161.8,B
2043-05-19,116.76,A
2043-05-20,95.31,A
2043-05-21,110.29,B
2043-05-22,137.3,A
2043-05-23,76.62,B
2043-05-24,80.44,B
2043-05-25,90.73,C
2043-05-26,36.86,B
2043-05-27,104.14,B
2043-05-28,43.42,A
2043-05-29,101.59,B
2043-05-30,125.96,B
2043-05-31,85.69,C
2043-06-01,150.31,A
2043-06-02,56.18,A
2043-06-03,135.74,C
2043-06-04,120.25,B
2043-06-05,48.52,C
2043-06-06,181.32,A
2043-06-07,98.09,C
2043-06-08,73.68,A
2043-06-09,57.79,C
2043-06-10,93.4,B
2043-06-11,96.18,B
2043-06-12,103.08,A
2043-06-13,91.07,C
2043-06-14,119.0,A
2043-06-15,88.37,B
2043-06-16,41.95,A
2043-06-17,95.52,B
2043-06-18,28.98,A
2043-06-19,110.61,C
2043-06-20,70.62,B
2043-06-21,104.9,A
2043-06-22,139.42,B
2043-06-23,71.54,C
2043-06-24,131.84,B
2043-06-25,87.0,C
2043-06-26,152.6,B
2043-06-27,89.05,C
2043-06-28,74.79,B
2043-06-29,120.62,C
2043-06-30,86.93,A
2043-07-01,74.76,A
2043-07-02,85.66,B
2043-07-03,98.66,C
2043-07-04,140.27,A
2043-07-05,41.98,B
2043-07-06,100.69,A
2043-07-07,118.75,C
2043-07-08,105.8,B
2043-07-09,139.3,C
2043-07-10,97.77,C
2043-07-11,94.46,C
2043-07-12,117.77,A
2043-07-13,102.98,B
2043-07-14,88.29,C
2043-07-15,115.48,A
2043-07-16,80.42,C
2043-07-17,81.66,A
2043-07-18,84.16,B
2043-07-19,140.59,B
2043-07-20,3.37,B
2043-07-21,111.87,B
2043-07-22,101.03,C
2043-07-23,49.57,A
2043-07-24,109.29,B
2043-07-25,114.27,C
2043-07-26,117.69,B
2043-07-27,20.45,B
2043-07-28,140.99,B
2043-07-29,77.87,A
2043-07-30,120.6,A
2043-07-31,122.23,A
2043-08-01,73.7,B
2043-08-02,119.81,C
2043-08-03,88.44,C
2043-08-04,37.86,C
2043-08-05,135.03,B
2043-08-06,76.75,A
2043-08-07,74.37,C
2043-08-08,87.91,B
2043-08-09,108.17,B
2043-08-10,98.28,C
2043-08-11,116.44,C
2043-08-12,59.82,B
2043-08-13,77.73,C
2043-08-14,96.52,B
2043-08-15,156.94,C
2043-08-16,96.59,A
2043-08-17,132.56,B
2043-08-18,101.16,A
2043-08-19,85.49,C
2043-08-20,92.6,B
2043-08-21,94.95,C
2043-08-22,118.11,B
2043-08-23,141.24,C
2043-08-24,110.79,A
2043-08-25,135.61,A
2043-08-26,71.06,C
2043-08-27,87.56,A
2043-08-28,78.27,A
2043-08-29,109.43,C
2043-08-30,89.23,B
2043-08-31,121.89,C
2043-09-01,72.07,B
2043-09-02,101.29,A
2043-09-03,120.61,A
2043-09-04,38.04,A
2043-09-05,163.65,A
2043-09-06,91.51,A
2043-09-07,39.25,B
2043-09-08,104.74,B
2043-09-09,101.0,B
2043-09-10,97.54,C
2043-09-11,129.74,A
2043-09-12,122.42,A
2043-09-13,108.94,A
2043-09-14,113.5,C
2043-09-15,96.87,C
2043-09-16,153.26,C
2043-09-17,55.62,A
2043-09-18,104.19,B
2043-09-19,101.95,C
2043-09-20,124.4,C
2043-09-21,74.68,B
2043-09-22,47.69,C
2043-09-23,104.7,B
2043-09-24,108.44,A
2043-09-25,144.69,B
2043-09-26,61.89,B
2043-09-27,134.64,A
2043-09-28,171.02,C
2043-09-29,88.67,A
2043-09-30,61.27,C
2043-10-01,87.79,C
2043-10-02,100.42,B
2043-10-03,116.91,A
2043-10-04,82.26,B
2043-10-05,146.66,A
2043-10-06,66.56,A
2043-10-07,47.82,B
2043-10-08,133.2,C
2043-10-09,31.76,C
2043-10-10,93.88,A
2043-10-11,193.53,C
2043-10-12,99.64,B
2043-10-13,66.38,B
2043-10-14,98.28,B
2043-10-15,98.48,A
2043-10-16,104.82,A
2043-10-17,119.87,C
2043-10-18,68.83,B
2043-10-19,150.63,B
2043-10-20,107.35,A
2043-10-21,121.8,A
2043-10-22,81.16,A
2043-10-23,138.59,B
2043-10-24,92.29,B
2043-10-25,108.33,C
2043-10-26,72.01,B
2043-10-27,70.09,C
2043-10-28,120.65,C
2043-10-29,86.69,B
2043-10-30,74.39,C
2043-10-31,80.7,C
2043-11-01,120.97,A
2043-11-02,81.13,B
2043-11-03,96.47,C
2043-11-04,100.45,C
2043-11-05,42.48,C
2043-11-06,55.87,B
2043-11-07,110.09,A
2043-11-08,77.89,A
2043-11-09,131.7,B
2043-11-10,129.0,B
2043-11-11,121.58,B
2043-11-12,154.59,B
2043-11-13,97.51,A
2043-11-14,119.47,C
2043-11-15,82.47,C
2043-11-16,173.57,B
2043-11-17,159.51,B
2043-11-18,100.36,C
2043-11-19,111.6,A
2043-11-20,140.78,B
2043-11-21,95.59,B
2043-11-22,80.3,A
2043-11-23,126.22,A
2043-11-24,102.35,A
2043-11-25,105.49,B
2043-11-26,74.97,A
2043-11-27,48.47,B
2043-11-28,107.08,B
2043-11-29,127.15,A
2043-11-30,93.23,C
2043-12-01,143.09,A
2043-12-02,78.88,C
2043-12-03,68.36,A
2043-12-04,97.98,A
2043-12-05,97.52,B
2043-12-06,123.1,B
2043-12-07,76.75,B
2043-12-08,141.04,B
2043-12-09,159.01,B
2043-12-10,98.83,B
2043-12-11,137.49,B
2043-12-12,68.31,C
2043-12-13,132.26,C
2043-12-14,131.42,B
2043-12-15,125.9,C
2043-12-16,99.53,A
2043-12-17,11.37,C
2043-12-18,80.75,B
2043-12-19,81.22,C
2043-12-20,70.65,A
2043-12-21,103.67,C
2043-12-22,87.43,B
2043-12-23,75.26,A
2043-12-24,68.24,A
2043-12-25,78.53,C
2043-12-26,133.64,A
2043-12-27,80.17,A
2043-12-28,84.55,A
2043-12-29,86.3,B
2043-12-30,110.17,A
2043-12-31,113.69,C
2044-01-01,8.66,C
2044-01-02,130.65,A
2044-01-03,143.09,C
2044-01-04,68.49,A
2044-01-05,70.83,C
2044-01-06,86.49,A
2044-01-07,77.32,A
2044-01-08,106.72,A
2044-01-09,41.6,B
2044-01-10,66.46,C
2044-01-11,146.18,A
2044-01-12,72.15,B
2044-01-13,137.3,B
2044-01-14,110.66,A
2044-01-15,45.72,A
2044-01-16,119.06,A
2044-01-17,96.47,A
2044-01-18,86.34,A
2044-01-19,107.35,C
2044-01-20,114.24,A
2044-01-21,121.23,A
2044-01-22,135.35,C
2044-01-23,112.24,B
2044-01-24,58.73,C
2044-01-25,85.79,B
2044-01-26,188.29,B
2044-01-27,121.73,C
2044-01-28,171.08,A
2044-01-29,141.86,B
2044-01-30,107.25,A
2044-01-31,109.04,C
2044-02-01,98.9,B
2044-02-02,47.87,A
2044-02-03,100.54,B
2044-02-04,136.32,B
2044-02-05,82.01,C
2044-02-06,112.36,A
2044-02-07,125.55,A
2044-02-08,72.71,B
2044-02-09,86.46,A
2044-02-10,69.46,B
2044-02-11,75.34,C
2044-02-12,61.37,C
2044-02-13,72.99,A
2044-02-14,142.58,C
2044-02-15,72.54,C
2044-02-16,103.79,A
2044-02-17,84.91,B
2044-02-18,135.37,C
2044-02-19,139.33,A
2044-02-20,125.92,A
2044-02-21,109.18,A
2044-02-22,93.07,C
2044-02-23,64.24,C
2044-02-24,158.31,B
2044-02-25,77.21,B
2044-02-26,119.91,B
2044-02-27,131.79,B
2044-02-28,83.34,B
2044-02-29,131.14,B
2044-03-01,122.93,A
2044-03-02,151.84,B
2044-03-03,85.92,C
2044-03-04,61.42,B
2044-03-05,84.84,C
2044-03-06,106.27,B
2044-03-07,101.75,A
2044-03-08,88.2,A
2044-03-09,42.72,C
2044-03-10,114.11,B
2044-03-11,159.17,A
2044-03-12,113.93,A
2044-03-13,118.27,B
2044-03-14,117.17,C
2044-03-15,111.91,C
2044-03-16,127.95,C
2044-03-17,111.86,A
2044-03-18,90.62,B
2044-03-19,76.12,A
2044-03-20,92.53,A
2044-03-21,109.42,A
2044-03-22,90.52,A
2044-03-23,109.43,B
2044-03-24,113.17,A
2044-03-25,125.0,A
2044-03-26,138.48,C
2044-03-27,102.34,A
2044-03-28,60.77,B
2044-03-29,89.65,A
2044-03-30,101.32,B
2044-03-31,44.93,B
2044-04-01,141.66,B
2044-04-02,104.27,C
2044-04-03,111.44,C
2044-04-04,140.97,A
2044-04-05,81.1,A
2044-04-06,117.22,A
2044-04-07,96.7,B
2044-04-08,94.48,B
2044-04-09,76.83,B
2044-04-10,149.97,A
2044-04-11,88.73,A
2044-04-12,120.98,A
2044-04-13,43.75,A
2044-04-14,135.4,C
2044-04-15,131.57,C
2044-04-16,102.92,C
2044-04-17,93.19,A
2044-04-18,112.0,C
2044-04-19,168.66,C
2044-04-20,47.1,A
2044-04-21,123.46,C
2044-04-22,72.65,A
2044-04-23,99.4,A
2044-04-24,92.56,A
2044-04-25,87.15,B
2044-04-26,99.69,A
2044-04-27,65.97,C
2044-04-28,130.88,C
2044-04-29,46.18,A
2044-04-30,116.19,B
2044-05-01,90.2,B
2044-05-02,64.87,B
2044-05-03,106.07,C
2044-05-04,53.29,B
2044-05-05,101.19,B
2044-05-06,57.31,A
2044-05-07,113.84,B
2044-05-08,72.27,B
2044-05-09,86.47,B
2044-05-10,108.82,A
2044-05-11,76.82,A
2044-05-12,98.26,B
2044-05-13,54.59,C
2044-05-14,118.93,C
2044-05-15,152.95,A
2044-05-16,183.77,B
2044-05-17,102.74,C
2044-05-18,123.65,B
2044-05-19,46.58,A
2044-05-20,145.08,C
2044-05-21,123.29,A
2044-05-22,85.48,B
2044-05-23,137.12,A
2044-05-24,92.37,A
2044-05-25,86.26,A
2044-05-26,114.26,A
2044-05-27,68.1,C
2044-05-28,130.65,B
2044-05-29,68.32,C
2044-05-30,68.5,B
2044-05-31,72.71,B
2044-06-01,122.79,B
2044-06-02,81.5,B
2044-06-03,59.56,B
2044-06-04,81.83,C
2044-06-05,99.04,B
2044-06-06,64.57,A
2044-06-07,65.45,C
2044-06-08,163.34,C
2044-06-09,74.18,B
2044-06-10,90.31,A
2044-06-11,88.98,A
2044-06-12,87.36,B
2044-06-13,118.64,B
2044-06-14,128.44,A
2044-06-15,90.82,C
2044-06-16,62.91,C
2044-06-17,138.23,B
2044-06-18,97.14,C
2044-06-19,151.91,C
2044-06-20,32.63,A
2044-06-21,105.86,C
2044-06-22,157.56,A
2044-06-23,89.4,B
2044-06-24,93.24,B
2044-06-25,108.87,A
2044-06-26,83.24,A
2044-06-27,90.64,B
2044-06-28,89.87,A
2044-06-29,81.18,B
2044-06-30,136.87,B
2044-07-01,113.83,B
2044-07-02,107.52,B
2044-07-03,124.26,B
2044-07-04,92.07,B
2044-07-05,76.77,C
2044-07-06,81.92,C
2044-07-07,100.05,B
2044-07-08,68.22,C
2044-07-09,84.99,A
2044-07-10,84.52,A
2044-07-11,163.74,B
2044-07-12,16.79,C
2044-07-13,94.61,A
2044-07-14,151.77,A
2044-07-15,103.66,A
2044-07-16,122.6,A
2044-07-17,102.99,C
2044-07-18,79.98,A
2044-07-19,32.74,A
2044-07-20,106.16,A
2044-07-21,76.73,B
2044-07-22,75.99,A
2044-07-23,85.03,A
2044-07-24,152.35,C
2044-07-25,172.15,A
2044-07-26,106.52,A
2044-07-27,86.06,A
2044-07-28,64.68,B
2044-07-29,114.06,C
2044-07-30,153.49,C
2044-07-31,92.58,B
2044-08-01,28.63,B
2044-08-02,120.1,C
2044-08-03,129.56,A
2044-08-04,52.42,C
2044-08-05,105.29,B
2044-08-06,113.54,A
2044-08-07,97.81,C
2044-08-08,76.28,A
2044-08-09,130.48,A
2044-08-10,87.9,B
2044-08-11,55.44,C
2044-08-12,38.52,B
2044-08-13,137.83,C
2044-08-14,118.5,C
2044-08-15,102.64,A
2044-08-16,86.79,A
2044-08-17,84.13,A
2044-08-18,67.73,B
2044-08-19,87.06,A
2044-08-20,36.4,B
2044-08-21,77.92,A
2044-08-22,90.67,B
2044-08-23,118.3,C
2044-08-24,98.8,B
2044-08-25,123.85,B
2044-08-26,140.47,B
2044-08-27,131.75,C
2044-08-28,114.12,B
2044-08-29,100.36,C
2044-08-30,70.58,A
2044-08-31,122.2,A
2044-09-01,150.39,C
2044-09-02,110.26,B
2044-09-03,157.52,A
2044-09-04,54.23,B
2044-09-05,65.38,B
2044-09-06,125.66,C
2044-09-07,107.93,A
2044-09-08,139.41,C
2044-09-09,99.54,C
2044-09-10,73.96,C
2044-09-11,93.59,C
2044-09-12,54.02,B
2044-09-13,66.14,A
2044-09-14,129.17,A
2044-09-15,126.18,A
2044-09-16,110.89,B
2044-09-17,48.37,B
2044-09-18,89.25,C
2044-09-19,63.25,C
2044-09-20,135.3,C
2044-09-21,82.49,C
2044-09-22,67.43,A
2044-09-23,100.89,C
2044-09-24,80.8,B
2044-09-25,144.51,B
2044-09-26,77.96,B
2044-09-27,58.02,C
2044-09-28,35.41,A
2044-09-29,99.08,A
2044-09-30,83.14,C
2044-10-01,57.13,C
2044-10-02,135.68,B
2044-10-03,120.02,B
2044-10-04,99.15,A
2044-10-05,140.93,B
2044-10-06,119.88,B
2044-10-07,75.31,C
2044-10-08,68.82,A
2044-10-09,124.54,C
2044-10-10,111.89,A
2044-10-11,105.09,C
2044-10-12,74.61,A
2044-10-13,118.24,B
2044-10-14,77.34,A
2044-10-15,96.94,A
2044-10-16,143.41,C
2044-10-17,80.17,A
2044-10-18,52.81,A
2044-10-19,68.42,A
2044-10-20,129.83,C
2044-10-21,107.64,A
2044-10-22,96.13,C
2044-10-23,84.73,B
2044-10-24,114.56,C
2044-10-25,74.53,A
2044-10-26,109.17,B
2044-10-27,78.06,A
2044-10-28,73.07,C
2044-10-29,80.88,B
2044-10-30,88.74,B
2044-10-31,98.54,B
2044-11-01,107.06,A
2044-11-02,-10.65,B
2044-11-03,75.77,B
2044-11-04,88.25,B
2044-11-05,145.15,B
2044-11-06,67.6,C
2044-11-07,104.88,A
2044-11-08,144.85,A
2044-11-09,60.61,B
2044-11-10,111.82,C
2044-11-11,75.93,B
2044-11-12,84.86,A
2044-11-13,75.91,A
2044-11-14,78.04,A
2044-11-15,181.66,B
2044-11-16,125.33,B
2044-11-17,133.05,C
2044-11-18,114.76,C
2044-11-19,51.81,A
2044-11-20,111.85,A
2044-11-21,132.19,C
2044-11-22,74.4,A
2044-11-23,128.96,C
2044-11-24,121.13,B
2044-11-25,155.56,A
2044-11-26,87.47,A
2044-11-27,67.07,C
2044-11-28,89.15,B
2044-11-29,143.07,A
2044-11-30,45.15,A
2044-12-01,37.95,C
2044-12-02,102.68,B
2044-12-03,86.72,C
2044-12-04,100.23,B
2044-12-05,128.36,B
2044-12-06,83.42,C
2044-12-07,43.81,C
2044-12-08,161.33,B
2044-12-09,70.08,C
2044-12-10,68.23,A
2044-12-11,133.11,A
2044-12-12,88.47,A
2044-12-13,98.1,A
2044-12-14,114.48,A
2044-12-15,132.51,C
2044-12-16,90.0,B
2044-12-17,145.6,C
2044-12-18,35.93,A
2044-12-19,122.79,A
2044-12-20,74.81,C
2044-12-21,141.17,C
2044-12-22,104.26,B
2044-12-23,79.81,A
2044-12-24,119.89,C
2044-12-25,126.8,B
2044-12-26,119.26,A
2044-12-27,69.11,A
2044-12-28,85.48,A
2044-12-29,99.88,C
2044-12-30,98.76,A
2044-12-31,122.49,A
2045-01-01,39.55,B
2045-01-02,110.03,C
2045-01-03,91.3,B
2045-01-04,74.84,C
2045-01-05,77.73,A
2045-01-06,93.56,B
2045-01-07,-8.03,C
2045-01-08,78.96,C
2045-01-09,73.73,A
2045-01-10,115.98,A
2045-01-11,75.83,B
2045-01-12,93.55,B
2045-01-13,146.27,C
2045-01-14,129.83,B
2045-01-15,127.07,A
2045-01-16,85.0,B
2045-01-17,116.29,B
2045-01-18,92.98,C
2045-01-19,94.12,A
2045-01-20,112.29,C
2045-01-21,82.09,A
2045-01-22,144.79,A
2045-01-23,95.2,C
2045-01-24,118.55,B
2045-01-25,143.67,C
2045-01-26,130.95,B
2045-01-27,57.73,C
2045-01-28,113.35,C
2045-01-29,125.52,B
2045-01-30,126.76,B
2045-01-31,66.6,B
2045-02-01,80.29,C
2045-02-02,129.84,B
2045-02-03,77.15,B
2045-02-04,91.01,B
2045-02-05,147.28,A
2045-02-06,98.57,B
2045-02-07,78.2,B
2045-02-08,73.67,B
2045-02-09,109.98,A
2045-02-10,60.01,A
2045-02-11,92.58,B
2045-02-12,110.0,B
2045-02-13,138.97,B
2045-02-14,120.03,B
2045-02-15,78.58,A
2045-02-16,135.35,B
2045-02-17,91.01,A
2045-02-18,62.93,C
2045-02-19,94.84,A
2045-02-20,143.15,C
2045-02-21,104.96,A
2045-02-22,100.38,A
2045-02-23,149.12,A
2045-02-24,57.06,B
2045-02-25,61.43,A
2045-02-26,51.52,B
2045-02-27,111.18,A
2045-02-28,104.99,A
2045-03-01,146.28,C
2045-03-02,107.3,A
2045-03-03,87.88,B
2045-03-04,100.05,C
2045-03-05,103.53,C
2045-03-06,49.85,A
2045-03-07,181.31,A
2045-03-08,115.6,C
2045-03-09,106.3,B
2045-03-10,118.45,A
2045-03-11,121.28,C
2045-03-12,95.78,B
2045-03-13,81.06,A
2045-03-14,117.98,C
2045-03-15,111.57,C
2045-03-16,75.7,A
2045-03-17,142.57,B
2045-03-18,108.8,B
2045-03-19,121.89,C
2045-03-20,141.76,B
2045-03-21,100.66,C
2045-03-22,109.51,A
2045-03-23,124.3,B
2045-03-24,78.25,A
2045-03-25,101.62,C
2045-03-26,103.77,A
2045-03-27,71.32,B
2045-03-28,169.53,A
2045-03-29,57.78,B
2045-03-30,61.06,B
2045-03-31,139.43,B
2045-04-01,121.4,B
2045-04-02,69.22,B
2045-04-03,118.22,C
2045-04-04,144.04,A
2045-04-05,99.09,C
2045-04-06,177.96,A
2045-04-07,48.28,A
2045-04-08,120.81,A
2045-04-09,135.0,C
2045-04-10,126.74,B
2045-04-11,93.87,B
2045-04-12,91.29,C
2045-04-13,113.32,B
2045-04-14,124.47,A
2045-04-15,118.07,B
2045-04-16,131.2,B
2045-04-17,69.43,B
2045-04-18,93.17,A
2045-04-19,71.8,B
2045-04-20,88.42,B
2045-04-21,130.46,B
2045-04-22,103.24,C
2045-04-23,178.21,C
2045-04-24,105.8,B
2045-04-25,101.79,C
2045-04-26,103.19,C
2045-04-27,80.87,A
2045-04-28,98.26,C
2045-04-29,133.83,C
2045-04-30,103.42,B
2045-05-01,76.2,A
2045-05-02,72.25,A
2045-05-03,86.49,A
2045-05-04,102.22,C
2045-05-05,135.81,B
2045-05-06,59.79,A
2045-05-07,90.41,A
2045-05-08,104.33,C
2045-05-09,44.99,A
2045-05-10,74.99,B
2045-05-11,79.6,A
2045-05-12,76.12,B
2045-05-13,105.76,B
2045-05-14,95.18,B
2045-05-15,83.34,C
2045-05-16,112.84,C
2045-05-17,151.69,B
2045-05-18,149.18,B
2045-05-19,42.32,A
2045-05-20,117.31,A
2045-05-21,94.66,A
2045-05-22,104.65,C
2045-05-23,88.05,A
2045-05-24,103.53,B
2045-05-25,104.54,B
2045-05-26,82.63,C
2045-05-27,73.11,C
2045-05-28,86.54,A
2045-05-29,107.03,C
2045-05-30,117.97,B
2045-05-31,125.6,B
2045-06-01,85.17,C
2045-06-02,108.52,B
2045-06-03,128.88,C
2045-06-04,134.97,B
2045-06-05,98.24,A
2045-06-06,93.37,C
2045-06-07,105.73,C
2045-06-08,65.51,C
2045-06-09,94.19,B
2045-06-10,122.34,A
2045-06-11,119.26,C
2045-06-12,91.89,B
2045-06-13,106.53,C
2045-06-14,75.26,B
2045-06-15,121.1,A
2045-06-16,97.67,B
2045-06-17,91.72,B
2045-06-18,89.15,C
2045-06-19,98.11,B
2045-06-20,86.56,C
2045-06-21,119.74,B
2045-06-22,88.14,A
2045-06-23,147.79,B
2045-06-24,143.96,C
2045-06-25,73.25,B
2045-06-26,102.09,B
2045-06-27,80.48,A
2045-06-28,90.2,C
2045-06-29,160.07,A
2045-06-30,90.77,B
2045-07-01,110.81,A
2045-07-02,116.24,B
2045-07-03,69.98,B
2045-07-04,74.35,A
2045-07-05,63.82,A
2045-07-06,73.42,B
2045-07-07,14.5,A
2045-07-08,116.34,A
2045-07-09,161.63,B
2045-07-10,104.43,C
2045-07-11,57.51,A
2045-07-12,80.72,A
2045-07-13,86.23,C
2045-07-14,69.47,A
2045-07-15,125.81,B
2045-07-16,124.31,A
2045-07-17,65.34,B
2045-07-18,121.75,C
2045-07-19,145.8,C
2045-07-20,157.08,C
2045-07-21,185.96,A
2045-07-22,72.94,C
2045-07-23,184.73,A
2045-07-24,89.37,A
2045-07-25,82.23,A
2045-07-26,109.81,C
2045-07-27,82.54,C
2045-07-28,111.97,A
2045-07-29,60.4,C
2045-07-30,92.84,A
2045-07-31,138.57,C
2045-08-01,98.29,B
2045-08-02,104.25,C
2045-08-03,114.26,C
2045-08-04,135.12,C
2045-08-05,50.34,A
2045-08-06,117.54,A
2045-08-07,91.96,B
2045-08-08,105.0,B
2045-08-09,131.8,B
2045-08-10,64.17,A
2045-08-11,69.62,B
2045-08-12,41.94,B
2045-08-13,83.53,C
2045-08-14,139.91,B
2045-08-15,96.38,C
2045-08-16,59.8,A
2045-08-17,85.43,A
2045-08-18,55.36,C
2045-08-19,66.24,B
2045-08-20,111.66,A
2045-08-21,64.78,C
2045-08-22,133.38,A
2045-08-23,97.87,A
2045-08-24,102.57,B
2045-08-25,91.65,A
2045-08-26,123.19,C
2045-08-27,123.49,B
2045-08-28,110.05,C
2045-08-29,116.94,C
2045-08-30,93.64,C
2045-08-31,116.27,B
2045-09-01,89.86,A
2045-09-02,100.07,C
2045-09-03,91.76,A
2045-09-04,86.28,A
2045-09-05,79.33,C
2045-09-06,114.03,A
2045-09-07,139.32,B
2045-09-08,135.13,B
2045-09-09,62.3,A
2045-09-10,138.68,A
2045-09-11,90.46,B
2045-09-12,118.02,A
2045-09-13,72.81,A
2045-09-14,104.26,A
2045-09-15,109.41,A
2045-09-16,55.46,C
2045-09-17,118.25,A
2045-09-18,140.38,B
2045-09-19,94.76,C
2045-09-20,140.81,C
2045-09-21,94.31,B
2045-09-22,125.49,A
2045-09-23,33.44,C
2045-09-24,172.56,C
2045-09-25,144.86,A
2045-09-26,91.46,A
2045-09-27,40.84,B
2045-09-28,108.64,C
2045-09-29,56.68,A
2045-09-30,113.35,A
2045-10-01,112.35,B
2045-10-02,71.76,C
2045-10-03,133.63,A
2045-10-04,124.17,B
2045-10-05,129.27,C
2045-10-06,100.27,C
2045-10-07,57.53,C
2045-10-08,129.34,C
2045-10-09,91.89,B
2045-10-10,58.31,A
2045-10-11,140.56,C
2045-10-12,162.98,C
2045-10-13,98.76,A
2045-10-14,164.77,C
2045-10-15,102.74,C
2045-10-16,88.59,A
2045-10-17,81.09,A
2045-10-18,149.11,C
2045-10-19,76.6,A
2045-10-20,122.06,B
2045-10-21,83.23,B
2045-10-22,86.19,C
2045-10-23,130.91,C
2045-10-24,88.6,A
2045-10-25,87.86,A
2045-10-26,133.98,C
2045-10-27,147.92,C
2045-10-28,119.34,B
2045-10-29,80.36,A
2045-10-30,84.68,C
2045-10-31,102.4,C
2045-11-01,109.63,A
2045-11-02,70.36,A
2045-11-03,121.69,C
2045-11-04,102.57,C
2045-11-05,48.53,C
2045-11-06,72.13,A
2045-11-07,84.88,C
2045-11-08,97.81,C
2045-11-09,156.68,C
2045-11-10,107.15,B
2045-11-11,74.78,B
2045-11-12,106.62,A
2045-11-13,89.44,C
2045-11-14,109.74,C
2045-11-15,117.42,A
2045-11-16,136.28,A
2045-11-17,99.27,A
2045-11-18,149.17,C
2045-11-19,127.24,B
2045-11-20,78.78,A
2045-11-21,166.3,C
2045-11-22,109.81,A
2045-11-23,74.95,C
2045-11-24,149.59,A
2045-11-25,162.33,C
2045-11-26,99.01,A
2045-11-27,84.89,B
2045-11-28,94.83,A
2045-11-29,121.44,A
2045-11-30,138.34,B
2045-12-01,117.11,A
2045-12-02,103.05,B
2045-12-03,144.94,C
2045-12-04,90.61,B
2045-12-05,130.93,B
2045-12-06,91.36,C
2045-12-07,112.93,B
2045-12-08,96.59,B
2045-12-09,98.32,C
2045-12-10,88.68,A
2045-12-11,140.35,A
2045-12-12,78.01,B
2045-12-13,77.79,A
2045-12-14,163.63,A
2045-12-15,129.34,B
2045-12-16,28.45,C
2045-12-17,83.04,A
2045-12-18,45.64,B
2045-12-19,71.48,C
2045-12-20,5.84,A
2045-12-21,95.89,C
2045-12-22,65.74,B
2045-12-23,101.8,C
2045-12-24,51.25,C
2045-12-25,102.49,B
2045-12-26,87.55,A
2045-12-27,145.06,A
2045-12-28,142.34,C
2045-12-29,123.56,C
2045-12-30,90.2,C
2045-12-31,159.56,C
2046-01-01,53.41,A
2046-01-02,57.89,B
2046-01-03,97.49,B
2046-01-04,167.62,B
2046-01-05,93.93,C
2046-01-06,94.5,C
2046-01-07,56.74,B
2046-01-08,128.38,A
2046-01-09,128.6,B
2046-01-10,123.67,A
2046-01-11,81.12,C
2046-01-12,78.8,B
2046-01-13,61.21,B
2046-01-14,102.52,C
2046-01-15,67.59,A
2046-01-16,64.68,B
2046-01-17,87.29,B
2046-01-18,85.33,A
2046-01-19,83.86,B
2046-01-20,113.99,B
2046-01-21,102.37,C
2046-01-22,91.36,A
2046-01-23,105.49,B
2046-01-24,70.25,B
2046-01-25,103.16,C
2046-01-26,104.52,B
2046-01-27,54.91,C
2046-01-28,129.87,B
2046-01-29,73.56,C
2046-01-30,102.71,A
2046-01-31,134.93,A
2046-02-01,127.77,B
2046-02-02,101.03,B
2046-02-03,89.13,C
2046-02-04,128.43,C
2046-02-05,120.24,A
2046-02-06,81.45,B
2046-02-07,76.15,C
2046-02-08,37.83,A
2046-02-09,104.62,A
2046-02-10,97.52,C
2046-02-11,134.09,B
2046-02-12,110.77,A
2046-02-13,140.03,C
2046-02-14,79.08,A
2046-02-15,134.14,B
2046-02-16,84.13,C
2046-02-17,123.33,B
2046-02-18,56.24,B
2046-02-19,107.05,A
2046-02-20,40.46,B
2046-02-21,91.99,A
2046-02-22,113.21,C
2046-02-23,84.37,C
2046-02-24,68.49,A
2046-02-25,125.38,A
2046-02-26,96.9,A
2046-02-27,85.31,B
2046-02-28,101.26,C
2046-03-01,179.1,C
2046-03-02,134.61,B
2046-03-03,134.88,A
2046-03-04,113.07,C
2046-03-05,104.99,B
2046-03-06,28.12,A
2046-03-07,79.63,C
2046-03-08,85.58,A
2046-03-09,43.75,B
2046-03-10,65.28,C
2046-03-11,40.57,A
2046-03-12,98.85,A
2046-03-13,124.56,A
2046-03-14,133.2,B
2046-03-15,112.44,A
2046-03-16,71.1,C
2046-03-17,42.04,C
2046-03-18,47.29,A
2046-03-19,61.93,B
2046-03-20,73.33,C
2046-03-21,95.21,C
2046-03-22,69.58,C
2046-03-23,110.17,A
2046-03-24,93.03,C
2046-03-25,100.32,A
2046-03-26,134.21,A
2046-03-27,64.59,A
2046-03-28,112.56,A
2046-03-29,99.58,A
2046-03-30,90.0,C
2046-03-31,126.56,A
2046-04-01,126.37,A
2046-04-02,71.61,A
2046-04-03,94.54,A
2046-04-04,108.44,A
2046-04-05,91.81,A
2046-04-06,117.55,C
2046-04-07,108.44,A
2046-04-08,154.98,A
2046-04-09,54.09,C
2046-04-10,96.33,C
2046-04-11,72.74,B
2046-04-12,118.12,C
2046-04-13,51.48,C
2046-04-14,110.39,A
2046-04-15,81.1,B
2046-04-16,77.98,A
2046-04-17,56.61,B
2046-04-18,86.05,A
2046-04-19,132.54,C
2046-04-20,99.24,C
2046-04-21,118.83,B
2046-04-22,149.94,A
2046-04-23,107.91,A
2046-04-24,83.24,C
2046-04-25,114.02,C
2046-04-26,83.67,B
2046-04-27,84.87,C
2046-04-28,54.4,B
2046-04-29,127.79,B
2046-04-30,88.42,B
2046-05-01,105.46,B
2046-05-02,68.92,C
2046-05-03,113.08,B
2046-05-04,77.14,C
2046-05-05,95.75,B
2046-05-06,126.33,C
2046-05-07,123.72,B
2046-05-08,99.34,C
2046-05-09,46.96,A
2046-05-10,189.78,B
2046-05-11,152.38,B
2046-05-12,121.68,A
2046-05-13,77.4,A
2046-05-14,96.59,B
2046-05-15,83.7,C
2046-05-16,137.92,B
2046-05-17,107.88,C
2046-05-18,76.48,A
2046-05-19,85.88,C
2046-05-20,133.64,B
2046-05-21,148.0,A
2046-05-22,145.85,A
2046-05-23,122.71,B
2046-05-24,92.42,B
2046-05-25,2.75,A
2046-05-26,108.91,C
2046-05-27,99.95,C
2046-05-28,104.2,C
2046-05-29,101.62,B
2046-05-30,60.21,A
2046-05-31,116.72,C
2046-06-01,118.97,A
2046-06-02,120.47,C
2046-06-03,128.4,A
2046-06-04,118.12,B
2046-06-05,87.7,B
2046-06-06,97.87,B
2046-06-07,102.73,C
2046-06-08,103.23,B
2046-06-09,129.95,B
2046-06-10,119.31,B
2046-06-11,162.32,A
2046-06-12,166.19,A
2046-06-13,102.39,A
2046-06-14,-15.1,A
2046-06-15,137.1,A
2046-06-16,83.79,A
2046-06-17,75.96,A
2046-06-18,88.05,B
2046-06-19,86.6,B
2046-06-20,118.37,C
2046-06-21,53.22,B
2046-06-22,67.67,A
2046-06-23,110.29,A
2046-06-24,118.69,C
2046-06-25,119.25,B
2046-06-26,76.39,B
2046-06-27,70.65,C
2046-06-28,84.8,A
2046-06-29,107.2,A
2046-06-30,144.56,A
2046-07-01,38.58,A
2046-07-02,93.38,C
2046-07-03,60.02,B
2046-07-04,50.43,A
2046-07-05,129.01,B
2046-07-06,116.34,B
2046-07-07,113.48,A
2046-07-08,127.63,A
2046-07-09,85.61,A
2046-07-10,87.02,B
2046-07-11,165.57,C
2046-07-12,92.6,A
2046-07-13,122.05,B
2046-07-14,100.19,B
2046-07-15,44.55,A
2046-07-16,38.66,C
2046-07-17,82.63,A
2046-07-18,131.68,A
2046-07-19,123.65,C
2046-07-20,127.42,A
2046-07-21,70.39,A
2046-07-22,148.48,C
2046-07-23,71.05,B
2046-07-24,106.38,B
2046-07-25,99.69,B
2046-07-26,94.78,B
2046-07-27,140.16,B
2046-07-28,78.85,A
2046-07-29,59.51,C
2046-07-30,100.6,B
2046-07-31,88.01,C
2046-08-01,201.32,B
2046-08-02,120.3,A
2046-08-03,87.72,B
2046-08-04,68.32,B
2046-08-05,43.59,C
2046-08-06,69.24,C
2046-08-07,103.45,A
2046-08-08,142.82,C
2046-08-09,133.65,C
2046-08-10,106.87,C
2046-08-11,90.3,A
2046-08-12,108.06,A
2046-08-13,84.44,A
2046-08-14,151.55,C
2046-08-15,94.32,C
2046-08-16,97.41,A
2046-08-17,33.53,C
2046-08-18,94.14,C
2046-08-19,125.09,B
2046-08-20,67.97,A
2046-08-21,77.48,A
2046-08-22,123.78,C
2046-08-23,55.7,C
2046-08-24,131.73,B
2046-08-25,84.04,A
2046-08-26,88.34,A
2046-08-27,66.28,C
2046-08-28,111.34,A
2046-08-29,173.8,C
2046-08-30,123.44,B
2046-08-31,74.07,C
2046-09-01,134.07,B
2046-09-02,90.5,C
2046-09-03,141.23,B
2046-09-04,124.2,B
2046-09-05,11.71,A
2046-09-06,69.88,C
2046-09-07,128.33,A
2046-09-08,104.55,A
2046-09-09,69.29,C
2046-09-10,80.99,A
2046-09-11,42.81,A
2046-09-12,97.35,A
2046-09-13,166.32,C
2046-09-14,124.15,A
2046-09-15,165.71,A
2046-09-16,126.47,A
2046-09-17,130.82,C
2046-09-18,159.22,B
2046-09-19,111.32,B
2046-09-20,94.38,B
2046-09-21,130.98,B
2046-09-22,143.62,A
2046-09-23,85.6,C
2046-09-24,78.35,A
2046-09-25,104.17,C
2046-09-26,124.94,A
2046-09-27,123.94,B
2046-09-28,57.27,B
2046-09-29,157.35,C
2046-09-30,80.77,A
2046-10-01,92.05,B
2046-10-02,72.92,C
2046-10-03,180.95,A
2046-10-04,107.26,C
2046-10-05,74.47,A
2046-10-06,125.92,A
2046-10-07,123.07,A
2046-10-08,94.37,C
2046-10-09,125.22,C
2046-10-10,75.24,A
2046-10-11,85.21,B
2046-10-12,39.69,A
2046-10-13,60.28,C
2046-10-14,154.23,A
2046-10-15,149.24,C
2046-10-16,101.97,A
2046-10-17,149.89,A
2046-10-18,95.2,B
2046-10-19,92.7,B
2046-10-20,101.7,C
2046-10-21,96.25,A
2046-10-22,93.54,C
2046-10-23,134.3,A
2046-10-24,104.5,A
2046-10-25,121.37,A
2046-10-26,130.94,B
2046-10-27,111.65,C
2046-10-28,70.89,A
2046-10-29,59.41,A
2046-10-30,104.46,C
2046-10-31,94.86,C
2046-11-01,102.62,A
2046-11-02,99.52,B
2046-11-03,177.51,B
2046-11-04,60.09,A
2046-11-05,40.3,B
2046-11-06,109.97,C
2046-11-07,88.71,B
2046-11-08,109.48,C
2046-11-09,117.52,C
2046-11-10,138.52,C
2046-11-11,54.78,C
2046-11-12,83.3,B
2046-11-13,112.86,C
2046-11-14,123.11,A
2046-11-15,165.8,C
2046-11-16,128.67,A
2046-11-17,98.3,C
2046-11-18,73.29,C
2046-11-19,83.07,B
2046-11-20,82.22,C
2046-11-21,119.66,C
2046-11-22,107.71,B
2046-11-23,60.29,B
2046-11-24,86.92,A
2046-11-25,126.33,B
2046-11-26,110.55,A
2046-11-27,117.06,C
2046-11-28,111.49,A
2046-11-29,148.79,B
2046-11-30,107.08,C
2046-12-01,88.42,A
2046-12-02,72.53,A
2046-12-03,107.49,A
2046-12-04,26.78,C
2046-12-05,94.47,A
2046-12-06,93.82,A
2046-12-07,59.07,B
2046-12-08,72.15,A
2046-12-09,124.29,C
2046-12-10,106.92,B
2046-12-11,103.05,C
2046-12-12,109.19,B
2046-12-13,102.78,B
2046-12-14,137.29,A
2046-12-15,103.7,C
2046-12-16,56.25,A
2046-12-17,148.9,B
2046-12-18,111.11,A
2046-12-19,82.32,B
2046-12-20,62.4,B
2046-12-21,111.88,C
2046-12-22,96.48,C
2046-12-23,147.22,C
2046-12-24,89.62,C
2046-12-25,74.88,A
2046-12-26,126.87,C
2046-12-27,127.76,C
2046-12-28,105.06,C
2046-12-29,120.29,B
2046-12-30,98.87,B
2046-12-31,107.02,C
2047-01-01,134.95,B
2047-01-02,118.76,B
2047-01-03,94.92,A
2047-01-04,140.57,B
2047-01-05,29.05,A
2047-01-06,161.56,B
2047-01-07,48.82,C
2047-01-08,81.19,B
2047-01-09,98.77,B
2047-01-10,95.61,A
2047-01-11,130.19,C
2047-01-12,68.26,A
2047-01-13,107.82,C
2047-01-14,82.65,C
2047-01-15,108.85,A
2047-01-16,98.06,B
2047-01-17,102.79,C
2047-01-18,83.5,B
2047-01-19,84.7,A
2047-01-20,108.62,A
2047-01-21,128.31,A
2047-01-22,82.05,A
2047-01-23,73.94,C
2047-01-24,118.4,A
2047-01-25,99.02,B
2047-01-26,124.91,B
2047-01-27,72.83,C
2047-01-28,46.62,A
2047-01-29,84.01,C
2047-01-30,77.01,C
2047-01-31,97.64,C
2047-02-01,80.89,A
2047-02-02,122.23,B
2047-02-03,91.36,B
2047-02-04,104.51,C
2047-02-05,104.73,C
2047-02-06,105.85,B
2047-02-07,99.28,B
2047-02-08,115.32,A
2047-02-09,76.13,C
2047-02-10,141.98,A
2047-02-11,92.9,B
2047-02-12,60.65,B
2047-02-13,105.64,B
2047-02-14,74.46,B
2047-02-15,119.96,A
2047-02-16,166.29,B
2047-02-17,76.4,C
2047-02-18,109.97,B
2047-02-19,47.33,C
2047-02-20,113.62,B
2047-02-21,73.43,A
2047-02-22,81.25,C
2047-02-23,66.96,A
2047-02-24,121.96,B
2047-02-25,130.22,A
2047-02-26,100.7,B
2047-02-27,74.07,A
2047-02-28,147.69,A
2047-03-01,77.49,C
2047-03-02,35.14,B
2047-03-03,113.75,A
2047-03-04,66.86,B
2047-03-05,129.66,C
2047-03-06,88.57,A
2047-03-07,118.11,B
2047-03-08,142.05,A
2047-03-09,63.73,A
2047-03-10,83.02,A
2047-03-11,97.73,A
2047-03-12,146.13,C
2047-03-13,80.25,C
2047-03-14,82.16,B
2047-03-15,114.34,B
2047-03-16,82.68,A
2047-03-17,104.58,A
2047-03-18,53.45,A
2047-03-19,166.43,C
2047-03-20,59.57,A
2047-03-21,90.35,B
2047-03-22,99.03,A
2047-03-23,157.38,C
2047-03-24,60.25,A
2047-03-25,159.51,C
2047-03-26,122.72,C
2047-03-27,112.51,C
2047-03-28,70.46,C
2047-03-29,117.13,B
2047-03-30,78.3,C
2047-03-31,138.0,B
2047-04-01,70.87,B
2047-04-02,96.61,A
2047-04-03,83.34,A
2047-04-04,70.37,C
2047-04-05,122.24,A
2047-04-06,116.06,C
2047-04-07,136.27,A
2047-04-08,46.6,C
2047-04-09,131.93,A
2047-04-10,175.16,A
2047-04-11,86.45,B
2047-04-12,156.85,A
2047-04-13,92.11,A
2047-04-14,58.83,C
2047-04-15,90.83,B
2047-04-16,131.45,C
2047-04-17,74.85,A
2047-04-18,81.99,C
2047-04-19,117.81,B
2047-04-20,100.09,C
2047-04-21,117.54,A
2047-04-22,107.13,C
2047-04-23,114.39,A
2047-04-24,87.03,B
2047-04-25,106.95,B
2047-04-26,72.18,C
2047-04-27,138.65,B
2047-04-28,80.97,C
2047-04-29,133.87,A
2047-04-30,92.99,C
2047-05-01,106.06,A
2047-05-02,85.15,B
2047-05-03,69.52,A
2047-05-04,47.66,A
2047-05-05,68.34,A
2047-05-06,90.28,A
2047-05-07,106.36,A
2047-05-08,130.31,C
2047-05-09,86.63,A
2047-05-10,73.54,A
2047-05-11,56.72,A
2047-05-12,114.4,A
2047-05-13,141.5,A
2047-05-14,137.99,A
2047-05-15,121.3,B
2047-05-16,119.16,C
2047-05-17,150.93,A
2047-05-18,109.96,C
2047-05-19,119.38,B
2047-05-20,90.38,B
2047-05-21,59.14,A
2047-05-22,79.48,C
2047-05-23,99.73,A
2047-05-24,101.82,B
2047-05-25,157.79,B
2047-05-26,102.44,C
2047-05-27,135.37,A
2047-05-28,86.95,C
2047-05-29,90.65,C
2047-05-30,60.13,B
2047-05-31,77.73,C
2047-06-01,115.81,B
2047-06-02,100.23,A
2047-06-03,128.29,C
2047-06-04,105.44,C
2047-06-05,89.43,A
2047-06-06,164.45,C
2047-06-07,97.18,A
2047-06-08,134.89,B
2047-06-09,87.96,B
2047-06-10,121.29,C
2047-06-11,127.84,B
2047-06-12,106.25,B
2047-06-13,29.37,A
2047-06-14,100.55,B
2047-06-15,136.91,B
2047-06-16,115.82,B
2047-06-17,113.38,A
2047-06-18,48.33,C
2047-06-19,78.39,A
2047-06-20,112.34,C
2047-06-21,112.31,C
2047-06-22,136.44,B
2047-06-23,95.65,B
2047-06-24,112.17,A
2047-06-25,100.49,A
2047-06-26,70.39,A
2047-06-27,32.33,B
2047-06-28,112.93,C
2047-06-29,105.22,A
2047-06-30,81.45,C
2047-07-01,51.42,B
2047-07-02,118.89,B
2047-07-03,92.02,B
2047-07-04,116.71,B
2047-07-05,155.92,C
2047-07-06,35.76,C
2047-07-07,79.01,A
2047-07-08,93.72,C
2047-07-09,143.64,C
2047-07-10,50.91,C
2047-07-11,158.08,A
2047-07-12,104.08,C
2047-07-13,97.12,C
2047-07-14,94.06,C
2047-07-15,86.91,B
2047-07-16,131.44,C
2047-07-17,67.94,A
2047-07-18,110.36,A
2047-07-19,88.47,A
2047-07-20,77.14,A
2047-07-21,177.58,B
2047-07-22,112.57,A
2047-07-23,118.4,B
2047-07-24,134.13,B
2047-07-25,57.36,B
2047-07-26,152.57,A
2047-07-27,24.73,A
2047-07-28,76.94,B
2047-07-29,86.23,B
2047-07-30,117.45,A
2047-07-31,106.56,C
2047-08-01,95.0,C
2047-08-02,99.64,C
2047-08-03,70.66,A
2047-08-04,55.08,A
2047-08-05,121.8,B
2047-08-06,120.81,A
2047-08-07,147.53,A
2047-08-08,76.3,A
2047-08-09,120.78,B
2047-08-10,91.49,B
2047-08-11,135.71,C
2047-08-12,130.66,C
2047-08-13,106.34,A
2047-08-14,89.81,A
2047-08-15,128.83,B
2047-08-16,119.41,C
2047-08-17,79.47,C
2047-08-18,60.17,C
2047-08-19,91.34,A
2047-08-20,145.46,A
2047-08-21,134.39,A
2047-08-22,66.63,B
2047-08-23,64.97,C
2047-08-24,138.24,B
2047-08-25,65.93,C
2047-08-26,115.28,A
2047-08-27,81.52,B
2047-08-28,148.51,B
2047-08-29,76.89,A
2047-08-30,134.3,C
2047-08-31,105.55,C
2047-09-01,107.57,C
2047-09-02,40.89,C
2047-09-03,33.91,A
2047-09-04,93.38,A
2047-09-05,107.41,A
2047-09-06,66.24,A
2047-09-07,67.34,B
2047-09-08,116.84,B
2047-09-09,116.89,B
2047-09-10,95.87,A
2047-09-11,123.41,C
2047-09-12,64.77,B
2047-09-13,89.32,B
2047-09-14,149.44,A
2047-09-15,102.05,C
2047-09-16,103.79,C
2047-09-17,169.18,C
2047-09-18,58.02,A
2047-09-19,123.89,C
2047-09-20,149.92,B
2047-09-21,90.25,B
2047-09-22,125.79,B
2047-09-23,70.54,A
2047-09-24,79.4,C
2047-09-25,111.55,A
2047-09-26,123.96,C
2047-09-27,133.63,A
2047-09-28,147.09,B
2047-09-29,97.98,A
2047-09-30,135.81,A
2047-10-01,89.73,A
2047-10-02,127.06,B
2047-10-03,106.22,A
2047-10-04,113.37,A
2047-10-05,112.71,B
2047-10-06,51.86,C
2047-10-07,66.28,A
2047-10-08,103.08,B
2047-10-09,49.38,A
2047-10-10,124.94,B
2047-10-11,126.15,C
2047-10-12,109.82,C
2047-10-13,66.37,A
2047-10-14,141.4,B
2047-10-15,161.12,C
2047-10-16,104.41,C
2047-10-17,115.32,B
2047-10-18,106.05,A
2047-10-19,115.91,C
2047-10-20,96.41,C
2047-10-21,98.96,C
2047-10-22,127.17,A
2047-10-23,170.37,C
2047-10-24,58.0,B
2047-10-25,96.89,A
2047-10-26,64.59,C
2047-10-27,147.76,B
2047-10-28,82.37,B
2047-10-29,56.7,A
2047-10-30,119.15,B
2047-10-31,152.33,A
2047-11-01,119.91,A
2047-11-02,106.14,A
2047-11-03,112.27,B
2047-11-04,142.45,A
2047-11-05,73.77,A
2047-11-06,96.64,B
2047-11-07,86.42,A
2047-11-08,64.96,C
2047-11-09,90.15,B
2047-11-10,133.23,B
2047-11-11,117.0,B
2047-11-12,119.33,C
2047-11-13,104.39,A
2047-11-14,115.7,B
2047-11-15,75.98,B
2047-11-16,90.71,B
2047-11-17,105.76,C
2047-11-18,108.33,C
2047-11-19,109.61,C
2047-11-20,95.85,A
2047-11-21,88.68,A
2047-11-22,194.73,C
2047-11-23,127.2,A
2047-11-24,112.39,C
2047-11-25,93.0,A
2047-11-26,86.46,A
2047-11-27,96.08,C
2047-11-28,108.78,B
2047-11-29,43.59,B
2047-11-30,52.92,A
2047-12-01,80.89,A
2047-12-02,106.21,A
2047-12-03,92.82,A
2047-12-04,144.34,A
2047-12-05,55.53,A
2047-12-06,77.46,C
2047-12-07,145.6,B
2047-12-08,103.93,B
2047-12-09,126.55,C
2047-12-10,161.54,C
2047-12-11,79.19,B
2047-12-12,97.02,B
2047-12-13,91.83,A
2047-12-14,41.36,B
2047-12-15,90.69,A
2047-12-16,126.43,A
2047-12-17,116.22,B
2047-12-18,118.57,A
2047-12-19,88.73,A
2047-12-20,80.32,C
2047-12-21,138.08,B
2047-12-22,85.08,C
2047-12-23,108.82,A
2047-12-24,104.9,A
2047-12-25,143.85,C
2047-12-26,129.14,C
2047-12-27,61.14,C
2047-12-28,64.37,A
2047-12-29,117.37,B
2047-12-30,98.07,A
2047-12-31,86.34,C
2048-01-01,130.47,B
2048-01-02,105.62,C
2048-01-03,101.57,A
2048-01-04,123.39,B
2048-01-05,88.75,B
2048-01-06,147.06,A
2048-01-07,76.36,C
2048-01-08,119.71,C
2048-01-09,143.01,C
2048-01-10,58.68,A
2048-01-11,89.7,C
2048-01-12,134.27,B
2048-01-13,134.72,C
2048-01-14,94.86,C
2048-01-15,77.96,B
2048-01-16,128.68,B
2048-01-17,135.51,C
2048-01-18,65.06,B
2048-01-19,63.88,A
2048-01-20,106.76,B
2048-01-21,118.88,A
2048-01-22,91.03,B
2048-01-23,120.38,A
2048-01-24,78.7,A
2048-01-25,36.56,C
2048-01-26,87.28,B
2048-01-27,101.9,B
2048-01-28,91.72,B
2048-01-29,65.24,A
2048-01-30,45.53,C
2048-01-31,84.58,B
2048-02-01,126.93,C
2048-02-02,137.69,C
2048-02-03,135.63,A
2048-02-04,75.52,B
2048-02-05,65.39,C
2048-02-06,114.9,B
2048-02-07,119.05,B
2048-02-08,112.01,B
2048-02-09,39.58,C
2048-02-10,73.99,A
2048-02-11,99.11,A
2048-02-12,114.62,C
2048-02-13,123.02,B
2048-02-14,104.33,B
2048-02-15,109.92,A
2048-02-16,102.55,C
2048-02-17,120.57,C
2048-02-18,109.39,A
2048-02-19,101.97,C
2048-02-20,91.95,C
2048-02-21,102.44,A
2048-02-22,114.54,A
2048-02-23,108.02,C
2048-02-24,111.42,C
2048-02-25,140.28,A
2048-02-26,107.95,A
2048-02-27,80.53,B
2048-02-28,126.51,C
2048-02-29,59.01,C
2048-03-01,67.56,A
2048-03-02,110.08,A
2048-03-03,115.49,B
2048-03-04,62.65,B
2048-03-05,88.52,A
2048-03-06,81.82,C
2048-03-07,85.4,B
2048-03-08,128.31,A
2048-03-09,121.2,A
2048-03-10,99.43,A
2048-03-11,83.62,C
2048-03-12,59.51,A
2048-03-13,142.61,A
2048-03-14,117.76,C
2048-03-15,78.8,A
2048-03-16,121.59,A
2048-03-17,97.89,B
2048-03-18,91.39,A
2048-03-19,113.06,C
2048-03-20,77.76,B
2048-03-21,89.69,A
2048-03-22,103.64,C
2048-03-23,85.02,A
2048-03-24,120.07,B
2048-03-25,156.07,C
2048-03-26,77.37,B
2048-03-27,50.05,C
2048-03-28,39.95,C
2048-03-29,95.49,A
2048-03-30,80.11,C
2048-03-31,106.24,C
2048-04-01,103.98,A
2048-04-02,51.9,C
2048-04-03,45.55,C
2048-04-04,108.88,C
2048-04-05,104.06,C
2048-04-06,87.23,A
2048-04-07,50.29,A
2048-04-08,114.14,B
2048-04-09,138.16,C
2048-04-10,108.34,A
2048-04-11,74.58,C
2048-04-12,101.34,A
2048-04-13,59.02,B
2048-04-14,131.74,C
2048-04-15,102.46,B
2048-04-16,46.96,B
2048-04-17,90.98,B
2048-04-18,113.9,A
2048-04-19,90.01,C
2048-04-20,91.72,A
2048-04-21,63.27,A
2048-04-22,117.0,C
2048-04-23,80.55,C
2048-04-24,35.31,C
2048-04-25,88.86,C
2048-04-26,129.43,A
2048-04-27,88.39,C
2048-04-28,152.14,C
2048-04-29,112.87,C
2048-04-30,110.16,B
2048-05-01,96.15,C
2048-05-02,97.95,C
2048-05-03,92.1,A
2048-05-04,90.93,B
2048-05-05,115.93,A
2048-05-06,71.93,B
2048-05-07,67.72,A
2048-05-08,66.3,A
2048-05-09,161.37,C
2048-05-10,115.91,C
2048-05-11,109.13,B
2048-05-12,68.27,B
2048-05-13,118.7,B
2048-05-14,96.5,C
2048-05-15,106.43,A
2048-05-16,70.7,B
2048-05-17,104.81,C
2048-05-18,87.28,C
2048-05-19,77.03,A
2048-05-20,122.02,A
2048-05-21,53.87,C
2048-05-22,116.94,A
2048-05-23,120.53,A
2048-05-24,73.61,C
2048-05-25,75.39,B
2048-05-26,93.31,B
2048-05-27,108.46,C
2048-05-28,95.41,B
2048-05-29,113.01,C
2048-05-30,152.89,C
2048-05-31,84.77,A
2048-06-01,48.54,B
2048-06-02,124.78,C
2048-06-03,150.5,B
2048-06-04,111.78,B
2048-06-05,67.37,B
2048-06-06,96.28,C
2048-06-07,66.44,A
2048-06-08,119.29,B
2048-06-09,59.46,C
2048-06-10,91.0,B
2048-06-11,69.87,A
2048-06-12,103.8,B
2048-06-13,89.01,C
2048-06-14,105.3,B
2048-06-15,100.67,B
2048-06-16,70.4,B
2048-06-17,118.81,A
2048-06-18,98.54,B
2048-06-19,70.34,C
2048-06-20,167.23,B
2048-06-21,114.29,B
2048-06-22,95.4,B
2048-06-23,142.46,A
2048-06-24,155.25,C
2048-06-25,95.64,B
2048-06-26,110.42,C
2048-06-27,70.36,C
2048-06-28,95.58,A
2048-06-29,94.03,C
2048-06-30,105.54,B
2048-07-01,109.98,A
2048-07-02,116.79,A
2048-07-03,107.13,C
2048-07-04,63.68,A
2048-07-05,71.85,C
2048-07-06,83.61,C
2048-07-07,91.53,A
2048-07-08,131.52,B
2048-07-09,65.11,C
2048-07-10,89.28,A
2048-07-11,128.59,A
2048-07-12,91.0,C
2048-07-13,113.81,C
2048-07-14,95.81,C
2048-07-15,107.7,C
2048-07-16,93.15,B
2048-07-17,120.01,B
2048-07-18,53.65,B
2048-07-19,89.1,B
2048-07-20,108.9,A
2048-07-21,116.01,C
2048-07-22,144.79,A
2048-07-23,88.27,A
2048-07-24,144.38,B
2048-07-25,80.23,A
2048-07-26,139.86,B
2048-07-27,124.52,C
2048-07-28,72.6,B
2048-07-29,109.21,C
2048-07-30,98.46,B
2048-07-31,118.77,A
2048-08-01,93.3,A
2048-08-02,90.78,B
2048-08-03,141.07,C
2048-08-04,97.04,C
2048-08-05,124.79,B
2048-08-06,93.24,A
2048-08-07,150.0,C
2048-08-08,79.65,B
2048-08-09,81.58,C
2048-08-10,145.74,A
2048-08-11,86.65,C
2048-08-12,148.03,B
2048-08-13,126.76,C
2048-08-14,95.36,C
2048-08-15,45.6,A
2048-08-16,99.42,A
2048-08-17,103.05,A
2048-08-18,135.02,A
2048-08-19,147.65,B
2048-08-20,79.45,C
2048-08-21,124.04,C
2048-08-22,122.96,C
2048-08-23,132.2,C
2048-08-24,114.96,B
2048-08-25,41.73,A
2048-08-26,95.34,C
2048-08-27,65.33,C
2048-08-28,93.92,A
2048-08-29,129.4,A
2048-08-30,109.15,C
2048-08-31,134.44,A
2048-09-01,110.25,C
2048-09-02,61.05,C
2048-09-03,24.37,C
2048-09-04,104.46,C
2048-09-05,93.58,C
2048-09-06,70.69,C
2048-09-07,48.98,A
2048-09-08,77.96,B
2048-09-09,94.17,B
2048-09-10,119.98,C
2048-09-11,105.66,A
2048-09-12,148.24,C
2048-09-13,68.93,A
2048-09-14,126.43,C
2048-09-15,101.42,C
2048-09-16,120.84,B
2048-09-17,86.9,A
2048-09-18,132.15,A
2048-09-19,108.86,B
2048-09-20,79.62,B
2048-09-21,99.64,B
2048-09-22,92.81,B
2048-09-23,114.68,A
2048-09-24,64.61,A
2048-09-25,99.12,B
2048-09-26,111.86,C
2048-09-27,100.99,C
2048-09-28,140.41,C
2048-09-29,123.22,C
2048-09-30,99.78,A
2048-10-01,106.5,A
2048-10-02,108.87,A
2048-10-03,112.1,C
2048-10-04,45.23,B
2048-10-05,90.95,C
2048-10-06,55.47,A
2048-10-07,138.35,C
2048-10-08,98.17,B
2048-10-09,94.2,B
2048-10-10,126.9,C
2048-10-11,112.67,A
2048-10-12,66.14,A
2048-10-13,97.87,A
2048-10-14,89.94,C
2048-10-15,47.66,A
2048-10-16,79.29,C
2048-10-17,94.49,A
2048-10-18,96.55,B
2048-10-19,55.53,B
2048-10-20,134.55,A
2048-10-21,96.94,C
2048-10-22,101.13,A
2048-10-23,112.02,C
2048-10-24,76.36,B
2048-10-25,113.69,C
2048-10-26,88.99,A
2048-10-27,73.1,B
2048-10-28,117.2,C
2048-10-29,144.75,C
2048-10-30,121.74,B
2048-10-31,127.69,A
2048-11-01,84.38,B
2048-11-02,52.07,A
2048-11-03,144.7,A
2048-11-04,161.64,C
2048-11-05,146.28,A
2048-11-06,89.93,A
2048-11-07,65.36,B
2048-11-08,68.03,B
2048-11-09,49.49,A
2048-11-10,96.78,C
2048-11-11,148.17,B
2048-11-12,95.75,C
2048-11-13,109.07,B
2048-11-14,141.27,B
2048-11-15,87.88,A
2048-11-16,161.07,B
2048-11-17,159.76,B
2048-11-18,96.07,C
2048-11-19,82.67,B
2048-11-20,120.92,B
2048-11-21,89.57,C
2048-11-22,65.3,A
2048-11-23,138.44,A
2048-11-24,136.49,B
2048-11-25,104.67,B
2048-11-26,62.11,C
2048-11-27,63.11,A
2048-11-28,103.22,A
2048-11-29,85.0,A
2048-11-30,89.77,C
2048-12-01,130.66,C
2048-12-02,122.0,C
2048-12-03,141.34,B
2048-12-04,70.28,A
2048-12-05,89.7,B
2048-12-06,122.77,B
2048-12-07,113.44,C
2048-12-08,145.97,A
2048-12-09,112.62,B
2048-12-10,155.44,A
2048-12-11,116.2,B
2048-12-12,131.87,C
2048-12-13,90.29,B
2048-12-14,100.43,C
2048-12-15,127.33,A
2048-12-16,148.39,B
2048-12-17,121.88,B
2048-12-18,97.5,C
2048-12-19,141.32,C
2048-12-20,34.24,A
2048-12-21,57.33,C
2048-12-22,120.45,A
2048-12-23,100.42,B
2048-12-24,118.17,A
2048-12-25,101.93,A
2048-12-26,117.13,A
2048-12-27,75.57,B
2048-12-28,117.11,B
2048-12-29,129.83,C
2048-12-30,136.62,A
2048-12-31,160.41,B
2049-01-01,107.32,A
2049-01-02,108.22,A
2049-01-03,54.3,A
2049-01-04,84.06,B
2049-01-05,94.21,A
2049-01-06,131.51,A
2049-01-07,81.67,C
2049-01-08,123.89,A
2049-01-09,99.95,C
2049-01-10,94.68,A
2049-01-11,107.47,C
2049-01-12,88.27,B
2049-01-13,87.13,B
2049-01-14,48.86,A
2049-01-15,92.56,C
2049-01-16,85.6,B
2049-01-17,84.52,A
2049-01-18,112.51,A
2049-01-19,119.76,C
2049-01-20,106.97,C
2049-01-21,129.86,A
2049-01-22,117.27,C
2049-01-23,91.15,C
2049-01-24,86.11,B
2049-01-25,109.38,C
2049-01-26,117.21,A
2049-01-27,141.94,C
2049-01-28,135.74,A
2049-01-29,83.76,A
2049-01-30,96.94,C
2049-01-31,120.13,C
2049-02-01,132.04,C
2049-02-02,128.17,B
2049-02-03,98.08,B
2049-02-04,62.38,C
2049-02-05,70.03,A
2049-02-06,108.18,C
2049-02-07,110.27,B
2049-02-08,67.04,B
2049-02-09,101.34,C
2049-02-10,118.94,A
2049-02-11,65.46,A
2049-02-12,133.15,B
2049-02-13,88.67,B
2049-02-14,137.73,B
2049-02-15,84.0,A
2049-02-16,69.2,B
2049-02-17,86.26,B
2049-02-18,97.15,A
2049-02-19,88.35,B
2049-02-20,93.52,B
2049-02-21,110.02,C
2049-02-22,94.86,B
2049-02-23,98.02,C
2049-02-24,48.72,B
2049-02-25,111.75,B
2049-02-26,97.32,C
2049-02-27,148.31,B
2049-02-28,71.81,C
2049-03-01,45.75,B
2049-03-02,118.54,C
2049-03-03,110.4,C
2049-03-04,69.75,B
2049-03-05,54.11,B
2049-03-06,146.12,C
2049-03-07,142.42,A
2049-03-08,25.38,B
2049-03-09,86.25,B
2049-03-10,149.93,A
2049-03-11,85.72,A
2049-03-12,85.28,C
2049-03-13,91.52,B
2049-03-14,66.37,C
2049-03-15,48.86,B
2049-03-16,131.58,A
2049-03-17,44.71,C
2049-03-18,107.42,B
2049-03-19,126.6,B
2049-03-20,56.46,A
2049-03-21,72.27,A
2049-03-22,136.4,B
2049-03-23,115.99,A
2049-03-24,81.48,A
2049-03-25,136.07,C
2049-03-26,136.2,B
2049-03-27,108.07,C
2049-03-28,73.0,A
2049-03-29,87.6,B
2049-03-30,108.59,A
2049-03-31,94.34,A
2049-04-01,128.17,B
2049-04-02,54.11,A
2049-04-03,55.17,C
2049-04-04,105.43,B
2049-04-05,85.25,A
2049-04-06,81.19,A
2049-04-07,46.96,A
2049-04-08,100.59,B
2049-04-09,98.42,C
2049-04-10,119.6,A
2049-04-11,102.07,A
2049-04-12,96.02,A
2049-04-13,135.31,B
2049-04-14,60.63,A
2049-04-15,116.09,A
2049-04-16,49.87,A
2049-04-17,74.85,B
2049-04-18,63.62,C
2049-04-19,123.46,B
2049-04-20,98.04,C
2049-04-21,99.59,C
2049-04-22,66.8,B
2049-04-23,91.01,A
2049-04-24,77.1,C
2049-04-25,111.5,A
2049-04-26,70.27,C
2049-04-27,139.89,A
2049-04-28,49.73,A
2049-04-29,75.8,B
2049-04-30,150.32,A
2049-05-01,86.95,C
2049-05-02,60.55,A
2049-05-03,106.57,A
2049-05-04,85.74,A
2049-05-05,74.92,C
2049-05-06,67.12,C
2049-05-07,183.05,C
2049-05-08,92.45,A
2049-05-09,42.23,B
2049-05-10,144.81,C
2049-05-11,118.33,A
2049-05-12,77.01,C
2049-05-13,72.01,B
2049-05-14,120.2,A
2049-05-15,92.59,C
2049-05-16,42.22,C
2049-05-17,153.62,B
2049-05-18,86.98,C
2049-05-19,144.87,A
2049-05-20,138.35,A
2049-05-21,69.97,B
2049-05-22,131.0,B
2049-05-23,80.46,B
2049-05-24,44.0,A
2049-05-25,87.26,B
2049-05-26,111.93,C
2049-05-27,86.12,C
2049-05-28,90.79,A
2049-05-29,134.31,C
2049-05-30,103.19,B
2049-05-31,74.7,C
2049-06-01,121.06,B
2049-06-02,105.93,C
2049-06-03,24.35,A
2049-06-04,112.85,B
2049-06-05,111.75,C
2049-06-06,100.6,A
2049-06-07,126.75,C
2049-06-08,79.83,A
2049-06-09,110.09,C
2049-06-10,52.99,B
2049-06-11,112.21,B
2049-06-12,158.59,B
2049-06-13,70.15,B
2049-06-14,77.21,B
2049-06-15,78.7,C
2049-06-16,103.9,C
2049-06-17,68.11,C
2049-06-18,112.64,C
2049-06-19,125.41,B
2049-06-20,116.03,A
2049-06-21,46.95,B
2049-06-22,129.86,C
2049-06-23,128.12,B
2049-06-24,124.9,B
2049-06-25,111.19,B
2049-06-26,106.61,A
2049-06-27,130.97,B
2049-06-28,157.71,B
2049-06-29,71.41,A
2049-06-30,132.34,B
2049-07-01,97.41,B
2049-07-02,96.11,B
2049-07-03,53.9,B
2049-07-04,91.46,B
2049-07-05,70.08,C
2049-07-06,86.08,B
2049-07-07,67.05,A
2049-07-08,86.53,B
2049-07-09,87.82,C
2049-07-10,102.9,A
2049-07-11,51.56,A
2049-07-12,56.63,B
2049-07-13,110.86,C
2049-07-14,76.08,B
2049-07-15,132.24,C
2049-07-16,106.76,C
2049-07-17,118.2,B
2049-07-18,142.32,C
2049-07-19,90.71,B
2049-07-20,82.77,C
2049-07-21,126.15,A
2049-07-22,115.29,B
2049-07-23,94.38,C
2049-07-24,141.74,A
2049-07-25,93.63,A
2049-07-26,100.98,A
2049-07-27,100.64,A
2049-07-28,71.96,B
2049-07-29,102.33,C
2049-07-30,141.9,A
2049-07-31,113.35,C
2049-08-01,100.93,C
2049-08-02,95.15,C
2049-08-03,109.93,B
2049-08-04,122.49,B
2049-08-05,118.28,A
2049-08-06,90.04,C
2049-08-07,109.74,B
2049-08-08,114.4,B
2049-08-09,69.78,B
2049-08-10,105.05,A
2049-08-11,35.16,B
2049-08-12,114.48,C
2049-08-13,119.81,B
2049-08-14,122.43,B
2049-08-15,88.15,A
2049-08-16,109.27,C
2049-08-17,142.72,B
2049-08-18,142.41,C
2049-08-19,99.12,C
2049-08-20,104.18,B
2049-08-21,133.33,A
2049-08-22,54.98,C
2049-08-23,90.31,B
2049-08-24,140.07,C
2049-08-25,129.26,C
2049-08-26,111.08,C
2049-08-27,54.83,B
2049-08-28,98.5,C
2049-08-29,82.3,C
2049-08-30,109.93,A
2049-08-31,75.72,B
2049-09-01,69.23,B
2049-09-02,38.05,C
2049-09-03,136.65,A
2049-09-04,88.01,C
2049-09-05,104.71,C
2049-09-06,99.93,B
2049-09-07,67.98,A
2049-09-08,62.23,C
2049-09-09,69.84,B
2049-09-10,78.3,C
2049-09-11,111.0,C
2049-09-12,76.13,A
2049-09-13,152.39,A
2049-09-14,130.81,A
2049-09-15,124.25,C
2049-09-16,96.41,A
2049-09-17,145.43,B
2049-09-18,93.46,C
2049-09-19,99.06,A
2049-09-20,105.1,C
2049-09-21,121.05,C
2049-09-22,119.11,B
2049-09-23,85.75,B
2049-09-24,84.01,B
2049-09-25,119.2,C
2049-09-26,121.46,C
2049-09-27,88.2,B
2049-09-28,52.38,C
2049-09-29,93.78,B
2049-09-30,118.71,B
2049-10-01,154.16,A
2049-10-02,64.06,B
2049-10-03,87.2,A
2049-10-04,88.32,B
2049-10-05,140.67,C
2049-10-06,98.95,A
2049-10-07,72.78,C
2049-10-08,116.76,C
2049-10-09,87.86,B
2049-10-10,63.27,B
2049-10-11,73.22,C
2049-10-12,152.02,B
2049-10-13,111.53,B
2049-10-14,103.23,B
2049-10-15,92.5,C
2049-10-16,108.76,A
2049-10-17,108.41,A
2049-10-18,108.77,C
2049-10-19,103.45,B
2049-10-20,126.06,B
2049-10-21,100.69,A
2049-10-22,70.66,C
2049-10-23,81.63,C
2049-10-24,40.63,B
2049-10-25,160.8,B
2049-10-26,125.88,B
2049-10-27,75.1,C
2049-10-28,91.54,A
2049-10-29,80.89,C
2049-10-30,146.28,C
2049-10-31,57.4,C
2049-11-01,103.43,A
2049-11-02,96.74,B
2049-11-03,124.81,B
2049-11-04,96.88,C
2049-11-05,47.25,C
2049-11-06,92.47,A
2049-11-07,70.84,A
2049-11-08,102.39,B
2049-11-09,143.03,C
2049-11-10,47.82,A
2049-11-11,75.99,A
2049-11-12,83.42,C
2049-11-13,88.55,C
2049-11-14,122.2,C
2049-11-15,94.79,C
2049-11-16,112.47,C
2049-11-17,81.23,C
2049-11-18,106.9,A
2049-11-19,123.33,C
2049-11-20,78.03,B
2049-11-21,123.44,C
2049-11-22,101.57,B
2049-11-23,131.36,B
2049-11-24,93.96,B
2049-11-25,111.44,C
2049-11-26,98.78,C
2049-11-27,97.11,B
2049-11-28,107.77,C
2049-11-29,116.1,A
2049-11-30,62.51,A
2049-12-01,143.01,A
2049-12-02,44.21,A
2049-12-03,79.46,C
2049-12-04,97.17,B
2049-12-05,112.37,C
2049-12-06,147.29,A
2049-12-07,44.25,C
2049-12-08,84.77,C
2049-12-09,36.19,A
2049-12-10,100.95,B
2049-12-11,108.15,C
2049-12-12,147.72,C
2049-12-13,103.62,A
2049-12-14,131.32,B
2049-12-15,117.52,A
2049-12-16,86.94,A
2049-12-17,57.73,C
2049-12-18,108.25,B
2049-12-19,55.24,A
2049-12-20,138.86,C
2049-12-21,95.14,A
2049-12-22,135.86,C
2049-12-23,85.59,B
2049-12-24,108.07,A
2049-12-25,106.28,B
2049-12-26,98.65,B
2049-12-27,87.02,C
2049-12-28,111.77,A
2049-12-29,72.22,A
2049-12-30,63.72,C
2049-12-31,137.28,C
2050-01-01,121.82,B
2050-01-02,101.98,A
2050-01-03,149.95,A
2050-01-04,91.97,C
2050-01-05,139.68,C
2050-01-06,105.27,C
2050-01-07,69.26,A
2050-01-08,155.44,A
2050-01-09,129.72,B
2050-01-10,96.37,C
2050-01-11,121.82,B
2050-01-12,87.89,A
2050-01-13,124.87,B
2050-01-14,28.27,B
2050-01-15,69.41,B
2050-01-16,129.38,C
2050-01-17,92.51,A
2050-01-18,106.23,C
2050-01-19,59.6,B
2050-01-20,155.44,C
2050-01-21,135.72,A
2050-01-22,93.98,A
2050-01-23,87.86,B
2050-01-24,130.89,B
2050-01-25,159.47,C
2050-01-26,128.61,A
2050-01-27,165.15,A
2050-01-28,72.33,C
2050-01-29,75.42,A
2050-01-30,64.33,B
2050-01-31,113.25,B
2050-02-01,105.27,C
2050-02-02,95.44,C
2050-02-03,124.28,C
2050-02-04,136.97,B
2050-02-05,54.23,C
2050-02-06,82.09,A
2050-02-07,124.6,C
2050-02-08,85.97,A
2050-02-09,76.05,B
2050-02-10,130.56,A
2050-02-11,61.31,A
2050-02-12,90.95,A
2050-02-13,109.86,B
2050-02-14,82.79,A
2050-02-15,123.49,A
2050-02-16,82.93,C
2050-02-17,142.26,C
2050-02-18,96.23,C
2050-02-19,120.14,A
2050-02-20,122.38,A
2050-02-21,81.17,B
2050-02-22,38.4,A
2050-02-23,110.82,A
2050-02-24,102.17,B
2050-02-25,116.67,A
2050-02-26,93.2,B
2050-02-27,87.09,B
2050-02-28,160.25,C
2050-03-01,55.83,A
2050-03-02,122.02,C
2050-03-03,82.76,A
2050-03-04,89.93,C
2050-03-05,90.49,B
2050-03-06,75.9,B
2050-03-07,117.14,B
2050-03-08,141.72,B
2050-03-09,114.1,A
2050-03-10,103.1,A
2050-03-11,70.41,C
2050-03-12,115.5,A
2050-03-13,164.69,A
2050-03-14,88.58,C
2050-03-15,88.72,B
2050-03-16,83.17,C
2050-03-17,85.32,C
2050-03-18,17.64,B
2050-03-19,112.15,C
2050-03-20,74.63,C
2050-03-21,51.69,C
2050-03-22,122.6,A
2050-03-23,67.54,B
2050-03-24,89.84,B
2050-03-25,143.99,C
2050-03-26,57.72,A
2050-03-27,54.02,B
2050-03-28,108.08,B
2050-03-29,134.48,C
2050-03-30,52.77,C
2050-03-31,112.23,C
2050-04-01,117.43,B
2050-04-02,38.23,C
2050-04-03,123.96,C
2050-04-04,126.3,C
2050-04-05,90.65,A
2050-04-06,137.57,C
2050-04-07,134.09,B
2050-04-08,103.09,C
2050-04-09,86.46,B
2050-04-10,70.82,A
2050-04-11,98.87,A
2050-04-12,117.09,C
2050-04-13,90.27,B
2050-04-14,34.07,C
2050-04-15,138.06,C
2050-04-16,105.71,B
2050-04-17,88.61,B
2050-04-18,125.71,B
2050-04-19,145.74,B
2050-04-20,15.94,B
2050-04-21,113.03,A
2050-04-22,57.2,A
2050-04-23,114.97,A
2050-04-24,103.84,B
2050-04-25,119.07,B
2050-04-26,85.02,B
2050-04-27,43.8,C
2050-04-28,86.49,A
2050-04-29,112.21,A
2050-04-30,130.46,C
2050-05-01,64.85,C
2050-05-02,130.81,A
2050-05-03,80.56,B
2050-05-04,110.87,A
2050-05-05,84.7,B
2050-05-06,105.47,A
2050-05-07,126.76,A
2050-05-08,63.19,C
2050-05-09,79.6,C
2050-05-10,105.55,B
2050-05-11,172.05,B
2050-05-12,74.1,A
2050-05-13,161.2,C
2050-05-14,160.13,B
2050-05-15,97.08,B
2050-05-16,91.06,B
2050-05-17,79.24,A
2050-05-18,97.95,B
2050-05-19,57.8,B
2050-05-20,97.71,C
2050-05-21,144.91,C
2050-05-22,81.67,C
2050-05-23,92.78,A
2050-05-24,48.97,C
2050-05-25,111.22,C
2050-05-26,107.93,C
2050-05-27,101.91,C
2050-05-28,93.5,B
2050-05-29,91.22,A
2050-05-30,115.06,B
2050-05-31,99.14,B
2050-06-01,109.45,A
2050-06-02,106.45,B
2050-06-03,133.7,C
2050-06-04,120.31,A
2050-06-05,105.79,B
2050-06-06,145.55,B
2050-06-07,112.61,C
2050-06-08,139.15,B
2050-06-09,118.21,B
2050-06-10,44.74,B
2050-06-11,139.15,A
2050-06-12,104.06,A
2050-06-13,98.44,B
2050-06-14,108.44,B
2050-06-15,89.65,C
2050-06-16,106.81,A
2050-06-17,82.24,A
2050-06-18,138.23,A
2050-06-19,96.59,B
2050-06-20,88.52,C
2050-06-21,72.25,C
2050-06-22,108.06,B
2050-06-23,105.61,B
2050-06-24,111.65,C
2050-06-25,69.18,C
2050-06-26,51.96,A
2050-06-27,118.77,B
2050-06-28,144.54,A
2050-06-29,69.21,C
2050-06-30,147.68,A
2050-07-01,108.52,C
2050-07-02,99.79,C
2050-07-03,88.07,A
2050-07-04,58.46,B
2050-07-05,129.63,A
2050-07-06,76.73,A
2050-07-07,76.61,B
2050-07-08,129.76,B
2050-07-09,43.51,A
2050-07-10,54.29,C
2050-07-11,99.46,A
2050-07-12,108.8,C
2050-07-13,114.54,B
2050-07-14,90.95,B
2050-07-15,175.91,C
2050-07-16,93.16,A
2050-07-17,93.08,B
2050-07-18,76.09,C
2050-07-19,103.72,C
2050-07-20,118.45,A
2050-07-21,136.11,C
2050-07-22,55.32,C
2050-07-23,92.62,A
2050-07-24,113.4,B
2050-07-25,117.61,B
2050-07-26,100.2,B
2050-07-27,116.27,C
2050-07-28,75.73,C
2050-07-29,146.74,A
2050-07-30,129.78,C
2050-07-31,59.65,C
2050-08-01,67.75,B
2050-08-02,68.59,C
2050-08-03,120.34,C
2050-08-04,61.46,C
2050-08-05,90.07,A
2050-08-06,113.64,C
2050-08-07,98.02,A
2050-08-08,107.77,B
2050-08-09,68.03,C
2050-08-10,117.85,A
2050-08-11,81.6,B
2050-08-12,90.26,C
2050-08-13,107.13,A
2050-08-14,103.09,A
2050-08-15,100.72,B
2050-08-16,77.48,C
2050-08-17,91.01,C
2050-08-18,100.07,C
2050-08-19,58.75,A
2050-08-20,86.12,C
2050-08-21,111.63,A
2050-08-22,61.68,A
2050-08-23,134.7,B
2050-08-24,138.09,A
2050-08-25,62.65,B
2050-08-26,140.73,B
2050-08-27,95.69,C
2050-08-28,112.56,B
2050-08-29,85.1,C
2050-08-30,127.69,B
2050-08-31,112.18,A
2050-09-01,75.58,C
2050-09-02,65.04,A
2050-09-03,56.19,C
2050-09-04,105.64,A
2050-09-05,117.17,B
2050-09-06,119.53,C
2050-09-07,96.46,C
2050-09-08,65.29,C
2050-09-09,79.54,B
2050-09-10,134.57,A
2050-09-11,85.46,B
2050-09-12,104.3,B
2050-09-13,67.73,B
2050-09-14,110.1,A
2050-09-15,86.27,B
2050-09-16,131.4,C
2050-09-17,123.21,B
2050-09-18,85.13,A
2050-09-19,96.2,C
2050-09-20,83.47,A
2050-09-21,57.42,B
2050-09-22,74.28,A
2050-09-23,105.95,C
2050-09-24,109.76,A
2050-09-25,104.46,C
2050-09-26,90.95,C
2050-09-27,127.99,B
2050-09-28,101.91,B
2050-09-29,121.1,C
2050-09-30,46.79,A
2050-10-01,-17.67,C
2050-10-02,108.51,B
2050-10-03,57.26,A
2050-10-04,93.63,C
2050-10-05,90.94,B
2050-10-06,147.62,C
2050-10-07,112.0,C
2050-10-08,104.98,C
2050-10-09,125.11,A
2050-10-10,111.05,B
2050-10-11,86.27,B
2050-10-12,132.14,C
2050-10-13,66.71,C
2050-10-14,109.7,C
2050-10-15,53.57,A
2050-10-16,106.12,B
2050-10-17,105.79,C
2050-10-18,150.29,B
2050-10-19,53.05,C
2050-10-20,81.97,B
2050-10-21,124.68,C
2050-10-22,110.06,A
2050-10-23,105.41,C
2050-10-24,71.32,C
2050-10-25,82.28,C
2050-10-26,137.35,C
2050-10-27,98.45,C
2050-10-28,111.26,B
2050-10-29,96.54,B
2050-10-30,64.51,A
2050-10-31,128.01,C
2050-11-01,115.1,B
2050-11-02,169.17,B
2050-11-03,69.53,C
2050-11-04,124.68,C
2050-11-05,60.65,A
2050-11-06,104.72,A
2050-11-07,129.3,B
2050-11-08,37.84,A
2050-11-09,93.85,B
2050-11-10,83.1,A
2050-11-11,78.94,C
2050-11-12,111.56,A
2050-11-13,133.62,B
2050-11-14,96.0,A
2050-11-15,106.16,B
2050-11-16,103.43,B
2050-11-17,100.05,A
2050-11-18,67.32,A
2050-11-19,133.25,B
2050-11-20,108.86,C
2050-11-21,111.85,A
2050-11-22,75.8,B
2050-11-23,86.22,C
2050-11-24,94.72,B
2050-11-25,53.98,A
2050-11-26,49.36,B
2050-11-27,81.98,B
2050-11-28,115.68,B
2050-11-29,80.41,A
2050-11-30,69.44,C
2050-12-01,75.56,C
2050-12-02,86.34,C
2050-12-03,89.38,C
2050-12-04,164.97,A
2050-12-05,131.64,B
2050-12-06,130.74,C
2050-12-07,135.06,C
2050-12-08,70.04,A
2050-12-09,63.33,B
2050-12-10,66.79,B
2050-12-11,140.04,C
2050-12-12,83.58,C
2050-12-13,62.46,B
2050-12-14,87.07,A
2050-12-15,92.84,B
2050-12-16,108.05,B
2050-12-17,107.53,C
2050-12-18,75.15,A
2050-12-19,120.57,A
2050-12-20,80.82,A
2050-12-21,95.31,C
2050-12-22,120.6,A
2050-12-23,80.75,A
2050-12-24,82.92,C
2050-12-25,117.75,B
2050-12-26,74.27,A
2050-12-27,88.46,B
2050-12-28,116.67,A
2050-12-29,161.62,B
2050-12-30,118.16,B
2050-12-31,114.18,C
2051-01-01,115.92,C
2051-01-02,67.8,A
2051-01-03,103.47,A
2051-01-04,99.11,C
2051-01-05,109.65,A
2051-01-06,103.5,A
2051-01-07,111.39,C
2051-01-08,90.66,B
2051-01-09,101.76,A
2051-01-10,63.17,C
2051-01-11,90.4,A
2051-01-12,141.73,A
2051-01-13,109.56,C
2051-01-14,99.37,C
2051-01-15,156.88,A
2051-01-16,59.17,A
2051-01-17,66.72,B
2051-01-18,151.38,B
2051-01-19,101.38,C
2051-01-20,92.72,A
2051-01-21,64.02,C
2051-01-22,113.43,C
2051-01-23,116.33,A
2051-01-24,67.17,B
2051-01-25,95.79,C
2051-01-26,89.93,C
2051-01-27,107.68,A
2051-01-28,66.42,B
2051-01-29,137.56,B
2051-01-30,127.47,A
2051-01-31,89.1,C
2051-02-01,93.51,A
2051-02-02,95.91,A
2051-02-03,76.65,B
2051-02-04,86.7,C
2051-02-05,91.89,C
2051-02-06,148.39,B
2051-02-07,128.4,C
2051-02-08,128.04,C
2051-02-09,111.1,A
2051-02-10,66.48,A
2051-02-11,98.76,A
2051-02-12,129.11,C
2051-02-13,62.25,C
2051-02-14,119.1,C
2051-02-15,169.68,C
2051-02-16,131.85,B
2051-02-17,142.42,B
2051-02-18,106.55,C
2051-02-19,129.04,A
2051-02-20,78.46,A
2051-02-21,123.88,B
2051-02-22,119.63,B
2051-02-23,108.7,C
2051-02-24,139.48,C
2051-02-25,83.84,C
2051-02-26,94.39,C
2051-02-27,102.86,C
2051-02-28,69.11,A
2051-03-01,106.81,A
2051-03-02,48.43,C
2051-03-03,131.41,A
2051-03-04,101.4,C
2051-03-05,84.37,C
2051-03-06,74.97,B
2051-03-07,92.96,A
2051-03-08,120.65,A
2051-03-09,116.43,B
2051-03-10,78.83,A
2051-03-11,74.62,B
2051-03-12,119.71,A
2051-03-13,78.33,C
2051-03-14,101.8,B
2051-03-15,103.71,C
2051-03-16,109.36,C
2051-03-17,125.75,A
2051-03-18,62.11,B
2051-03-19,42.67,C
2051-03-20,82.98,C
2051-03-21,154.05,B
2051-03-22,160.67,A
2051-03-23,154.94,C
2051-03-24,94.21,A
2051-03-25,46.54,B
2051-03-26,29.71,A
2051-03-27,81.58,B
2051-03-28,94.06,A
2051-03-29,139.02,A
2051-03-30,126.03,C
2051-03-31,106.82,A
2051-04-01,73.3,C
2051-04-02,71.18,B
2051-04-03,107.62,B
2051-04-04,120.91,C
2051-04-05,111.76,A
2051-04-06,68.96,B
2051-04-07,119.52,B
2051-04-08,112.78,B
2051-04-09,67.88,C
2051-04-10,76.47,B
2051-04-11,120.65,B
2051-04-12,92.96,B
2051-04-13,147.67,B
2051-04-14,115.03,C
2051-04-15,85.4,C
2051-04-16,99.69,A
2051-04-17,101.9,B
2051-04-18,78.15,C
2051-04-19,72.62,A
2051-04-20,121.04,A
2051-04-21,125.36,B
2051-04-22,118.11,A
2051-04-23,145.46,C
2051-04-24,83.75,A
2051-04-25,150.23,A
2051-04-26,72.97,C
2051-04-27,69.62,A
2051-04-28,47.2,A
2051-04-29,86.63,A
2051-04-30,84.89,A
2051-05-01,115.78,B
2051-05-02,107.32,B
2051-05-03,64.21,C
2051-05-04,88.22,A
2051-05-05,88.86,C
2051-05-06,46.72,B
2051-05-07,70.57,A
2051-05-08,76.88,A
2051-05-09,143.01,B
2051-05-10,105.74,A
2051-05-11,119.87,A
2051-05-12,55.04,B
2051-05-13,135.83,C
2051-05-14,139.03,C
2051-05-15,40.05,A
2051-05-16,78.84,B
2051-05-17,114.87,C
2051-05-18,119.33,C
================================================
FILE: cookbook/pocketflow-batch-node/flow.py
================================================
from pocketflow import Flow, Node
from nodes import CSVProcessor
class ShowStats(Node):
"""Node to display the final statistics."""
def prep(self, shared):
"""Get statistics from shared store."""
return shared["statistics"]
def post(self, shared, prep_res, exec_res):
"""Display the statistics."""
stats = prep_res
print("\nFinal Statistics:")
print(f"- Total Sales: ${stats['total_sales']:,.2f}")
print(f"- Average Sale: ${stats['average_sale']:,.2f}")
print(f"- Total Transactions: {stats['total_transactions']:,}\n")
return "end"
def create_flow():
"""Create and return the processing flow."""
# Create nodes
processor = CSVProcessor(chunk_size=1000)
show_stats = ShowStats()
# Connect nodes
processor - "show_stats" >> show_stats
# Create and return flow
return Flow(start=processor)
================================================
FILE: cookbook/pocketflow-batch-node/main.py
================================================
import os
from flow import create_flow
def main():
"""Run the batch processing example."""
# Create data directory if it doesn't exist
os.makedirs("data", exist_ok=True)
# Create sample CSV if it doesn't exist
if not os.path.exists("data/sales.csv"):
print("Creating sample sales.csv...")
import pandas as pd
import numpy as np
# Generate sample data
np.random.seed(42)
n_rows = 10000
df = pd.DataFrame({
"date": pd.date_range("2024-01-01", periods=n_rows),
"amount": np.random.normal(100, 30, n_rows).round(2),
"product": np.random.choice(["A", "B", "C"], n_rows)
})
df.to_csv("data/sales.csv", index=False)
# Initialize shared store
shared = {
"input_file": "data/sales.csv"
}
# Create and run flow
print(f"Processing sales.csv in chunks...")
flow = create_flow()
flow.run(shared)
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-batch-node/nodes.py
================================================
import pandas as pd
from pocketflow import BatchNode
class CSVProcessor(BatchNode):
"""BatchNode that processes a large CSV file in chunks."""
def __init__(self, chunk_size=1000):
"""Initialize with chunk size."""
super().__init__()
self.chunk_size = chunk_size
def prep(self, shared):
"""Split CSV file into chunks.
Returns an iterator of DataFrames, each containing chunk_size rows.
"""
# Read CSV in chunks
chunks = pd.read_csv(
shared["input_file"],
chunksize=self.chunk_size
)
return chunks
def exec(self, chunk):
"""Process a single chunk of the CSV.
Args:
chunk: pandas DataFrame containing chunk_size rows
Returns:
dict: Statistics for this chunk
"""
return {
"total_sales": chunk["amount"].sum(),
"num_transactions": len(chunk),
"total_amount": chunk["amount"].sum()
}
def post(self, shared, prep_res, exec_res_list):
"""Combine results from all chunks.
Args:
prep_res: Original chunks iterator
exec_res_list: List of results from each chunk
Returns:
str: Action to take next
"""
# Combine statistics from all chunks
total_sales = sum(res["total_sales"] for res in exec_res_list)
total_transactions = sum(res["num_transactions"] for res in exec_res_list)
total_amount = sum(res["total_amount"] for res in exec_res_list)
# Calculate final statistics
shared["statistics"] = {
"total_sales": total_sales,
"average_sale": total_amount / total_transactions,
"total_transactions": total_transactions
}
return "show_stats"
================================================
FILE: cookbook/pocketflow-batch-node/requirements.txt
================================================
pocketflow
pandas>=2.0.0
================================================
FILE: cookbook/pocketflow-chat/README.md
================================================
# Simple PocketFlow Chat
A basic chat application using PocketFlow with OpenAI's GPT-4o model.
## Features
- Conversational chat interface in the terminal
- Maintains full conversation history for context
- Simple implementation demonstrating PocketFlow's node and flow concepts
## Run It
1. Make sure your OpenAI API key is set:
```bash
export OPENAI_API_KEY="your-api-key-here"
```
Alternatively, you can edit the `utils.py` file to include your API key directly.
2. Install requirements and run the application:
```bash
pip install -r requirements.txt
python main.py
```
## How It Works
```mermaid
flowchart LR
chat[ChatNode] -->|continue| chat
```
The chat application uses:
- A single `ChatNode` with a self-loop that:
- Takes user input in the `prep` method
- Sends the complete conversation history to GPT-4o
- Adds responses to the conversation history
- Loops back to continue the chat until the user types 'exit'
## Files
- [`main.py`](./main.py): Implementation of the ChatNode and chat flow
- [`utils.py`](./utils.py): Simple wrapper for calling the OpenAI API
================================================
FILE: cookbook/pocketflow-chat/main.py
================================================
from pocketflow import Node, Flow
from utils import call_llm
class ChatNode(Node):
def prep(self, shared):
# Initialize messages if this is the first run
if "messages" not in shared:
shared["messages"] = []
print("Welcome to the chat! Type 'exit' to end the conversation.")
# Get user input
user_input = input("\nYou: ")
# Check if user wants to exit
if user_input.lower() == 'exit':
return None
# Add user message to history
shared["messages"].append({"role": "user", "content": user_input})
# Return all messages for the LLM
return shared["messages"]
def exec(self, messages):
if messages is None:
return None
# Call LLM with the entire conversation history
response = call_llm(messages)
return response
def post(self, shared, prep_res, exec_res):
if prep_res is None or exec_res is None:
print("\nGoodbye!")
return None # End the conversation
# Print the assistant's response
print(f"\nAssistant: {exec_res}")
# Add assistant message to history
shared["messages"].append({"role": "assistant", "content": exec_res})
# Loop back to continue the conversation
return "continue"
# Create the flow with self-loop
chat_node = ChatNode()
chat_node - "continue" >> chat_node # Loop back to continue conversation
flow = Flow(start=chat_node)
# Start the chat
if __name__ == "__main__":
shared = {}
flow.run(shared)
================================================
FILE: cookbook/pocketflow-chat/requirements.txt
================================================
pocketflow>=0.0.1
openai>=1.0.0
================================================
FILE: cookbook/pocketflow-chat/utils.py
================================================
from openai import OpenAI
import os
def call_llm(messages):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.7
)
return response.choices[0].message.content
if __name__ == "__main__":
# Test the LLM call
messages = [{"role": "user", "content": "In a few words, what's the meaning of life?"}]
response = call_llm(messages)
print(f"Prompt: {messages[0]['content']}")
print(f"Response: {response}")
================================================
FILE: cookbook/pocketflow-chat-guardrail/README.md
================================================
# Travel Advisor Chat with Guardrails
A travel-focused chat application using PocketFlow with OpenAI's GPT-4o model, enhanced with input validation to ensure only travel-related queries are processed.
## Features
- Travel advisor chatbot that answers questions about destinations, planning, accommodations, etc.
- **Topic-specific guardrails** to ensure only travel-related queries are accepted
## Run It
1. Make sure your OpenAI API key is set:
```bash
export OPENAI_API_KEY="your-api-key-here"
```
Alternatively, you can edit the `utils.py` file to include your API key directly.
2. Install requirements and run the application:
```bash
pip install -r requirements.txt
python main.py
```
## How It Works
```mermaid
flowchart LR
user[UserInputNode] -->|validate| guardrail[GuardrailNode]
guardrail -->|retry| user
guardrail -->|process| llm[LLMNode]
llm -->|continue| user
```
The chat application uses:
- A `UserInputNode` that collects user input in its `exec` method
- A `GuardrailNode` that validates if the query is travel-related using:
- Basic validation checks (empty input, too short)
- LLM-based validation to determine if the query relates to travel
- An `LLMNode` that processes valid travel queries using GPT-4o with a travel advisor system prompt
- Flow connections that route inputs through validation before processing and handle retries for non-travel related queries
## Files
- [`main.py`](./main.py): Implementation of the nodes and chat flow
- [`utils.py`](./utils.py): Utilities for calling the OpenAI API
## Example Outputs
```
Welcome to the Travel Advisor Chat! Type 'exit' to end the conversation.
You: Plan my trip to Thailand
Travel Advisor: Thailand offers a blend of vibrant cities, serene beaches, and rich culture. Begin in Bangkok to explore the Grand Palace and local markets. Head north to Chiang Mai for temples and elephant sanctuaries. Fly south to the islands; Phuket or Koh Samui for beaches, diving, and nightlife. Consider visiting during the cool season (November-February) for pleasant weather. Accommodation ranges from budget hostels to luxury resorts. Internal flights, trains, and buses connect major destinations. Don’t miss local cuisine, from street food to fine dining. Ensure your passport is valid for six months and consider travel insurance for peace of mind. Enjoy!
You: exit
Goodbye! Safe travels!
```
```
Welcome to the Travel Advisor Chat! Type 'exit' to end the conversation.
You: How to study large language models?
Travel Advisor: The query is not related to travel advice, destinations, planning, or other travel topics. It is about studying large language models, which is a topic related to artificial intelligence and machine learning.
You: exit
Goodbye! Safe travels!
```
================================================
FILE: cookbook/pocketflow-chat-guardrail/main.py
================================================
from pocketflow import Node, Flow
from utils import call_llm
class UserInputNode(Node):
def prep(self, shared):
# Initialize messages if this is the first run
if "messages" not in shared:
shared["messages"] = []
print("Welcome to the Travel Advisor Chat! Type 'exit' to end the conversation.")
return None
def exec(self, _):
# Get user input
user_input = input("\nYou: ")
return user_input
def post(self, shared, prep_res, exec_res):
user_input = exec_res
# Check if user wants to exit
if user_input and user_input.lower() == 'exit':
print("\nGoodbye! Safe travels!")
return None # End the conversation
# Store user input in shared
shared["user_input"] = user_input
# Move to guardrail validation
return "validate"
class GuardrailNode(Node):
def prep(self, shared):
# Get the user input from shared data
user_input = shared.get("user_input", "")
return user_input
def exec(self, user_input):
# Basic validation checks
if not user_input or user_input.strip() == "":
return False, "Your query is empty. Please provide a travel-related question."
if len(user_input.strip()) < 3:
return False, "Your query is too short. Please provide more details about your travel question."
# LLM-based validation for travel topics
prompt = f"""
Evaluate if the following user query is related to travel advice, destinations, planning, or other travel topics.
The chat should ONLY answer travel-related questions and reject any off-topic, harmful, or inappropriate queries.
User query: {user_input}
Return your evaluation in YAML format:
```yaml
valid: true/false
reason: [Explain why the query is valid or invalid]
```"""
# Call LLM with the validation prompt
messages = [{"role": "user", "content": prompt}]
response = call_llm(messages)
# Extract YAML content
yaml_content = response.split("```yaml")[1].split("```")[0].strip() if "```yaml" in response else response
import yaml
result = yaml.safe_load(yaml_content)
assert result is not None, "Error: Invalid YAML format"
assert "valid" in result and "reason" in result, "Error: Invalid YAML format"
is_valid = result.get("valid", False)
reason = result.get("reason", "Missing reason in YAML response")
return is_valid, reason
def post(self, shared, prep_res, exec_res):
is_valid, message = exec_res
if not is_valid:
# Display error message to user
print(f"\nTravel Advisor: {message}")
# Skip LLM call and go back to user input
return "retry"
# Valid input, add to message history
shared["messages"].append({"role": "user", "content": shared["user_input"]})
# Proceed to LLM processing
return "process"
class LLMNode(Node):
def prep(self, shared):
# Add system message if not present
if not any(msg.get("role") == "system" for msg in shared["messages"]):
shared["messages"].insert(0, {
"role": "system",
"content": "You are a helpful travel advisor that provides information about destinations, travel planning, accommodations, transportation, activities, and other travel-related topics. Only respond to travel-related queries and keep responses informative and friendly. Your response are concise in 100 words."
})
# Return all messages for the LLM
return shared["messages"]
def exec(self, messages):
# Call LLM with the entire conversation history
response = call_llm(messages)
return response
def post(self, shared, prep_res, exec_res):
# Print the assistant's response
print(f"\nTravel Advisor: {exec_res}")
# Add assistant message to history
shared["messages"].append({"role": "assistant", "content": exec_res})
# Loop back to continue the conversation
return "continue"
# Create the flow with nodes and connections
user_input_node = UserInputNode()
guardrail_node = GuardrailNode()
llm_node = LLMNode()
# Create flow connections
user_input_node - "validate" >> guardrail_node
guardrail_node - "retry" >> user_input_node # Loop back if input is invalid
guardrail_node - "process" >> llm_node
llm_node - "continue" >> user_input_node # Continue conversation
flow = Flow(start=user_input_node)
# Start the chat
if __name__ == "__main__":
shared = {}
flow.run(shared)
================================================
FILE: cookbook/pocketflow-chat-guardrail/requirements.txt
================================================
pocketflow>=0.0.1
openai>=1.0.0
================================================
FILE: cookbook/pocketflow-chat-guardrail/utils.py
================================================
from openai import OpenAI
import os
def call_llm(messages):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.7
)
return response.choices[0].message.content
if __name__ == "__main__":
# Test the LLM call
messages = [{"role": "user", "content": "In a few words, what's the meaning of life?"}]
response = call_llm(messages)
print(f"Prompt: {messages[0]['content']}")
print(f"Response: {response}")
================================================
FILE: cookbook/pocketflow-chat-memory/README.md
================================================
# PocketFlow Chat with Memory
A chat application with memory retrieval using PocketFlow. This example maintains a sliding window of recent conversations while retrieving relevant past conversations based on context.
This implementation is based directly on the tutorial: [Build AI Agent Memory From Scratch — Tutorial For Dummies](https://zacharyhuang.substack.com/p/build-ai-agent-memory-from-scratch).
## Features
- Maintains a window of 3 most recent conversation pairs
- Archives older conversations with embeddings
- Uses vector similarity to retrieve the most relevant past conversation
- Combines recent context (3 pairs) with retrieved context (1 pair) for better responses
## Run It
1. Make sure your OpenAI API key is set:
```bash
export OPENAI_API_KEY="your-api-key-here"
```
2. Install requirements and run the application:
```bash
pip install -r requirements.txt
python main.py
```
## How It Works
```mermaid
flowchart LR
Question[GetUserQuestionNode] -->|retrieve| Retrieve[RetrieveNode]
Retrieve -->|answer| Answer[AnswerNode]
Answer -->|question| Question
Answer -->|embed| Embed[EmbedNode]
Embed -->|question| Question
```
The chat application uses:
- Four specialized nodes:
- `GetUserQuestionNode`: Handles interactive user input
- `RetrieveNode`: Finds relevant past conversations using vector similarity
- `AnswerNode`: Generates responses using both recent and retrieved context
- `EmbedNode`: Archives older conversations with embeddings
- A sliding window approach that maintains only the 3 most recent conversation pairs in active context
## Files
- [`nodes.py`](./nodes.py): Four node implementations with clear separation of concerns
- [`flow.py`](./flow.py): Chat flow structure definition
- [`main.py`](./main.py): Entry point for running the demo
- [`utils/`](./utils/): Utility functions for embeddings, LLM calls, and vector operations
## Example Output
```
==================================================
PocketFlow Chat with Memory
==================================================
This chat keeps your 3 most recent conversations
and brings back relevant past conversations when helpful
Type 'exit' to end the conversation
==================================================
Welcome to the interactive chat! Type 'exit' to end the conversation.
You: remember, my cat name is Whiskers and she only drinks from running water taps
Assistant: Got it! Whiskers only drinks from running water taps. If you have any questions or need advice related to her, feel free to ask!
You: remember, I have a peanut allergy but I can eat almonds and cashews without problems
Assistant: Understood! You have a peanut allergy but can safely eat almonds and cashews. If you need any assistance or information related to your dietary preferences or restrictions, just let me know!
You: remember, my anniversary is June 17th and I always forget to buy a gift until the last minute
Assistant: Got it! Your anniversary is on June 17th, and you often remember to buy a gift at the last minute. If you'd like, I can help you brainstorm gift ideas or set reminders as the date approaches!
You: remember, I lived in Portugal for 3 years when I was a child and still speak some Portuguese
Assistant: That's great to know! You lived in Portugal for three years during your childhood and still speak some Portuguese. If you ever want to practice or need help with anything related to Portuguese language or culture, feel free to ask!
✅ Added conversation to index at position 0
✅ Index now contains 1 conversations
You: remember, my favorite dessert is tiramisu but only if it's made with real mascarpone cheese
🔍 Finding relevant conversation for: remember, my favorite dessert ...
📄 Retrieved conversation (distance: 0.5008)
Assistant: Got it! Your favorite dessert is tiramisu, but only when it's made with real mascarpone cheese. If you ever need recommendations or recipes, just let me know!
✅ Added conversation to index at position 1
✅ Index now contains 2 conversations
You: remember, I collect vintage mechanical watches and my most valuable one is a 1965 Omega Seamaster
🔍 Finding relevant conversation for: remember, I collect vintage me...
📄 Retrieved conversation (distance: 0.5374)
Assistant: Got it! You collect vintage mechanical watches, and your most valuable piece is a 1965 Omega Seamaster. If you have questions about watches or need assistance with your collection, feel free to reach out!
✅ Added conversation to index at position 2
✅ Index now contains 3 conversations
You: what's my cat name?
🔍 Finding relevant conversation for: what's my cat name?...
📄 Retrieved conversation (distance: 0.3643)
Assistant: Your cat's name is Whiskers.
✅ Added conversation to index at position 3
✅ Index now contains 4 conversations
```
================================================
FILE: cookbook/pocketflow-chat-memory/flow.py
================================================
from pocketflow import Flow
from nodes import GetUserQuestionNode, RetrieveNode, AnswerNode, EmbedNode
def create_chat_flow():
# Create the nodes
question_node = GetUserQuestionNode()
retrieve_node = RetrieveNode()
answer_node = AnswerNode()
embed_node = EmbedNode()
# Connect the flow:
# 1. Start with getting a question
# 2. Retrieve relevant conversations
# 3. Generate an answer
# 4. Optionally embed old conversations
# 5. Loop back to get the next question
# Main flow path
question_node - "retrieve" >> retrieve_node
retrieve_node - "answer" >> answer_node
# When we need to embed old conversations
answer_node - "embed" >> embed_node
# Loop back for next question
answer_node - "question" >> question_node
embed_node - "question" >> question_node
# Create the flow starting with question node
return Flow(start=question_node)
# Initialize the flow
chat_flow = create_chat_flow()
================================================
FILE: cookbook/pocketflow-chat-memory/main.py
================================================
from flow import chat_flow
def run_chat_memory_demo():
"""
Run an interactive chat interface with memory retrieval.
Features:
1. Maintains a window of the 3 most recent conversation pairs
2. Archives older conversations with embeddings
3. Retrieves 1 relevant past conversation when needed
4. Total context to LLM: 3 recent pairs + 1 retrieved pair
"""
print("=" * 50)
print("PocketFlow Chat with Memory")
print("=" * 50)
print("This chat keeps your 3 most recent conversations")
print("and brings back relevant past conversations when helpful")
print("Type 'exit' to end the conversation")
print("=" * 50)
# Run the chat flow
chat_flow.run({})
if __name__ == "__main__":
run_chat_memory_demo()
================================================
FILE: cookbook/pocketflow-chat-memory/nodes.py
================================================
from pocketflow import Node
from utils.vector_index import create_index, add_vector, search_vectors
from utils.call_llm import call_llm
from utils.get_embedding import get_embedding
class GetUserQuestionNode(Node):
def prep(self, shared):
"""Initialize messages if first run"""
if "messages" not in shared:
shared["messages"] = []
print("Welcome to the interactive chat! Type 'exit' to end the conversation.")
return None
def exec(self, _):
"""Get user input interactively"""
# Get interactive input from user
user_input = input("\nYou: ")
# Check if user wants to exit
if user_input.lower() == 'exit':
return None
return user_input
def post(self, shared, prep_res, exec_res):
# If exec_res is None, the user wants to exit
if exec_res is None:
print("\nGoodbye!")
return None # End the conversation
# Add user message to current messages
shared["messages"].append({"role": "user", "content": exec_res})
return "retrieve"
class AnswerNode(Node):
def prep(self, shared):
"""Prepare context for the LLM"""
if not shared.get("messages"):
return None
# 1. Get the last 3 conversation pairs (or fewer if not available)
recent_messages = shared["messages"][-6:] if len(shared["messages"]) > 6 else shared["messages"]
# 2. Add the retrieved relevant conversation if available
context = []
if shared.get("retrieved_conversation"):
# Add a system message to indicate this is a relevant past conversation
context.append({
"role": "system",
"content": "The following is a relevant past conversation that may help with the current query:"
})
context.extend(shared["retrieved_conversation"])
context.append({
"role": "system",
"content": "Now continue the current conversation:"
})
# 3. Add the recent messages
context.extend(recent_messages)
return context
def exec(self, messages):
"""Generate a response using the LLM"""
if messages is None:
return None
# Call LLM with the context
response = call_llm(messages)
return response
def post(self, shared, prep_res, exec_res):
"""Process the LLM response"""
if prep_res is None or exec_res is None:
return None # End the conversation
# Print the assistant's response
print(f"\nAssistant: {exec_res}")
# Add assistant message to history
shared["messages"].append({"role": "assistant", "content": exec_res})
# If we have more than 6 messages (3 conversation pairs), archive the oldest pair
if len(shared["messages"]) > 6:
return "embed"
# We only end if the user explicitly typed 'exit'
# Even if last_question is set, we continue in interactive mode
return "question"
class EmbedNode(Node):
def prep(self, shared):
"""Extract the oldest conversation pair for embedding"""
if len(shared["messages"]) <= 6:
return None
# Extract the oldest user-assistant pair
oldest_pair = shared["messages"][:2]
# Remove them from current messages
shared["messages"] = shared["messages"][2:]
return oldest_pair
def exec(self, conversation):
"""Embed a conversation"""
if not conversation:
return None
# Combine user and assistant messages into a single text for embedding
user_msg = next((msg for msg in conversation if msg["role"] == "user"), {"content": ""})
assistant_msg = next((msg for msg in conversation if msg["role"] == "assistant"), {"content": ""})
combined = f"User: {user_msg['content']} Assistant: {assistant_msg['content']}"
# Generate embedding
embedding = get_embedding(combined)
return {
"conversation": conversation,
"embedding": embedding
}
def post(self, shared, prep_res, exec_res):
"""Store the embedding and add to index"""
if not exec_res:
# If there's nothing to embed, just continue with the next question
return "question"
# Initialize vector index if not exist
if "vector_index" not in shared:
shared["vector_index"] = create_index()
shared["vector_items"] = [] # Track items separately
# Add the embedding to the index and store the conversation
position = add_vector(shared["vector_index"], exec_res["embedding"])
shared["vector_items"].append(exec_res["conversation"])
print(f"✅ Added conversation to index at position {position}")
print(f"✅ Index now contains {len(shared['vector_items'])} conversations")
# Continue with the next question
return "question"
class RetrieveNode(Node):
def prep(self, shared):
"""Get the current query for retrieval"""
if not shared.get("messages"):
return None
# Get the latest user message for searching
latest_user_msg = next((msg for msg in reversed(shared["messages"])
if msg["role"] == "user"), {"content": ""})
# Check if we have a vector index with items
if ("vector_index" not in shared or
"vector_items" not in shared or
len(shared["vector_items"]) == 0):
return None
return {
"query": latest_user_msg["content"],
"vector_index": shared["vector_index"],
"vector_items": shared["vector_items"]
}
def exec(self, inputs):
"""Find the most relevant past conversation"""
if not inputs:
return None
query = inputs["query"]
vector_index = inputs["vector_index"]
vector_items = inputs["vector_items"]
print(f"🔍 Finding relevant conversation for: {query[:30]}...")
# Create embedding for the query
query_embedding = get_embedding(query)
# Search for the most similar conversation
indices, distances = search_vectors(vector_index, query_embedding, k=1)
if not indices:
return None
# Get the corresponding conversation
conversation = vector_items[indices[0]]
return {
"conversation": conversation,
"distance": distances[0]
}
def post(self, shared, prep_res, exec_res):
"""Store the retrieved conversation"""
if exec_res is not None:
shared["retrieved_conversation"] = exec_res["conversation"]
print(f"📄 Retrieved conversation (distance: {exec_res['distance']:.4f})")
else:
shared["retrieved_conversation"] = None
return "answer"
================================================
FILE: cookbook/pocketflow-chat-memory/requirements.txt
================================================
pocketflow>=0.0.2
numpy>=1.20.0
faiss-cpu>=1.7.0
openai>=1.0.0
================================================
FILE: cookbook/pocketflow-chat-memory/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-chat-memory/utils/call_llm.py
================================================
import os
from openai import OpenAI
def call_llm(messages):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.7
)
return response.choices[0].message.content
if __name__ == "__main__":
# Test the LLM call
messages = [{"role": "user", "content": "In a few words, what's the meaning of life?"}]
response = call_llm(messages)
print(f"Prompt: {messages[0]['content']}")
print(f"Response: {response}")
================================================
FILE: cookbook/pocketflow-chat-memory/utils/get_embedding.py
================================================
import os
import numpy as np
from openai import OpenAI
def get_embedding(text):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "YOUR_API_KEY"))
response = client.embeddings.create(
model="text-embedding-ada-002",
input=text
)
# Extract the embedding vector from the response
embedding = response.data[0].embedding
# Convert to numpy array for consistency with other embedding functions
return np.array(embedding, dtype=np.float32)
if __name__ == "__main__":
# Test the embedding function
text1 = "The quick brown fox jumps over the lazy dog."
text2 = "Python is a popular programming language for data science."
emb1 = get_embedding(text1)
emb2 = get_embedding(text2)
print(f"Embedding 1 shape: {emb1.shape}")
print(f"Embedding 2 shape: {emb2.shape}")
# Calculate similarity (dot product)
similarity = np.dot(emb1, emb2)
print(f"Similarity between texts: {similarity:.4f}")
================================================
FILE: cookbook/pocketflow-chat-memory/utils/vector_index.py
================================================
import numpy as np
import faiss
def create_index(dimension=1536):
return faiss.IndexFlatL2(dimension)
def add_vector(index, vector):
# Make sure the vector is a numpy array with the right shape for FAISS
vector = np.array(vector).reshape(1, -1).astype(np.float32)
# Add the vector to the index
index.add(vector)
# Return the position (index.ntotal is the total number of vectors in the index)
return index.ntotal - 1
def search_vectors(index, query_vector, k=1):
"""Search for the k most similar vectors to the query vector
Args:
index: The FAISS index
query_vector: The query vector (numpy array or list)
k: Number of results to return (default: 1)
Returns:
tuple: (indices, distances) where:
- indices is a list of positions in the index
- distances is a list of the corresponding distances
"""
# Make sure we don't try to retrieve more vectors than exist in the index
k = min(k, index.ntotal)
if k == 0:
return [], []
# Make sure the query is a numpy array with the right shape for FAISS
query_vector = np.array(query_vector).reshape(1, -1).astype(np.float32)
# Search the index
distances, indices = index.search(query_vector, k)
return indices[0].tolist(), distances[0].tolist()
# Example usage
if __name__ == "__main__":
# Create a new index
index = create_index(dimension=3)
# Add some random vectors and track them separately
items = []
for i in range(5):
vector = np.random.random(3)
position = add_vector(index, vector)
items.append(f"Item {i}")
print(f"Added vector at position {position}")
print(f"Index contains {index.ntotal} vectors")
# Search for a similar vector
query = np.random.random(3)
indices, distances = search_vectors(index, query, k=2)
print("Query:", query)
print("Found indices:", indices)
print("Distances:", distances)
print("Retrieved items:", [items[idx] for idx in indices])
================================================
FILE: cookbook/pocketflow-cli-hitl/README.md
================================================
# PocketFlow Command-Line Joke Generator (Human-in-the-Loop Example)
A simple, interactive command-line application that generates jokes based on user-provided topics and direct human feedback. This serves as a clear example of a Human-in-the-Loop (HITL) workflow orchestrated by PocketFlow.
## Features
- **Interactive Joke Generation**: Ask for jokes on any topic.
- **Human-in-the-Loop Feedback**: Dislike a joke? Your feedback directly influences the next generation attempt.
- **Minimalist Design**: A straightforward example of using PocketFlow for HITL tasks.
- **Powered by LLMs**: (Uses Anthropic Claude via an API call for joke generation).
## Getting Started
This project is part of the PocketFlow cookbook examples. It's assumed you have already cloned the [PocketFlow repository](https://github.com/the-pocket/PocketFlow) and are in the `cookbook/pocketflow-cli-hitl` directory.
1. **Install required dependencies**:
```bash
pip install -r requirements.txt
```
2. **Set up your Anthropic API key**:
The application uses Anthropic Claude to generate jokes. You need to set your API key as an environment variable.
```bash
export ANTHROPIC_API_KEY="your-anthropic-api-key-here"
```
You can test if your `call_llm.py` utility is working by running it directly:
```bash
python utils/call_llm.py
```
3. **Run the Joke Generator**:
```bash
python main.py
```
## How It Works
The system uses a simple PocketFlow workflow:
```mermaid
flowchart TD
GetTopic[GetTopicNode] --> GenerateJoke[GenerateJokeNode]
GenerateJoke --> GetFeedback[GetFeedbackNode]
GetFeedback -- "Approve" --> Z((End))
GetFeedback -- "Disapprove" --> GenerateJoke
```
1. **GetTopicNode**: Prompts the user to enter a topic for the joke.
2. **GenerateJokeNode**: Sends the topic (and any previously disliked jokes as context) to an LLM to generate a new joke.
3. **GetFeedbackNode**: Shows the joke to the user and asks if they liked it.
* If **yes** (approved), the application ends.
* If **no** (disapproved), the disliked joke is recorded, and the flow loops back to `GenerateJokeNode` to try again.
## Sample Output
Here's an example of an interaction with the Joke Generator:
```
Welcome to the Command-Line Joke Generator!
What topic would you like a joke about? Pocket Flow: 100-line LLM framework
Joke: Pocket Flow: Finally, an LLM framework that fits in your pocket! Too bad your model still needs a data center.
Did you like this joke? (yes/no): no
Okay, let me try another one.
Joke: Pocket Flow: A 100-line LLM framework where 99 lines are imports and the last line is `print("TODO: implement intelligence")`.
Did you like this joke? (yes/no): yes
Great! Glad you liked it.
Thanks for using the Joke Generator!
```
## Files
- [`main.py`](./main.py): Entry point for the application.
- [`flow.py`](./flow.py): Defines the PocketFlow graph and node connections.
- [`nodes.py`](./nodes.py): Contains the definitions for `GetTopicNode`, `GenerateJokeNode`, and `GetFeedbackNode`.
- [`utils/call_llm.py`](./utils/call_llm.py): Utility function to interact with the LLM (Anthropic Claude).
- [`requirements.txt`](./requirements.txt): Lists project dependencies.
- [`docs/design.md`](./docs/design.md): The design document for this application.
================================================
FILE: cookbook/pocketflow-cli-hitl/docs/design.md
================================================
# Design Doc: Command-Line Joke Generator
> Please DON'T remove notes for AI
## Requirements
> Notes for AI: Keep it simple and clear.
> If the requirements are abstract, write concrete user stories
The system will be a command-line application that:
1. Asks the user for a topic for a joke.
2. Generates a joke based on the provided topic.
3. Asks the user if they approve of the joke.
4. If the user approves, the application can end or offer to generate another joke (for simplicity, we'll end for now).
5. If the user does not approve, the application should:
a. Take note that the user disliked the previous joke.
b. Generate a new joke about the same topic, attempting to make it different from the disliked one.
c. Repeat step 3.
## Flow Design
> Notes for AI:
> 1. Consider the design patterns of agent, map-reduce, rag, and workflow. Apply them if they fit.
> 2. Present a concise, high-level description of the workflow.
### Applicable Design Pattern:
**Agent**: The system acts as an agent that interacts with the user. It takes user input (topic, feedback), performs an action (generates a joke), and then decides the next step based on user feedback (either end or try generating another joke). This iterative process of generation and feedback fits the agent pattern.
### Flow high-level Design:
1. **GetTopicNode**: Prompts the user to enter the topic for the joke.
2. **GenerateJokeNode**: Generates a joke based on the topic and any previous feedback.
3. **GetFeedbackNode**: Presents the joke to the user and asks for approval. Based on the feedback, it either transitions to end the flow or back to `GenerateJokeNode`.
```mermaid
flowchart TD
GetTopic[GetTopicNode] --> GenerateJoke[GenerateJokeNode]
GenerateJoke --> GetFeedback[GetFeedbackNode]
GetFeedback -- "Approve" --> Z((End))
GetFeedback -- "Disapprove" --> GenerateJoke
```
## Utility Functions
> Notes for AI:
> 1. Understand the utility function definition thoroughly by reviewing the doc.
> 2. Include only the necessary utility functions, based on nodes in the flow.
1. **Call LLM** (`utils/call_llm.py`)
* *Input*: `prompt` (str), potentially including context like previously disliked jokes.
* *Output*: `response` (str) - the generated joke.
* *Necessity*: Used by `GenerateJokeNode` to generate jokes.
## Node Design
### Shared Store
> Notes for AI: Try to minimize data redundancy
The shared store structure is organized as follows:
```python
shared = {
"topic": None, # Stores the user-provided joke topic
"current_joke": None, # Stores the most recently generated joke
"disliked_jokes": [], # A list to store jokes the user didn't like, for context
"user_feedback": None # Stores the user's latest feedback (e.g., "approve", "disapprove")
}
```
### Node Steps
> Notes for AI: Carefully decide whether to use Batch/Async Node/Flow.
1. **GetTopicNode**
* *Purpose*: To get the desired joke topic from the user.
* *Type*: Regular
* *Steps*:
* `prep`: (None needed for the first run, or could check if a topic already exists if we were to loop for a new topic)
* `exec`: Prompt the user via `input()` for a joke topic.
* `post`: Store the user's input topic into `shared["topic"]`. Return `"default"` action to proceed to `GenerateJokeNode`.
2. **GenerateJokeNode**
* *Purpose*: To generate a joke using an LLM, based on the topic and any previously disliked jokes.
* *Type*: Regular
* *Steps*:
* `prep`: Read `shared["topic"]` and `shared["disliked_jokes"]`. Construct a prompt for the LLM, including the topic and a message like "The user did not like the following jokes: [list of disliked jokes]. Please generate a new, different joke about [topic]."
* `exec`: Call the `call_llm` utility function with the prepared prompt.
* `post`: Store the generated joke in `shared["current_joke"]`. Print the joke to the console. Return `"default"` action to proceed to `GetFeedbackNode`.
3. **GetFeedbackNode**
* *Purpose*: To get feedback from the user about the generated joke and decide the next step.
* *Type*: Regular
* *Steps*:
* `prep`: Read `shared["current_joke"]`.
* `exec`: Prompt the user (e.g., "Did you like this joke? (yes/no) or (approve/disapprove): "). Get user's input.
* `post`:
* If user input indicates approval (e.g., "yes", "approve"):
* Store "approve" in `shared["user_feedback"]`.
* Return `"Approve"` action (leading to flow termination or a thank you message).
* If user input indicates disapproval (e.g., "no", "disapprove"):
* Store "disapprove" in `shared["user_feedback"]`.
* Add `shared["current_joke"]` to the `shared["disliked_jokes"]` list.
* Return `"Disapprove"` action (leading back to `GenerateJokeNode`).
================================================
FILE: cookbook/pocketflow-cli-hitl/flow.py
================================================
from pocketflow import Flow
from nodes import GetTopicNode, GenerateJokeNode, GetFeedbackNode
def create_joke_flow() -> Flow:
"""Creates and returns the joke generation flow."""
get_topic_node = GetTopicNode()
generate_joke_node = GenerateJokeNode()
get_feedback_node = GetFeedbackNode()
get_topic_node >> generate_joke_node
generate_joke_node >> get_feedback_node
get_feedback_node - "Disapprove" >> generate_joke_node
joke_flow = Flow(start=get_topic_node)
return joke_flow
================================================
FILE: cookbook/pocketflow-cli-hitl/main.py
================================================
from flow import create_joke_flow
def main():
"""Main function to run the joke generator application."""
print("Welcome to the Command-Line Joke Generator!")
shared = {
"topic": None,
"current_joke": None,
"disliked_jokes": [],
"user_feedback": None
}
joke_flow = create_joke_flow()
joke_flow.run(shared)
print("\nThanks for using the Joke Generator!")
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-cli-hitl/nodes.py
================================================
from pocketflow import Node
from utils.call_llm import call_llm
class GetTopicNode(Node):
"""Prompts the user to enter the topic for the joke."""
def exec(self, _shared):
return input("What topic would you like a joke about? ")
def post(self, shared, _prep_res, exec_res):
shared["topic"] = exec_res
class GenerateJokeNode(Node):
"""Generates a joke based on the topic and any previous feedback."""
def prep(self, shared):
topic = shared.get("topic", "anything")
disliked_jokes = shared.get("disliked_jokes", [])
prompt = f"Please generate an one-liner joke about: {topic}. Make it short and funny."
if disliked_jokes:
disliked_str = "; ".join(disliked_jokes)
prompt = f"The user did not like the following jokes: [{disliked_str}]. Please generate a new, different joke about {topic}."
return prompt
def exec(self, prep_res):
return call_llm(prep_res)
def post(self, shared, _prep_res, exec_res):
shared["current_joke"] = exec_res
print(f"\nJoke: {exec_res}")
class GetFeedbackNode(Node):
"""Presents the joke to the user and asks for approval."""
def exec(self, _prep_res):
while True:
feedback = input("Did you like this joke? (yes/no): ").strip().lower()
if feedback in ["yes", "y", "no", "n"]:
return feedback
print("Invalid input. Please type 'yes' or 'no'.")
def post(self, shared, _prep_res, exec_res):
if exec_res in ["yes", "y"]:
shared["user_feedback"] = "approve"
print("Great! Glad you liked it.")
return "Approve"
else:
shared["user_feedback"] = "disapprove"
current_joke = shared.get("current_joke")
if current_joke:
if "disliked_jokes" not in shared:
shared["disliked_jokes"] = []
shared["disliked_jokes"].append(current_joke)
print("Okay, let me try another one.")
return "Disapprove"
================================================
FILE: cookbook/pocketflow-cli-hitl/requirements.txt
================================================
pocketflow>=0.0.1
anthropic>=0.20.0 # Or a recent version
================================================
FILE: cookbook/pocketflow-cli-hitl/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-cli-hitl/utils/call_llm.py
================================================
from anthropic import Anthropic
import os
def call_llm(prompt: str) -> str:
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "your-anthropic-api-key")) # Default if key not found
response = client.messages.create(
model="claude-3-haiku-20240307", # Using a smaller model for jokes
max_tokens=150, # Jokes don't need to be very long
messages=[
{"role": "user", "content": prompt}
]
)
return response.content[0].text
if __name__ == "__main__":
print("Testing Anthropic LLM call for jokes:")
joke_prompt = "Tell me a one-liner joke about a cat."
print(f"Prompt: {joke_prompt}")
try:
response = call_llm(joke_prompt)
print(f"Response: {response}")
except Exception as e:
print(f"Error calling LLM: {e}")
print("Please ensure your ANTHROPIC_API_KEY environment variable is set correctly.")
================================================
FILE: cookbook/pocketflow-code-generator/README.md
================================================
# PocketFlow Code Generator
An intelligent AI system that takes LeetCode-style coding problems and automatically generates comprehensive test cases, implements solutions, and iteratively improves them until all tests pass.
- Check out the [Substack Post Tutorial](https://pocketflow.substack.com/p/build-your-own-ai-code-generator) for more!
## Features
- **Automatic Test Case Generation**: Creates diverse test cases including edge cases
- **Intelligent Code Implementation**: Generates `run_code` functions with proper algorithms
- **Iterative Improvement**: Analyzes failures and decides whether to revise tests or code
- **Rich Debugging Output**: Detailed progress tracking and validation
## Getting Started
1. Install required dependencies:
```bash
pip install -r requirements.txt
```
2. Set up your Anthropic API key:
```bash
export ANTHROPIC_API_KEY="your-api-key-here"
```
Test your API key is working:
```bash
python utils/call_llm.py
```
3. Run the code generator with the default Two Sum problem:
```bash
python main.py
```
4. Or provide your own problem:
```bash
python main.py "Reverse a linked list. Given the head of a singly linked list, reverse the list and return the reversed list."
```
## How It Works
The system follows an intelligent workflow combining **Agent** and **Workflow** design patterns:
```mermaid
flowchart TD
start[Problem Input] --> generateTests[Generate Test Cases]
generateTests --> implement[Implement Function]
implement --> runTests[Run Tests - Batch]
runTests --> decision{All Tests Pass?}
decision -->|Yes| success[Success!]
decision -->|No| revise[Revise - Agent Decision]
revise --> runTests
decision -->|Max Iterations| maxIter[Max Iterations Reached]
```
### The Process
1. **GenerateTestCases**: Creates 5-7 comprehensive test cases from problem description
2. **ImplementFunction**: Writes a `run_code` function based on problem and test cases
3. **RunTests**: Executes function against all test cases using batch processing
4. **Revise**: Analyzes failures and makes intelligent decisions to revise test cases and/or function code
5. **Loop**: Continues until all tests pass or max iterations reached
## Sample Output
Here's what you'll see when running the Two Sum example:
```
Starting PocketFlow Code Generator...
=== Generated 7 Test Cases ===
1. Basic case - solution at beginning
input: {'nums': [2, 7, 11, 15], 'target': 9}
expected: [0, 1]
2. Basic case - solution in middle
input: {'nums': [3, 2, 4], 'target': 6}
expected: [1, 2]
3. Edge case - minimum array size with duplicates
input: {'nums': [3, 3], 'target': 6}
expected: [0, 1]
4. Case with negative numbers
input: {'nums': [-1, -2, -3, -4, -5], 'target': -8}
expected: [2, 4]
5. Case with zero and negative target
input: {'nums': [0, 4, 3, 0], 'target': 0}
expected: [0, 3]
6. Case with solution at the end
input: {'nums': [1, 2, 3, 4, 5, 6], 'target': 11}
expected: [4, 5]
7. Larger array case
input: {'nums': [5, 75, 25, 45, 42, 2, 11, 9, 55, 12], 'target': 14}
expected: [2, 6]
=== Implemented Function ===
def run_code(nums, target):
# Dictionary to store number -> index mapping
num_to_index = {}
# Iterate through the array
for i, num in enumerate(nums):
# Calculate what number we need to reach the target
complement = target - num
# Check if the complement exists in our map
if complement in num_to_index:
# Found the pair! Return indices
return [num_to_index[complement], i]
# Store current number and its index
num_to_index[num] = i
# Should never reach here given problem constraints
return []
=== Test Results: 6/7 Passed ===
Failed tests:
1. Larger array case:
error: Expected [2, 6], got [0, 7]
expected: [2, 6]
=== Revisions (Iteration 1) ===
Revising test cases:
Test 7: 'Larger array case' -> 'Larger array case'
old input: {'nums': [5, 75, 25, 45, 42, 2, 11, 9, 55, 12], 'target': 14}
new input: {'nums': [5, 75, 25, 45, 42, 2, 11, 9, 55, 12], 'target': 14}
old expected: [2, 6]
new expected: [0, 7]
=== Test Results: 7/7 Passed ===
```
## Key Features
### Intelligent Decision Making
The **Revise** node acts as an agent that analyzes test failures and decides whether to:
- Fix test cases (if they have incorrect expected outputs)
- Fix the function implementation (if the logic is wrong)
- Or both
### Structured Output with Validation
All LLM interactions use YAML format with:
- **Reasoning fields**: Transparent decision-making process
- **Validation asserts**: Ensures outputs match expected structure
- **Rich debugging**: Comprehensive logging of all steps
### Batch Processing
The **RunTests** node uses PocketFlow's BatchNode to efficiently test the function against all test cases in parallel.
## Files
- [`main.py`](./main.py): Entry point with sample Two Sum problem
- [`flow.py`](./flow.py): Connects all nodes into the complete workflow
- [`nodes.py`](./nodes.py): Core logic nodes with validation and debugging
- [`utils/call_llm.py`](./utils/call_llm.py): Anthropic Claude API wrapper
- [`utils/code_executor.py`](./utils/code_executor.py): Safe Python code execution utility
- [`doc/design.md`](./doc/design.md): Detailed system design documentation
## Design Patterns Used
- **[Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html)**: Sequential steps of test generation → coding → testing
- **[Agent](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html)**: Intelligent decision-making when tests fail
- **[Batch](https://the-pocket.github.io/PocketFlow/core_abstraction/batch.html)**: Efficient parallel test execution
- **[Structured Output](https://the-pocket.github.io/PocketFlow/design_pattern/structure.html)**: YAML validation for reliable LLM outputs
================================================
FILE: cookbook/pocketflow-code-generator/doc/design.md
================================================
# Design Doc: PocketFlow Code Generator
> Please DON'T remove notes for AI
## Requirements
> Notes for AI: Keep it simple and clear.
> If the requirements are abstract, write concrete user stories
**User Story**: As a developer, I want an AI system that can take a LeetCode-style coding problem and automatically:
1. Generate comprehensive test cases including edge cases
2. Implement a solution function
3. Test the implementation against the test cases
4. When tests fail, intelligently decide whether to revise the test cases or the function
5. Iterate until all tests pass
**Sample Problem**: Two Sum - Given an array of integers and a target, return indices of two numbers that add up to the target.
This is well-suited for AI because:
- ✅ Routine task: Test case generation follows patterns
- ✅ Creative task: Code generation from clear problem descriptions
- ✅ Clear decision criteria: Whether to revise tests vs implementation
## Flow Design
> Notes for AI:
> 1. Consider the design patterns of agent, map-reduce, rag, and workflow. Apply them if they fit.
> 2. Present a concise, high-level description of the workflow.
### Applicable Design Pattern:
1. **Workflow Pattern**: Sequential steps of test generation → coding → testing
2. **Agent Pattern**: Decision-making when tests fail with structured output
- *Context*: Test results, current test cases, and function code
- *Actions*: Structured output to revise test cases and/or function
### Flow high-level Design:
1. **Generate Test Cases**: Create comprehensive input/output test pairs from problem description
2. **Implement Function**: Write `def run_code` function based on problem and current test cases
3. **Run Tests**: Execute function against all test cases using batch processing
4. **Revise**: Analyze failures and output structured revisions for test cases and/or function
5. **Loop back to Run Tests** until all pass
```mermaid
flowchart TD
start[Problem Input] --> generateTests[Generate Test Cases]
generateTests --> implement[Implement Function]
implement --> runTests[Run Tests - Batch]
runTests --> decision{All Tests Pass?}
decision -->|Yes| success[Success!]
decision -->|No| revise[Revise]
revise --> runTests
```
## Utility Functions
> Notes for AI:
> 1. Understand the utility function definition thoroughly by reviewing the doc.
> 2. Include only the necessary utility functions, based on nodes in the flow.
1. **Call LLM** (`utils/call_llm.py`)
- *Input*: prompt (str)
- *Output*: response (str)
- Used by all LLM-powered nodes for generating tests, code, and analysis
2. **Execute Python Code** (`utils/code_executor.py`)
- *Input*: function_code (str), input (dict/list/any)
- *Output*: output (any), error (str)
- Used by RunTests batch node to safely execute generated code against individual input
## Node Design
### Shared Memory
> Notes for AI: Try to minimize data redundancy
The shared memory structure is organized as follows:
```python
shared = {
"problem": "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.",
"test_cases": [
{"name": "Basic case", "input": {"nums": [2,7,11,15], "target": 9}, "expected": [0,1]},
{"name": "Different order", "input": {"nums": [3,2,4], "target": 6}, "expected": [1,2]},
# ... more test cases
],
"function_code": "def run_code(nums, target): ...",
"test_results": [
{"test_case": {...}, "passed": True/False, "error": "..."},
# ... results for each test case
],
"iteration_count": 0,
"max_iterations": 5
}
```
### Node Steps
> Notes for AI: Carefully decide whether to use Batch/Async Node/Flow.
1. **GenerateTestCases Node**
- *Purpose*: Create comprehensive test cases including edge cases from problem description
- *Type*: Regular Node
- *Steps*:
- *prep*: Read problem description from shared store
- *exec*: Call LLM to generate diverse test cases in structured format
- *post*: Store test cases directly in shared["test_cases"]
2. **ImplementFunction Node**
- *Purpose*: Generate `def run_code` function based on problem and current test cases
- *Type*: Regular Node
- *Steps*:
- *prep*: Read problem description and test cases from shared store
- *exec*: Call LLM to implement `def run_code` function with clean code output
- *post*: Store function code directly in shared["function_code"]
3. **RunTests Node**
- *Purpose*: Execute function against all test cases using batch processing
- *Type*: Batch Node
- *Steps*:
- *prep*: Read function code from shared store, return list of test cases
- *exec*: Use code executor utility to run function against each individual test case
- *post*: Store all results in shared["test_results"], return "success" if all pass else "failure"
4. **Revise Node** (Agent with Structured Output)
- *Purpose*: Analyze test failures and output structured revisions for test cases and/or function
- *Type*: Regular Node (Agent decision-making)
- *Steps*:
- *prep*: Read test results, test cases, function code, iteration count from shared store
- *exec*: Call LLM to analyze failures and output structured YAML with revised test cases and/or function code
- *post*: Update shared["test_cases"] and/or shared["function_code"] based on structured output
================================================
FILE: cookbook/pocketflow-code-generator/flow.py
================================================
from pocketflow import Flow
from nodes import GenerateTestCases, ImplementFunction, RunTests, Revise
def create_code_generator_flow():
"""Creates and returns the code generator flow."""
# Create nodes
generate_tests = GenerateTestCases()
implement_function = ImplementFunction()
run_tests = RunTests()
revise = Revise()
# Define transitions
generate_tests >> implement_function
implement_function >> run_tests
run_tests - "failure" >> revise
revise >> run_tests
# Create flow starting with test generation
flow = Flow(start=generate_tests)
return flow
================================================
FILE: cookbook/pocketflow-code-generator/main.py
================================================
import sys
from flow import create_code_generator_flow
def main():
"""Runs the PocketFlow Code Generator application."""
print("Starting PocketFlow Code Generator...")
# Check if problem is provided as argument
if len(sys.argv) > 1:
problem = " ".join(sys.argv[1:])
else:
# Default Two Sum problem
problem = """Two Sum
Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.
You may assume that each input would have exactly one solution, and you may not use the same element twice.
Example 1:
Input: nums = [2,7,11,15], target = 9
Output: [0,1]
Example 2:
Input: nums = [3,2,4], target = 6
Output: [1,2]
Example 3:
Input: nums = [3,3], target = 6
Output: [0,1]"""
shared = {
"problem": problem,
"test_cases": [], # Will be populated with [{name, input, expected}, ...]
"function_code": "",
"test_results": [],
"iteration_count": 0,
"max_iterations": 5
}
# Create and run the flow
flow = create_code_generator_flow()
flow.run(shared)
print("\n=== Final Results ===")
print(f"Problem: {shared['problem'][:50]}...")
print(f"Iterations: {shared['iteration_count']}")
print(f"Function:\n{shared['function_code']}")
print(f"Test Results: {len([r for r in shared['test_results'] if r['passed']])}/{len(shared['test_results'])} passed")
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-code-generator/nodes.py
================================================
import yaml
from pocketflow import Node, BatchNode
from utils.call_llm import call_llm
from utils.code_executor import execute_python
class GenerateTestCases(Node):
def prep(self, shared):
return shared["problem"]
def exec(self, problem):
prompt = f"""Generate 5-7 test cases for this coding problem:
{problem}
Output in this YAML format with reasoning:
```yaml
reasoning: |
The input parameters should be: param1 as a string, and param2 as a number.
To test the function, I will consider basic cases, edge cases, and corner cases.
For this problem, I need to test...
test_cases:
- name: "Basic case"
input: {{param1: value1, param2: value2}}
expected: result1
- name: "Edge case - empty"
input: {{param1: value3, param2: value4}}
expected: result2
```"""
response = call_llm(prompt)
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
result = yaml.safe_load(yaml_str)
# Validation asserts
assert "test_cases" in result, "Result must have 'test_cases' field"
assert isinstance(result["test_cases"], list), "test_cases must be a list"
for i, test_case in enumerate(result["test_cases"]):
assert "name" in test_case, f"Test case {i} missing 'name' field"
assert isinstance(test_case["name"], str), f"Test case {i} 'name' must be string"
assert "input" in test_case, f"Test case {i} missing 'input' field"
assert isinstance(test_case["input"], dict), f"Test case {i} 'input' must be dict"
assert "expected" in test_case, f"Test case {i} missing 'expected' field"
return result
def post(self, shared, prep_res, exec_res):
shared["test_cases"] = exec_res["test_cases"]
# Print all generated test cases
print(f"\n=== Generated {len(exec_res['test_cases'])} Test Cases ===")
for i, test_case in enumerate(exec_res["test_cases"], 1):
print(f"{i}. {test_case['name']}")
print(f" input: {test_case['input']}")
print(f" expected: {test_case['expected']}")
class ImplementFunction(Node):
def prep(self, shared):
return shared["problem"], shared["test_cases"]
def exec(self, inputs):
problem, test_cases = inputs
# Format test cases nicely for the prompt
formatted_tests = ""
for i, test in enumerate(test_cases, 1):
formatted_tests += f"{i}. {test['name']}\n"
formatted_tests += f" input: {test['input']}\n"
formatted_tests += f" expected: {test['expected']}\n\n"
prompt = f"""Implement a solution for this problem:
{problem}
Test cases to consider:
{formatted_tests}
IMPORTANT: The function name must be exactly "run_code"
Output in this YAML format:
```yaml
reasoning: |
To implement this function, I will...
My approach is...
function_code: |
def run_code(...):
# your implementation
return result
```"""
response = call_llm(prompt)
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
result = yaml.safe_load(yaml_str)
# Validation asserts
assert "function_code" in result, "Result must have 'function_code' field"
assert isinstance(result["function_code"], str), "function_code must be string"
assert "def run_code" in result["function_code"], "Function must be named 'run_code'"
return result["function_code"]
def post(self, shared, prep_res, exec_res):
shared["function_code"] = exec_res
# Print the implemented function
print(f"\n=== Implemented Function ===")
print(exec_res)
class RunTests(BatchNode):
def prep(self, shared):
function_code = shared["function_code"]
test_cases = shared["test_cases"]
# Return list of tuples (function_code, test_case)
return [(function_code, test_case) for test_case in test_cases]
def exec(self, test_data):
function_code, test_case = test_data
output, error = execute_python(function_code, test_case["input"])
if error:
return {
"test_case": test_case,
"passed": False,
"actual": None,
"expected": test_case["expected"],
"error": error
}
passed = output == test_case["expected"]
return {
"test_case": test_case,
"passed": passed,
"actual": output,
"expected": test_case["expected"],
"error": None if passed else f"Expected {test_case['expected']}, got {output}"
}
def post(self, shared, prep_res, exec_res_list):
shared["test_results"] = exec_res_list
all_passed = all(result["passed"] for result in exec_res_list)
shared["iteration_count"] = shared.get("iteration_count", 0) + 1
# Print test results
passed_count = len([r for r in exec_res_list if r["passed"]])
total_count = len(exec_res_list)
print(f"\n=== Test Results: {passed_count}/{total_count} Passed ===")
failed_tests = [r for r in exec_res_list if not r["passed"]]
if failed_tests:
print("Failed tests:")
for i, result in enumerate(failed_tests, 1):
test_case = result['test_case']
print(f"{i}. {test_case['name']}:")
if result['error']:
print(f" error: {result['error']}")
else:
print(f" output: {result['actual']}")
print(f" expected: {result['expected']}")
if all_passed:
return "success"
elif shared["iteration_count"] >= shared.get("max_iterations", 5):
return "max_iterations"
else:
return "failure"
class Revise(Node):
def prep(self, shared):
failed_tests = [r for r in shared["test_results"] if not r["passed"]]
return {
"problem": shared["problem"],
"test_cases": shared["test_cases"],
"function_code": shared["function_code"],
"failed_tests": failed_tests
}
def exec(self, inputs):
# Format current test cases nicely
formatted_tests = ""
for i, test in enumerate(inputs['test_cases'], 1):
formatted_tests += f"{i}. {test['name']}\n"
formatted_tests += f" input: {test['input']}\n"
formatted_tests += f" expected: {test['expected']}\n\n"
# Format failed tests nicely
formatted_failures = ""
for i, result in enumerate(inputs['failed_tests'], 1):
test_case = result['test_case']
formatted_failures += f"{i}. {test_case['name']}:\n"
if result['error']:
formatted_failures += f" error: {result['error']}\n"
else:
formatted_failures += f" output: {result['actual']}\n"
formatted_failures += f" expected: {result['expected']}\n\n"
prompt = f"""Problem: {inputs['problem']}
Current test cases:
{formatted_tests}
Current function:
```python
{inputs['function_code']}
```
Failed tests:
{formatted_failures}
Analyze the failures and output revisions in YAML. You can revise test cases, function code, or both:
```yaml
reasoning: |
Looking at the failures, I see that...
The issue appears to be...
I will revise...
test_cases: # Dictionary mapping test case index (1-based) to revised test case
1:
name: "Revised test name"
input: {{...}}
expected: ...
function_code: | # Include this if revising function
def run_code(...):
return ...
```"""
response = call_llm(prompt)
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
result = yaml.safe_load(yaml_str)
# Validation asserts
if "test_cases" in result:
assert isinstance(result["test_cases"], dict), "test_cases must be a dictionary"
for index_str, test_case in result["test_cases"].items():
assert isinstance(index_str, (str, int)), "test_cases keys must be strings or ints"
assert "name" in test_case, f"Revised test case {index_str} missing 'name' field"
assert "input" in test_case, f"Revised test case {index_str} missing 'input' field"
assert "expected" in test_case, f"Revised test case {index_str} missing 'expected' field"
if "function_code" in result:
assert isinstance(result["function_code"], str), "function_code must be string"
assert "def run_code" in result["function_code"], "Function must be named 'run_code'"
return result
def post(self, shared, prep_res, exec_res):
# Print what is being revised
print(f"\n=== Revisions (Iteration {shared['iteration_count']}) ===")
# Handle test case revisions - map indices to actual test cases
if "test_cases" in exec_res:
current_tests = shared["test_cases"].copy()
print("Revising test cases:")
for index_str, revised_test in exec_res["test_cases"].items():
index = int(index_str) - 1 # Convert to 0-based
if 0 <= index < len(current_tests):
old_test = current_tests[index]
print(f" Test {index_str}: '{old_test['name']}' -> '{revised_test['name']}'")
print(f" old input: {old_test['input']}")
print(f" new input: {revised_test['input']}")
print(f" old expected: {old_test['expected']}")
print(f" new expected: {revised_test['expected']}")
current_tests[index] = revised_test
shared["test_cases"] = current_tests
if "function_code" in exec_res:
print("Revising function code:")
print("New function:")
print(exec_res["function_code"])
shared["function_code"] = exec_res["function_code"]
================================================
FILE: cookbook/pocketflow-code-generator/requirements.txt
================================================
anthropic
pocketflow
pyyaml
================================================
FILE: cookbook/pocketflow-code-generator/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-code-generator/utils/call_llm.py
================================================
from anthropic import Anthropic
import os
def call_llm(prompt):
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "your-api-key"))
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=6000,
messages=[
{"role": "user", "content": prompt}
]
)
return response.content[0].text
if __name__ == "__main__":
print("## Testing call_llm")
prompt = "In a few words, what is the meaning of life?"
print(f"## Prompt: {prompt}")
response = call_llm(prompt)
print(f"## Response: {response}")
================================================
FILE: cookbook/pocketflow-code-generator/utils/code_executor.py
================================================
import sys
import io
import traceback
from contextlib import redirect_stdout, redirect_stderr
def execute_python(function_code, input):
try:
namespace = {"__builtins__": __builtins__}
stdout_capture = io.StringIO()
stderr_capture = io.StringIO()
with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
exec(function_code, namespace)
if "run_code" not in namespace:
return None, "Function 'run_code' not found"
run_code = namespace["run_code"]
if isinstance(input, dict):
result = run_code(**input)
elif isinstance(input, (list, tuple)):
result = run_code(*input)
else:
result = run_code(input)
return result, None
except Exception as e:
return None, f"{type(e).__name__}: {str(e)}"
if __name__ == "__main__":
# Test 1: Working function
function_code = """
def run_code(nums, target):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
return []
"""
input = {"nums": [2, 7, 11, 15], "target": 9}
output, error = execute_python(function_code, input)
print(f"Output: {output}")
print(f"Error: {error}")
# Test 2: Function with error
broken_function_code = """
def run_code(nums, target):
return nums[100] # Index error
"""
output2, error2 = execute_python(broken_function_code, input)
print(f"Output: {output2}")
print(f"Error: {error2}")
================================================
FILE: cookbook/pocketflow-communication/README.md
================================================
# PocketFlow Communication Example
This example demonstrates the [Communication](https://the-pocket.github.io/PocketFlow/communication.html) concept in PocketFlow, specifically focusing on the Shared Store pattern.
## Overview
The example implements a simple word counter that shows how nodes can communicate using a shared store. It demonstrates:
- How to initialize and structure a shared store
- How nodes can read from and write to the shared store
- How to maintain state across multiple node executions
- Best practices for shared store usage
## Project Structure
```
pocketflow-communication/
├── README.md
├── requirements.txt
├── main.py
├── flow.py
└── nodes.py
```
## Installation
```bash
pip install -r requirements.txt
```
## Usage
```bash
python main.py
```
Enter text when prompted. The program will:
1. Count words in the text
2. Store statistics in the shared store
3. Display running statistics (total texts, total words, average)
Enter 'q' to quit.
## How it Works
The example uses three nodes:
1. `TextInput`: Reads user input and initializes the shared store
2. `WordCounter`: Counts words and updates statistics in the shared store
3. `ShowStats`: Displays statistics from the shared store
This demonstrates how nodes can share and maintain state using the shared store pattern.
================================================
FILE: cookbook/pocketflow-communication/flow.py
================================================
"""Flow configuration for the communication example."""
from pocketflow import Flow
from nodes import TextInput, WordCounter, ShowStats, EndNode
def create_flow():
"""Create and configure the flow with all nodes."""
# Create nodes
text_input = TextInput()
word_counter = WordCounter()
show_stats = ShowStats()
end_node = EndNode()
# Configure transitions
text_input - "count" >> word_counter
word_counter - "show" >> show_stats
show_stats - "continue" >> text_input
text_input - "exit" >> end_node
# Create and return flow
return Flow(start=text_input)
================================================
FILE: cookbook/pocketflow-communication/main.py
================================================
from flow import create_flow
def main():
"""Run the communication example."""
flow = create_flow()
shared = {}
flow.run(shared)
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-communication/nodes.py
================================================
"""Node implementations for the communication example."""
from pocketflow import Node
class EndNode(Node):
"""Node that handles flow termination."""
pass
class TextInput(Node):
"""Node that reads text input and initializes the shared store."""
def prep(self, shared):
"""Get user input and ensure shared store is initialized."""
return input("Enter text (or 'q' to quit): ")
def post(self, shared, prep_res, exec_res):
"""Store text and initialize/update statistics."""
if prep_res == 'q':
return "exit"
# Store the text
shared["text"] = prep_res
# Initialize statistics if they don't exist
if "stats" not in shared:
shared["stats"] = {
"total_texts": 0,
"total_words": 0
}
shared["stats"]["total_texts"] += 1
return "count"
class WordCounter(Node):
"""Node that counts words in the text."""
def prep(self, shared):
"""Get text from shared store."""
return shared["text"]
def exec(self, text):
"""Count words in the text."""
return len(text.split())
def post(self, shared, prep_res, exec_res):
"""Update word count statistics."""
shared["stats"]["total_words"] += exec_res
return "show"
class ShowStats(Node):
"""Node that displays statistics from the shared store."""
def prep(self, shared):
"""Get statistics from shared store."""
return shared["stats"]
def post(self, shared, prep_res, exec_res):
"""Display statistics and continue the flow."""
stats = prep_res
print(f"\nStatistics:")
print(f"- Texts processed: {stats['total_texts']}")
print(f"- Total words: {stats['total_words']}")
print(f"- Average words per text: {stats['total_words'] / stats['total_texts']:.1f}\n")
return "continue"
================================================
FILE: cookbook/pocketflow-communication/requirements.txt
================================================
pocketflow==0.1.0
================================================
FILE: cookbook/pocketflow-fastapi-background/README.md
================================================
# PocketFlow FastAPI Background Jobs with Real-time Progress
A web application demonstrating PocketFlow workflows running as FastAPI background jobs with real-time progress updates via Server-Sent Events (SSE).
## Features
- **Modern Web UI**: Clean interface with real-time progress visualization
- **Background Processing**: Non-blocking article generation using FastAPI BackgroundTasks
- **Server-Sent Events**: Real-time progress streaming without polling
- **Granular Progress**: Section-by-section updates during content generation
- **PocketFlow Integration**: Three-node workflow (Outline → Content → Style)
## How to Run
1. Install Dependencies:
```bash
pip install -r requirements.txt
```
2. Set your OpenAI API key:
```bash
export OPENAI_API_KEY=your_api_key_here
```
3. Run the FastAPI Server:
```bash
python main.py
```
4. Access the Web UI:
Open your browser and navigate to `http://localhost:8000`.
5. Use the Application:
- Enter an article topic or click suggested topics
- Click "Generate Article" to start background processing
- Watch real-time progress updates with step indicators
- Copy the final article when complete
## How It Works
The application uses PocketFlow to define a three-step article generation workflow. FastAPI handles web requests and manages real-time SSE communication for progress updates.
**PocketFlow Workflow:**
```mermaid
flowchart LR
A[Generate Outline] --> B[Write Content]
B --> C[Apply Style]
```
1. **`GenerateOutline`**: Creates structured outline with up to 3 sections
2. **`WriteContent` (BatchNode)**: Writes content for each section individually, sending progress updates
3. **`ApplyStyle`**: Polishes the article with conversational tone
**FastAPI & SSE Integration:**
- The `/start-job` endpoint creates a unique job, initializes an SSE queue, and schedules the workflow using `BackgroundTasks`
- Nodes send progress updates to the job-specific `sse_queue` during execution
- The `/progress/{job_id}` endpoint streams real-time updates to the client via Server-Sent Events
- The web UI displays progress with animated bars, step indicators, and detailed status messages
**Progress Updates:**
- 33%: Outline generation complete
- 33-66%: Content writing (individual section updates)
- 66-100%: Style application
- 100%: Article ready
## Files
- [`main.py`](./main.py): FastAPI application with background jobs and SSE endpoints
- [`flow.py`](./flow.py): PocketFlow workflow definition connecting the three nodes
- [`nodes.py`](./nodes.py): Workflow nodes (GenerateOutline, WriteContent BatchNode, ApplyStyle)
- [`utils/call_llm.py`](./utils/call_llm.py): OpenAI LLM utility function
- [`static/index.html`](./static/index.html): Modern job submission form with topic suggestions
- [`static/progress.html`](./static/progress.html): Real-time progress monitoring with animations
================================================
FILE: cookbook/pocketflow-fastapi-background/docs/design.md
================================================
# Design Doc: PocketFlow FastAPI Background Job with SSE Progress
> Please DON'T remove notes for AI
## Requirements
> Notes for AI: Keep it simple and clear.
> If the requirements are abstract, write concrete user stories
**User Story**: As a user, I want to submit an article topic via a web API and receive real-time progress updates while the article is being generated in the background, so I can see the workflow progress without blocking the UI.
**Core Requirements**:
1. Submit article topic via REST API endpoint
2. Start background job for article generation workflow
3. Receive real-time progress updates via Server-Sent Events (SSE)
4. Get final article result when workflow completes
5. Handle multiple concurrent requests
**Technical Requirements**:
- FastAPI web server with REST endpoints
- Background task processing using asyncio
- Server-Sent Events for progress streaming
- Simple web interface to test the functionality
## Flow Design
> Notes for AI:
> 1. Consider the design patterns of agent, map-reduce, rag, and workflow. Apply them if they fit.
> 2. Present a concise, high-level description of the workflow.
### Applicable Design Pattern:
**Workflow Pattern**: Sequential processing of article generation steps with progress reporting at each stage.
### Flow High-level Design:
1. **Generate Outline Node**: Creates a structured outline for the article topic
2. **Write Content Node**: Writes content for each section in the outline
3. **Apply Style Node**: Applies conversational styling to the final article
Each node puts progress updates into an asyncio.Queue for SSE streaming.
```mermaid
flowchart LR
outline[Generate Outline] --> content[Write Content]
content --> styling[Apply Style]
```
## Utility Functions
> Notes for AI:
> 1. Understand the utility function definition thoroughly by reviewing the doc.
> 2. Include only the necessary utility functions, based on nodes in the flow.
1. **Call LLM** (`utils/call_llm.py`)
- *Input*: prompt (str)
- *Output*: response (str)
- Used by all workflow nodes for LLM tasks
## Node Design
### Shared Store
> Notes for AI: Try to minimize data redundancy
The shared store structure is organized as follows:
```python
shared = {
"topic": "user-provided-topic",
"sse_queue": asyncio.Queue(), # For sending SSE updates
"sections": ["section1", "section2", "section3"],
"draft": "combined-section-content",
"final_article": "styled-final-article"
}
```
### Node Steps
> Notes for AI: Carefully decide whether to use Batch/Async Node/Flow.
1. **Generate Outline Node**
- *Purpose*: Create a structured outline with 3 main sections using YAML output
- *Type*: Regular Node (synchronous LLM call)
- *Steps*:
- *prep*: Read "topic" from shared store
- *exec*: Call LLM to generate YAML outline, parse and validate structure
- *post*: Write "sections" to shared store, put progress update in sse_queue
2. **Write Content Node**
- *Purpose*: Generate concise content for each outline section
- *Type*: BatchNode (processes each section independently)
- *Steps*:
- *prep*: Read "sections" from shared store (returns list of sections)
- *exec*: For one section, call LLM to write 100-word content
- *post*: Combine all section content into "draft", put progress update in sse_queue
3. **Apply Style Node**
- *Purpose*: Apply conversational, engaging style to the combined content
- *Type*: Regular Node (single LLM call for styling)
- *Steps*:
- *prep*: Read "draft" from shared store
- *exec*: Call LLM to rewrite in conversational style
- *post*: Write "final_article" to shared store, put completion update in sse_queue
================================================
FILE: cookbook/pocketflow-fastapi-background/flow.py
================================================
from pocketflow import Flow
from nodes import GenerateOutline, WriteContent, ApplyStyle
def create_article_flow():
"""
Create and configure the article writing workflow
"""
# Create node instances
outline_node = GenerateOutline()
content_node = WriteContent()
style_node = ApplyStyle()
# Connect nodes in sequence
outline_node >> content_node >> style_node
# Create flow starting with outline node
article_flow = Flow(start=outline_node)
return article_flow
================================================
FILE: cookbook/pocketflow-fastapi-background/main.py
================================================
import asyncio
import json
import uuid
from fastapi import FastAPI, BackgroundTasks, Form
from fastapi.responses import StreamingResponse
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from flow import create_article_flow
app = FastAPI()
# Mount static files
app.mount("/static", StaticFiles(directory="static"), name="static")
# Store active jobs and their SSE queues
active_jobs = {}
def run_article_workflow(job_id: str, topic: str):
"""Run the article workflow in background"""
try:
# Get the pre-created queue from active_jobs
sse_queue = active_jobs[job_id]
shared = {
"topic": topic,
"sse_queue": sse_queue,
"sections": [],
"draft": "",
"final_article": ""
}
# Run the workflow
flow = create_article_flow()
flow.run(shared)
except Exception as e:
# Send error message
error_msg = {"step": "error", "progress": 0, "data": {"error": str(e)}}
if job_id in active_jobs:
active_jobs[job_id].put_nowait(error_msg)
@app.post("/start-job")
async def start_job(background_tasks: BackgroundTasks, topic: str = Form(...)):
"""Start a new article generation job"""
job_id = str(uuid.uuid4())
# Create SSE queue and register job immediately
sse_queue = asyncio.Queue()
active_jobs[job_id] = sse_queue
# Start background task
background_tasks.add_task(run_article_workflow, job_id, topic)
return {"job_id": job_id, "topic": topic, "status": "started"}
@app.get("/progress/{job_id}")
async def get_progress(job_id: str):
"""Stream progress updates via SSE"""
async def event_stream():
if job_id not in active_jobs:
yield f"data: {json.dumps({'error': 'Job not found'})}\n\n"
return
sse_queue = active_jobs[job_id]
# Send initial connection confirmation
yield f"data: {json.dumps({'step': 'connected', 'progress': 0, 'data': {'message': 'Connected to job progress'}})}\n\n"
try:
while True:
# Wait for next progress update
try:
# Use asyncio.wait_for to avoid blocking forever
progress_msg = await asyncio.wait_for(sse_queue.get(), timeout=1.0)
yield f"data: {json.dumps(progress_msg)}\n\n"
# If job is complete, clean up and exit
if progress_msg.get("step") == "complete":
del active_jobs[job_id]
break
except asyncio.TimeoutError:
# Send heartbeat to keep connection alive
yield f"data: {json.dumps({'heartbeat': True})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'error': str(e)})}\n\n"
return StreamingResponse(
event_stream(),
media_type="text/plain",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Content-Type": "text/event-stream"
}
)
@app.get("/")
async def get_index():
"""Serve the main page"""
return FileResponse("static/index.html")
@app.get("/progress.html")
async def get_progress_page():
"""Serve the progress page"""
return FileResponse("static/progress.html")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
================================================
FILE: cookbook/pocketflow-fastapi-background/nodes.py
================================================
import yaml
from pocketflow import Node, BatchNode
from utils.call_llm import call_llm
class GenerateOutline(Node):
def prep(self, shared):
return shared["topic"]
def exec(self, topic):
prompt = f"""
Create a simple outline for an article about {topic}.
Include at most 3 main sections (no subsections).
Output the sections in YAML format as shown below:
```yaml
sections:
- First section title
- Second section title
- Third section title
```"""
response = call_llm(prompt)
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
structured_result = yaml.safe_load(yaml_str)
return structured_result
def post(self, shared, prep_res, exec_res):
sections = exec_res["sections"]
shared["sections"] = sections
# Send progress update via SSE queue
progress_msg = {"step": "outline", "progress": 33, "data": {"sections": sections}}
shared["sse_queue"].put_nowait(progress_msg)
return "default"
class WriteContent(BatchNode):
def prep(self, shared):
# Store sections and sse_queue for use in exec
self.sections = shared.get("sections", [])
self.sse_queue = shared["sse_queue"]
return self.sections
def exec(self, section):
prompt = f"""
Write a short paragraph (MAXIMUM 100 WORDS) about this section:
{section}
Requirements:
- Explain the idea in simple, easy-to-understand terms
- Use everyday language, avoiding jargon
- Keep it very concise (no more than 100 words)
- Include one brief example or analogy
"""
content = call_llm(prompt)
# Send progress update for this section
current_section_index = self.sections.index(section) if section in self.sections else 0
total_sections = len(self.sections)
# Progress from 33% (after outline) to 66% (before styling)
# Each section contributes (66-33)/total_sections = 33/total_sections percent
section_progress = 33 + ((current_section_index + 1) * 33 // total_sections)
progress_msg = {
"step": "content",
"progress": section_progress,
"data": {
"section": section,
"completed_sections": current_section_index + 1,
"total_sections": total_sections
}
}
self.sse_queue.put_nowait(progress_msg)
return f"## {section}\n\n{content}\n"
def post(self, shared, prep_res, exec_res_list):
draft = "\n".join(exec_res_list)
shared["draft"] = draft
return "default"
class ApplyStyle(Node):
def prep(self, shared):
return shared["draft"]
def exec(self, draft):
prompt = f"""
Rewrite the following draft in a conversational, engaging style:
{draft}
Make it:
- Conversational and warm in tone
- Include rhetorical questions that engage the reader
- Add analogies and metaphors where appropriate
- Include a strong opening and conclusion
"""
return call_llm(prompt)
def post(self, shared, prep_res, exec_res):
shared["final_article"] = exec_res
# Send completion update via SSE queue
progress_msg = {"step": "complete", "progress": 100, "data": {"final_article": exec_res}}
shared["sse_queue"].put_nowait(progress_msg)
return "default"
================================================
FILE: cookbook/pocketflow-fastapi-background/requirements.txt
================================================
fastapi
uvicorn
openai
pyyaml
python-multipart
================================================
FILE: cookbook/pocketflow-fastapi-background/static/index.html
================================================
Article Generator
✨ Article AI
Generate engaging articles with AI assistance
Popular Topics
AI SafetyClimate ChangeSpace ExplorationRenewable EnergyMental HealthFuture of Work
================================================
FILE: cookbook/pocketflow-fastapi-background/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-fastapi-background/utils/call_llm.py
================================================
import os
from openai import OpenAI
def call_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
if __name__ == "__main__":
print(call_llm("Tell me a short joke"))
================================================
FILE: cookbook/pocketflow-fastapi-hitl/README.md
================================================
# PocketFlow Web Human-in-the-Loop (HITL) Feedback Service
This project demonstrates a minimal web application for human-in-the-loop workflows using PocketFlow, FastAPI, and Server-Sent Events (SSE). Users can submit text, have it processed (simulated), review the output, and approve or reject it, potentially triggering reprocessing until approved.
## Features
- **Web UI:** Simple interface for submitting tasks and providing feedback.
- **PocketFlow Workflow:** Manages the process -> review -> result/reprocess logic.
- **FastAPI Backend:** Serves the UI and handles API requests asynchronously.
- **Server-Sent Events (SSE):** Provides real-time status updates to the client without polling.
## How to Run
1. Install Dependencies:
```bash
pip install -r requirements.txt
```
2. Run the FastAPI Server:
Use Uvicorn (or another ASGI server):
```bash
uvicorn server:app --reload --port 8000
```
*(The `--reload` flag is useful for development.)*
3. Access the Web UI:
Open your web browser and navigate to `http://127.0.0.1:8000`.
4. Use the Application:
* Enter text into the textarea and click "Submit".
* Observe the status updates pushed via SSE.
* When prompted ("waiting_for_review"), use the "Approve" or "Reject" buttons.
* If rejected, the process loops back. If approved, the final result is displayed.
## How It Works
The application uses PocketFlow to define and execute the feedback loop workflow. FastAPI handles web requests and manages the real-time SSE communication.
**PocketFlow Workflow:**
The core logic is orchestrated by an `AsyncFlow` defined in `flow.py`:
```mermaid
flowchart TD
subgraph FeedbackFlow[MinimalFeedbackFlow]
Process[ProcessNode] -- default --> Review[ReviewNode]
Review -- approved --> Result[ResultNode]
Review -- rejected --> Process
end
```
1. **`ProcessNode`**: Receives input text, calls the minimal `process_task` utility, and stores the output.
2. **`ReviewNode` (Async)**:
* Pushes a "waiting_for_review" status with the processed output to the SSE queue.
* Waits asynchronously for an external signal (triggered by the `/feedback` API endpoint).
* Based on the received feedback ("approved" or "rejected"), determines the next step in the flow. Stores the result if approved.
3. **`ResultNode`**: Logs the final approved result.
**FastAPI & SSE Integration:**
* The `/submit` endpoint creates a unique task, initializes the PocketFlow `shared` state (including an `asyncio.Event` for review and an `asyncio.Queue` for SSE), and schedules the flow execution using `BackgroundTasks`.
* Nodes within the flow (specifically `ReviewNode`'s prep logic) put status updates onto the task-specific `sse_queue`.
* The `/stream/{task_id}` endpoint uses `StreamingResponse` to read from the task's `sse_queue` and push formatted status updates to the connected client via Server-Sent Events.
* The `/feedback/{task_id}` endpoint receives the human's decision, updates the `shared` state, and sets the `asyncio.Event` to unblock the waiting `ReviewNode`.
This setup allows for a decoupled workflow logic (PocketFlow) and web interaction layer (FastAPI), with efficient real-time updates pushed to the user.
## Files
- [`server.py`](./server.py): The main FastAPI application handling HTTP requests, SSE, state management, and background task scheduling.
- [`nodes.py`](./nodes.py): Defines the PocketFlow `Node` classes (`ProcessNode`, `ReviewNode`, `ResultNode`) for the workflow steps.
- [`flow.py`](./flow.py): Defines the PocketFlow `AsyncFlow` that connects the nodes into the feedback loop.
- [`utils/process_task.py`](./utils/process_task.py): Contains the minimal simulation function for task processing.
- [`templates/index.html`](./templates/index.html): The HTML structure for the frontend user interface.
- [`static/style.css`](./static/style.css): Basic CSS for styling the frontend.
- [`requirements.txt`](./requirements.txt): Project dependencies (FastAPI, Uvicorn, Jinja2, PocketFlow).
================================================
FILE: cookbook/pocketflow-fastapi-hitl/docs/design.md
================================================
# Human-in-the-Loop Web Service
## 1. Requirements
* **Goal:** Create a web service for task submission, processing, human review (Approve/Reject loop via UI), and finalization.
* **Interface:** Simple web UI (HTML/JS) for input, status display, and feedback buttons.
* **Backend:** FastAPI using PocketFlow for workflow management.
* **Real-time Updates:** Use Server-Sent Events (SSE) to push status changes (pending, running, waiting_for_review, completed, failed) and intermediate results to the client without page reloads.
* **State:** Use in-memory storage for task state (Warning: Not suitable for production).
## 2. Flow Design
* **Core Pattern:** Workflow with a conditional loop based on human feedback. SSE for asynchronous status communication.
* **Nodes:**
1. `ProcessNode` (Regular): Takes input, executes the (simulated) task processing.
2. `ReviewNode` (Async): Waits for human feedback signaled via an `asyncio.Event`. Pushes "waiting\_for\_review" status to the SSE queue.
3. `ResultNode` (Regular): Marks the task as complete and logs the final result.
* **Shared Store (`shared` dict per task):**
* `task_input`: Initial data from user.
* `processed_output`: Result from `ProcessNode`.
* `feedback`: 'approved' or 'rejected' set by the `/feedback` endpoint.
* `review_event`: `asyncio.Event` used by `ReviewNode` to wait and `/feedback` to signal.
* `final_result`: The approved output.
* `current_attempt`: Tracks reprocessing count.
* `task_id`: Unique identifier for the task.
* **SSE Communication:** An `asyncio.Queue` (stored alongside the `shared` store in the server's global `tasks` dict, *not directly in PocketFlow's shared store*) is used per task. Nodes (or wrapper code) put status updates onto this queue. The `/stream` endpoint reads from the queue and sends SSE messages.
* **Mermaid Diagram:**
```mermaid
flowchart TD
Process[Process Task] -- "default" --> Review{Wait for Feedback}
Review -- "approved" --> Result[Final Result]
Review -- "rejected" --> Process
```
## 3. Utilities
For this specific example, the core "utility" is the processing logic itself. Let's simulate it with a simple function. The FastAPI server acts as the external interface.
* `process_task(input_data)`: A placeholder function. In a real scenario, this might call an LLM (`utils/call_llm.py`).
## 4. Node Design (Detailed)
* **`ProcessNode` (Node):**
* `prep`: Reads `task_input`, `current_attempt` from `shared`.
* `exec`: Calls `utils.process_task.process_task`.
* `post`: Writes `processed_output` to `shared`, increments `current_attempt`. Returns "default".
* **`ReviewNode` (AsyncNode):**
* `prep_async`: (As modified/wrapped by server.py) Reads `review_event`, `processed_output` from `shared`. **Puts "waiting\_for\_review" status onto the task's SSE queue.**
* `exec_async`: `await shared["review_event"].wait()`.
* `post_async`: Reads `feedback` from `shared`. Clears the event. Returns "approved" or "rejected". If approved, stores `processed_output` into `final_result`.
* **`ResultNode` (Node):**
* `prep`: Reads `final_result` from `shared`.
* `exec`: Prints/logs the final result.
* `post`: Returns `None` (ends flow).
================================================
FILE: cookbook/pocketflow-fastapi-hitl/flow.py
================================================
from pocketflow import AsyncFlow
from nodes import ProcessNode, ReviewNode, ResultNode
def create_feedback_flow():
"""Creates the minimal feedback workflow."""
process_node = ProcessNode()
review_node = ReviewNode()
result_node = ResultNode()
# Define transitions
process_node >> review_node
review_node - "approved" >> result_node
review_node - "rejected" >> process_node # Loop back
# Create the AsyncFlow
flow = AsyncFlow(start=process_node)
print("Minimal feedback flow created.")
return flow
================================================
FILE: cookbook/pocketflow-fastapi-hitl/nodes.py
================================================
from pocketflow import Node, AsyncNode
from utils.process_task import process_task
class ProcessNode(Node):
def prep(self, shared):
task_input = shared.get("task_input", "No input")
print("ProcessNode Prep")
return task_input
def exec(self, prep_res):
return process_task(prep_res)
def post(self, shared, prep_res, exec_res):
shared["processed_output"] = exec_res
print("ProcessNode Post: Output stored.")
return "default" # Go to ReviewNode
class ReviewNode(AsyncNode):
async def prep_async(self, shared):
review_event = shared.get("review_event")
queue = shared.get("sse_queue") # Expect queue in shared
processed_output = shared.get("processed_output", "N/A")
if not review_event or not queue:
print("ERROR: ReviewNode Prep - Missing review_event or sse_queue in shared store!")
return None # Signal failure
# Push status update to SSE queue
status_update = {
"status": "waiting_for_review",
"output_to_review": processed_output
}
await queue.put(status_update)
print("ReviewNode Prep: Put 'waiting_for_review' on SSE queue.")
return review_event # Return event for exec_async
async def exec_async(self, prep_res):
review_event = prep_res
if not review_event:
print("ReviewNode Exec: Skipping wait (no event from prep).")
return
print("ReviewNode Exec: Waiting on review_event...")
await review_event.wait()
print("ReviewNode Exec: review_event set.")
async def post_async(self, shared, prep_res, exec_res):
feedback = shared.get("feedback")
print(f"ReviewNode Post: Processing feedback '{feedback}'")
# Clear the event for potential loops
review_event = shared.get("review_event")
if review_event:
review_event.clear()
shared["feedback"] = None # Reset feedback
if feedback == "approved":
shared["final_result"] = shared.get("processed_output")
print("ReviewNode Post: Action=approved")
return "approved"
else:
print("ReviewNode Post: Action=rejected")
return "rejected"
class ResultNode(Node):
def prep(self, shared):
print("ResultNode Prep")
return shared.get("final_result", "No final result.")
def exec(self, prep_res):
print(f"--- FINAL RESULT ---")
print(prep_res)
print(f"--------------------")
return prep_res
def post(self, shared, prep_res, exec_res):
print("ResultNode Post: Flow finished.")
return None # End flow
================================================
FILE: cookbook/pocketflow-fastapi-hitl/requirements.txt
================================================
pocketflow>=0.0.1
fastapi
uvicorn[standard] # ASGI server for FastAPI
jinja2 # For HTML templating
================================================
FILE: cookbook/pocketflow-fastapi-hitl/server.py
================================================
import asyncio
import uuid
import json
import os
from fastapi import FastAPI, Request, HTTPException, status, BackgroundTasks # Import BackgroundTasks
from fastapi.responses import HTMLResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel, Field # Import Pydantic for request/response models
from typing import Dict, Any, Literal # For type hinting
from flow import create_feedback_flow # PocketFlow imports
# --- Configuration ---
app = FastAPI(title="Minimal Feedback Loop API")
static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'static'))
if os.path.isdir(static_dir):
app.mount("/static", StaticFiles(directory=static_dir), name="static")
else:
print(f"Warning: Static directory '{static_dir}' not found.")
template_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates'))
if os.path.isdir(template_dir):
templates = Jinja2Templates(directory=template_dir)
else:
print(f"Warning: Template directory '{template_dir}' not found.")
templates = None
# --- State Management (In-Memory - NOT FOR PRODUCTION) ---
# Global dictionary to store task state. In production, use Redis, DB, etc.
tasks: Dict[str, Dict[str, Any]] = {}
# Structure: task_id -> {"shared": dict, "status": str, "task_obj": asyncio.Task | None}
# --- Background Flow Runner ---
# This function remains mostly the same, as it defines the work to be done.
# It will be scheduled by FastAPI's BackgroundTasks now.
async def run_flow_background(task_id: str, flow, shared: Dict[str, Any]):
"""Runs the flow in background, uses queue in shared for SSE."""
# Check if task exists (might have been cancelled/deleted)
if task_id not in tasks:
print(f"Background task {task_id}: Task not found, aborting.")
return
queue = shared.get("sse_queue")
if not queue:
print(f"ERROR: Task {task_id} missing sse_queue in shared store!")
tasks[task_id]["status"] = "failed"
# Cannot report failure via SSE if queue is missing
return
tasks[task_id]["status"] = "running"
await queue.put({"status": "running"})
print(f"Task {task_id}: Background flow starting.")
final_status = "unknown"
error_message = None
try:
# Execute the potentially long-running PocketFlow
await flow.run_async(shared)
# Determine final status based on shared state after flow completion
if shared.get("final_result") is not None:
final_status = "completed"
else:
# If flow ends without setting final_result
final_status = "finished_incomplete"
print(f"Task {task_id}: Flow finished with status: {final_status}")
except Exception as e:
final_status = "failed"
error_message = str(e)
print(f"Task {task_id}: Flow execution failed: {e}")
# Consider logging traceback here in production
finally:
# Ensure task still exists before updating state
if task_id in tasks:
tasks[task_id]["status"] = final_status
final_update = {"status": final_status}
if final_status == "completed":
final_update["final_result"] = shared.get("final_result")
elif error_message:
final_update["error"] = error_message
# Put final status update onto the queue
await queue.put(final_update)
# Signal the end of the SSE stream by putting None
# Must happen regardless of whether task was deleted mid-run
if queue:
await queue.put(None)
print(f"Task {task_id}: Background task ended. Final update sentinel put on queue.")
# Remove the reference to the completed/failed asyncio Task object
if task_id in tasks:
tasks[task_id]["task_obj"] = None
# --- Pydantic Models for Request/Response Validation ---
class SubmitRequest(BaseModel):
data: str = Field(..., min_length=1, description="Input data for the task")
class SubmitResponse(BaseModel):
message: str = "Task submitted"
task_id: str
class FeedbackRequest(BaseModel):
feedback: Literal["approved", "rejected"] # Use Literal for specific choices
class FeedbackResponse(BaseModel):
message: str
# --- FastAPI Routes ---
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def get_index(request: Request):
"""Serves the main HTML frontend."""
if templates is None:
raise HTTPException(status_code=500, detail="Templates directory not configured.")
return templates.TemplateResponse("index.html", {"request": request})
@app.post("/submit", response_model=SubmitResponse, status_code=status.HTTP_202_ACCEPTED)
async def submit_task(
submit_request: SubmitRequest, # Use Pydantic model for validation
background_tasks: BackgroundTasks # Inject BackgroundTasks instance
):
"""
Submits a new task. The actual processing runs in the background.
Returns immediately with the task ID.
"""
task_id = str(uuid.uuid4())
feedback_event = asyncio.Event()
status_queue = asyncio.Queue()
shared = {
"task_input": submit_request.data,
"processed_output": None,
"feedback": None,
"review_event": feedback_event,
"sse_queue": status_queue,
"final_result": None,
"task_id": task_id
}
flow = create_feedback_flow()
# Store task state BEFORE scheduling background task
tasks[task_id] = {
"shared": shared,
"status": "pending",
"task_obj": None # Placeholder for the asyncio Task created by BackgroundTasks
}
await status_queue.put({"status": "pending", "task_id": task_id})
# Schedule the flow execution using FastAPI's BackgroundTasks
# This runs AFTER the response has been sent
background_tasks.add_task(run_flow_background, task_id, flow, shared)
# Note: We don't get a direct reference to the asyncio Task object this way,
# which is fine for this minimal example. If cancellation were needed,
# managing asyncio.create_task manually would be necessary.
print(f"Task {task_id}: Submitted, scheduled for background execution.")
return SubmitResponse(task_id=task_id)
@app.post("/feedback/{task_id}", response_model=FeedbackResponse)
async def provide_feedback(task_id: str, feedback_request: FeedbackRequest):
"""Provides feedback (approved/rejected) to potentially unblock a waiting task."""
if task_id not in tasks:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found")
task_info = tasks[task_id]
shared = task_info["shared"]
queue = shared.get("sse_queue")
review_event = shared.get("review_event")
async def report_error(message, status_code=status.HTTP_400_BAD_REQUEST):
# Helper to log, put status on queue, and raise HTTP exception
print(f"Task {task_id}: Feedback error - {message}")
if queue: await queue.put({"status": "feedback_error", "error": message})
raise HTTPException(status_code=status_code, detail=message)
if not review_event:
# This indicates an internal setup error if the task exists but has no event
await report_error("Task not configured for feedback", status.HTTP_500_INTERNAL_SERVER_ERROR)
if review_event.is_set():
# Prevent processing feedback multiple times or if the task isn't waiting
await report_error("Task not awaiting feedback or feedback already sent", status.HTTP_409_CONFLICT)
feedback = feedback_request.feedback # Already validated by Pydantic
print(f"Task {task_id}: Received feedback via POST: {feedback}")
# Update status *before* setting the event, so client sees 'processing' first
if queue: await queue.put({"status": "processing_feedback", "feedback_value": feedback})
tasks[task_id]["status"] = "processing_feedback" # Update central status tracker
# Store feedback and signal the waiting ReviewNode
shared["feedback"] = feedback
review_event.set()
return FeedbackResponse(message=f"Feedback '{feedback}' received")
# --- SSE Endpoint ---
@app.get("/stream/{task_id}")
async def stream_status(task_id: str):
"""Streams status updates for a given task using Server-Sent Events."""
if task_id not in tasks or "sse_queue" not in tasks[task_id]["shared"]:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task or queue not found")
queue = tasks[task_id]["shared"]["sse_queue"]
async def event_generator():
"""Yields SSE messages from the task's queue."""
print(f"SSE Stream: Client connected for {task_id}")
try:
while True:
# Wait for the next status update from the queue
update = await queue.get()
if update is None: # Sentinel value indicates end of stream
print(f"SSE Stream: Sentinel received for {task_id}, closing stream.")
yield f"data: {json.dumps({'status': 'stream_closed'})}\n\n"
break
sse_data = json.dumps(update)
print(f"SSE Stream: Sending for {task_id}: {sse_data}")
yield f"data: {sse_data}\n\n" # SSE format: "data: \n\n"
queue.task_done() # Acknowledge processing the queue item
except asyncio.CancelledError:
# This happens if the client disconnects
print(f"SSE Stream: Client disconnected for {task_id}.")
except Exception as e:
# Log unexpected errors during streaming
print(f"SSE Stream: Error in generator for {task_id}: {e}")
# Optionally send an error message to the client if possible
try:
yield f"data: {json.dumps({'status': 'stream_error', 'error': str(e)})}\n\n"
except Exception: # Catch errors if yield fails (e.g., connection already closed)
pass
finally:
print(f"SSE Stream: Generator finished for {task_id}.")
# Consider cleanup here (e.g., removing task if no longer needed)
# if task_id in tasks: del tasks[task_id]
# Use FastAPI/Starlette's StreamingResponse for SSE
headers = {'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}
return StreamingResponse(event_generator(), media_type="text/event-stream", headers=headers)
# --- Main Execution Guard (for running with uvicorn) ---
if __name__ == "__main__":
print("Starting FastAPI server using Uvicorn is recommended:")
print("uvicorn server:app --reload --host 0.0.0.0 --port 8000")
# Example using uvicorn programmatically (less common than CLI)
# import uvicorn
# uvicorn.run(app, host="0.0.0.0", port=8000)
================================================
FILE: cookbook/pocketflow-fastapi-hitl/static/style.css
================================================
body {
font-family: sans-serif;
margin: 0; /* Remove default body margin */
padding: 20px; /* Add some padding around the content */
background-color: #f8f9fa; /* Lighter grey background */
display: flex; /* Enable Flexbox */
flex-direction: column; /* Stack children vertically */
align-items: center; /* Center children horizontally */
min-height: 100vh; /* Ensure body takes at least full viewport height */
box-sizing: border-box; /* Include padding in height calculation */
}
h1 {
text-align: center; /* Center the main title */
color: #343a40;
margin-bottom: 25px;
}
/* Style the main containers */
.container, .status-container {
background: #ffffff;
padding: 20px 25px; /* More padding */
border: 1px solid #dee2e6; /* Softer border */
margin-bottom: 20px;
border-radius: 6px; /* Slightly rounder corners */
width: 90%; /* Responsive width */
max-width: 650px; /* Max width for readability */
box-shadow: 0 2px 5px rgba(0,0,0,0.05); /* Subtle shadow */
box-sizing: border-box; /* Include padding/border in width */
}
textarea {
width: 100%; /* Take full width of parent container */
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1em;
min-height: 60px;
box-sizing: border-box;
}
button {
padding: 9px 15px; /* Slightly adjusted padding */
margin-right: 8px;
cursor: pointer;
border: none; /* Remove default border */
border-radius: 4px;
font-weight: 500;
transition: background-color 0.2s ease;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
/* Specific button styling */
#submit-button {
background-color: #0d6efd; /* Bootstrap primary blue */
color: white;
}
#submit-button:hover:not(:disabled) {
background-color: #0b5ed7;
}
.approve {
background-color: #198754; /* Bootstrap success green */
color: white;
}
.approve:hover:not(:disabled) {
background-color: #157347;
}
.reject {
background-color: #dc3545; /* Bootstrap danger red */
color: white;
}
.reject:hover:not(:disabled) {
background-color: #bb2d3b;
}
#task-id-display {
font-size: 0.9em;
color: #6c757d; /* Bootstrap secondary text color */
margin-bottom: 8px;
word-wrap: break-word;
}
#status-display {
font-weight: bold;
margin-bottom: 15px;
padding: 10px;
background-color: #e9ecef; /* Light grey background */
border: 1px solid #dee2e6;
border-radius: 4px;
color: #495057;
}
.hidden {
display: none;
}
/* Review/Result Box Styling */
.review-box, .result-box {
border: 1px solid #dee2e6;
padding: 15px;
margin-top: 15px;
border-radius: 4px;
background-color: #f8f9fa; /* Very light background */
}
h2, h3 {
margin-top: 0; /* Remove default top margin */
margin-bottom: 15px;
color: #495057;
}
h3 {
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
pre {
background-color: #e9ecef;
padding: 12px;
border: 1px solid #ced4da;
border-radius: 4px;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 250px; /* Adjusted height */
overflow-y: auto;
font-family: monospace;
font-size: 0.95em;
color: #212529;
}
================================================
FILE: cookbook/pocketflow-fastapi-hitl/templates/index.html
================================================
Pocket Flow Web Feedback
Pocket Flow Web Feedback
Status
Task ID: N/A
Submit a task.
Review Output
Final Result
================================================
FILE: cookbook/pocketflow-fastapi-hitl/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-fastapi-hitl/utils/process_task.py
================================================
import time
def process_task(input_data):
"""Minimal simulation of processing the input data."""
print(f"Processing: '{input_data[:50]}...'")
# Simulate work
time.sleep(2)
processed_result = f"Processed: {input_data}"
print(f"Finished processing.")
return processed_result
# We don't need a separate utils/call_llm.py for this minimal example,
# but you would add it here if ProcessNode used an LLM.
================================================
FILE: cookbook/pocketflow-fastapi-websocket/README.md
================================================
# PocketFlow FastAPI WebSocket Chat
Real-time chat interface with streaming LLM responses using PocketFlow, FastAPI, and WebSocket.
## Features
- **Real-time Streaming**: See AI responses typed out in real-time as the LLM generates them
- **Conversation Memory**: Maintains chat history across messages
- **Modern UI**: Clean, responsive chat interface with gradient design
- **WebSocket Connection**: Persistent connection for instant communication
- **PocketFlow Integration**: Uses PocketFlow `AsyncNode` and `AsyncFlow` for streaming
## How to Run
1. **Set OpenAI API Key:**
```bash
export OPENAI_API_KEY="your-openai-api-key"
```
2. **Install Dependencies:**
```bash
pip install -r requirements.txt
```
3. **Run the Application:**
```bash
python main.py
```
4. **Access the Web UI:**
Open `http://localhost:8000` in your browser.
## Usage
1. **Type Message**: Enter your message in the input field
2. **Send**: Press Enter or click Send button
3. **Watch Streaming**: See the AI response appear in real-time
4. **Continue Chat**: Conversation history is maintained automatically
## Files
- [`main.py`](./main.py): FastAPI application with WebSocket endpoint
- [`nodes.py`](./nodes.py): PocketFlow `StreamingChatNode` definition
- [`flow.py`](./flow.py): PocketFlow `AsyncFlow` for chat processing
- [`utils/stream_llm.py`](./utils/stream_llm.py): OpenAI streaming utility
- [`static/index.html`](./static/index.html): Modern chat interface
- [`requirements.txt`](./requirements.txt): Project dependencies
- [`docs/design.md`](./docs/design.md): System design documentation
- [`README.md`](./README.md): This file
================================================
FILE: cookbook/pocketflow-fastapi-websocket/docs/design.md
================================================
# Design Doc: FastAPI WebSocket Chat Interface
> Please DON'T remove notes for AI
## Requirements
> Notes for AI: Keep it simple and clear.
> If the requirements are abstract, write concrete user stories
**User Story**: As a user, I want to interact with an AI chatbot through a web interface where:
1. I can send messages and receive real-time streaming responses
2. The connection stays persistent (WebSocket)
3. I can see the AI response being typed out in real-time as the LLM generates it
4. The interface is minimal and easy to use
**Technical Requirements**:
- FastAPI backend with WebSocket support
- Real-time bidirectional communication
- True LLM streaming integration using PocketFlow AsyncNode
- Simple HTML/JavaScript frontend
- Minimal dependencies
## Flow Design
> Notes for AI:
> 1. Consider the design patterns of agent, map-reduce, rag, and workflow. Apply them if they fit.
> 2. Present a concise, high-level description of the workflow.
### Applicable Design Pattern:
**Single Async Node Pattern**: One PocketFlow AsyncNode handles the entire LLM streaming process with real-time WebSocket streaming
### Flow high-level Design:
**PocketFlow AsyncFlow**: Just one async node
1. **Streaming Chat Node**: Processes message, calls LLM with real streaming, sends chunks immediately to WebSocket
**Integration**: FastAPI WebSocket endpoint calls the PocketFlow AsyncFlow
```mermaid
flowchart TD
user((User Browser)) --> websocket(FastAPI WebSocket)
websocket --> flow[Streaming Chat AsyncNode]
flow --> websocket
websocket --> user
style user fill:#e1f5fe
style websocket fill:#f3e5f5
style flow fill:#e8f5e8,stroke:#4caf50,stroke-width:3px
```
## Utility Functions
> Notes for AI:
> 1. Understand the utility function definition thoroughly by reviewing the doc.
> 2. Include only the necessary utility functions, based on nodes in the flow.
1. **Stream LLM** (`utils/stream_llm.py`)
- *Input*: messages (list of chat history)
- *Output*: generator yielding real-time response chunks from OpenAI API
- Used by streaming chat node to get LLM chunks as they're generated
## Node Design
### Shared Store
> Notes for AI: Try to minimize data redundancy
The shared store structure is organized as follows:
```python
shared = {
"websocket": None, # WebSocket connection object
"user_message": "", # Current user message
"conversation_history": [] # List of message history with roles
}
```
### Node Steps
> Notes for AI: Carefully decide whether to use Batch/Async Node/Flow.
1. **Streaming Chat Node**
- *Purpose*: Process user message, call LLM with real streaming, and send chunks immediately via WebSocket
- *Type*: AsyncNode (for real-time streaming)
- *Steps*:
- *prep*: Read user message, build conversation history with new message
- *exec_async*: Call streaming LLM utility, stream each chunk immediately to WebSocket as received
- *post*: Update conversation history with complete assistant response
================================================
FILE: cookbook/pocketflow-fastapi-websocket/flow.py
================================================
from pocketflow import AsyncFlow
from nodes import StreamingChatNode
def create_streaming_chat_flow():
chat_node = StreamingChatNode()
return AsyncFlow(start=chat_node)
================================================
FILE: cookbook/pocketflow-fastapi-websocket/main.py
================================================
import json
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from flow import create_streaming_chat_flow
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/")
async def get_chat_interface():
return FileResponse("static/index.html")
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
# Initialize conversation history for this connection
shared_store = {
"websocket": websocket,
"conversation_history": []
}
try:
while True:
data = await websocket.receive_text()
message = json.loads(data)
# Update only the current message, keep conversation history
shared_store["user_message"] = message.get("content", "")
flow = create_streaming_chat_flow()
await flow.run_async(shared_store)
except WebSocketDisconnect:
pass
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
================================================
FILE: cookbook/pocketflow-fastapi-websocket/nodes.py
================================================
import asyncio
import json
from pocketflow import AsyncNode
from utils.stream_llm import stream_llm
class StreamingChatNode(AsyncNode):
async def prep_async(self, shared):
user_message = shared.get("user_message", "")
websocket = shared.get("websocket")
conversation_history = shared.get("conversation_history", [])
conversation_history.append({"role": "user", "content": user_message})
return conversation_history, websocket
async def exec_async(self, prep_res):
messages, websocket = prep_res
await websocket.send_text(json.dumps({"type": "start", "content": ""}))
full_response = ""
async for chunk_content in stream_llm(messages):
full_response += chunk_content
await websocket.send_text(json.dumps({
"type": "chunk",
"content": chunk_content
}))
await websocket.send_text(json.dumps({"type": "end", "content": ""}))
return full_response, websocket
async def post_async(self, shared, prep_res, exec_res):
full_response, websocket = exec_res
conversation_history = shared.get("conversation_history", [])
conversation_history.append({"role": "assistant", "content": full_response})
shared["conversation_history"] = conversation_history
================================================
FILE: cookbook/pocketflow-fastapi-websocket/requirements.txt
================================================
fastapi==0.104.1
uvicorn[standard]==0.24.0
openai==1.3.8
pocketflow
================================================
FILE: cookbook/pocketflow-fastapi-websocket/static/index.html
================================================
PocketFlow Chat
PocketFlow Chat
Connecting...
================================================
FILE: cookbook/pocketflow-fastapi-websocket/utils/__init__.py
================================================
# Utils package for FastAPI WebSocket Chat Interface
================================================
FILE: cookbook/pocketflow-fastapi-websocket/utils/stream_llm.py
================================================
import os
from openai import AsyncOpenAI
async def stream_llm(messages):
client = AsyncOpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
stream = await client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
stream=True,
temperature=0.7
)
async for chunk in stream:
if chunk.choices[0].delta.content is not None:
yield chunk.choices[0].delta.content
if __name__ == "__main__":
import asyncio
async def test():
messages = [{"role": "user", "content": "Hello!"}]
async for chunk in stream_llm(messages):
print(chunk, end="", flush=True)
print()
asyncio.run(test())
================================================
FILE: cookbook/pocketflow-flow/README.md
================================================
# Text Converter Flow
This project demonstrates an interactive text transformation tool built with PocketFlow.
## Features
- Convert text to UPPERCASE
- Convert text to lowercase
- Reverse text
- Remove extra spaces
- Interactive command-line interface
- Continuous flow with option to process multiple texts
## Getting Started
1. Install the required dependencies:
```bash
pip install -r requirements.txt
```
2. Run the application:
```bash
python main.py
```
## How It Works
The workflow features an interactive loop with branching paths:
```mermaid
graph TD
Input[TextInput Node] -->|transform| Transform[TextTransform Node]
Transform -->|input| Input
Transform -->|exit| End[End]
Input -->|exit| End
```
Here's what each part does:
1. **TextInput Node**: Collects text input and handles menu choices
2. **TextTransform Node**: Applies the selected transformation to the text
## Example Output
```
Welcome to Text Converter!
=========================
Enter text to convert: Pocket Flow is a 100-line LLM framework
Choose transformation:
1. Convert to UPPERCASE
2. Convert to lowercase
3. Reverse text
4. Remove extra spaces
5. Exit
Your choice (1-5): 1
Result: POCKET FLOW IS A 100-LINE LLM FRAMEWORK
Convert another text? (y/n): n
Thank you for using Text Converter!
```
## Files
- [`main.py`](./main.py): Main entry point for running the text converter
- [`flow.py`](./flow.py): Defines the nodes and flow for text transformation
- [`requirements.txt`](./requirements.txt): Lists the required dependencies
================================================
FILE: cookbook/pocketflow-flow/flow.py
================================================
from pocketflow import Node, Flow
class TextInput(Node):
def prep(self, shared):
"""Get text input from user."""
if "text" not in shared:
text = input("\nEnter text to convert: ")
shared["text"] = text
return shared["text"]
def post(self, shared, prep_res, exec_res):
print("\nChoose transformation:")
print("1. Convert to UPPERCASE")
print("2. Convert to lowercase")
print("3. Reverse text")
print("4. Remove extra spaces")
print("5. Exit")
choice = input("\nYour choice (1-5): ")
if choice == "5":
return "exit"
shared["choice"] = choice
return "transform"
class TextTransform(Node):
def prep(self, shared):
return shared["text"], shared["choice"]
def exec(self, inputs):
text, choice = inputs
if choice == "1":
return text.upper()
elif choice == "2":
return text.lower()
elif choice == "3":
return text[::-1]
elif choice == "4":
return " ".join(text.split())
else:
return "Invalid option!"
def post(self, shared, prep_res, exec_res):
print("\nResult:", exec_res)
if input("\nConvert another text? (y/n): ").lower() == 'y':
shared.pop("text", None) # Remove previous text
return "input"
return "exit"
class EndNode(Node):
pass
# Create nodes
text_input = TextInput()
text_transform = TextTransform()
end_node = EndNode()
# Connect nodes
text_input - "transform" >> text_transform
text_transform - "input" >> text_input
text_transform - "exit" >> end_node
# Create flow
flow = Flow(start=text_input)
================================================
FILE: cookbook/pocketflow-flow/main.py
================================================
from flow import flow
def main():
print("\nWelcome to Text Converter!")
print("=========================")
# Initialize shared store
shared = {}
# Run the flow
flow.run(shared)
print("\nThank you for using Text Converter!")
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-flow/requirements.txt
================================================
pocketflow>=0.1.0
================================================
FILE: cookbook/pocketflow-google-calendar/.gitignore
================================================
.env
Pipfile.lock
credentials.json
token.pickle
================================================
FILE: cookbook/pocketflow-google-calendar/Pipfile
================================================
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
python-dotenv = ">=0.19.0"
pocketflow = ">=0.0.2"
google-auth-oauthlib = ">=1.0.0"
google-auth-httplib2 = ">=0.1.0"
google-api-python-client = ">=2.0.0"
[dev-packages]
[requires]
python_version = "3.13"
================================================
FILE: cookbook/pocketflow-google-calendar/README.md
================================================
# Pocket Google Calendar
An application based on the Pocket Flow framework for Google Calendar integration.
## 📋 Description
This project implements a Google Calendar integration using the Pocket Flow framework, allowing efficient management of events and appointments through a simple and intuitive interface.
## 🚀 Features
- Google Calendar API Integration
- Event Management
- Appointment Viewing
- Flow-based Interface using Pocket Flow
## 🛠️ Technologies Used
- Python
- Pocket Flow Framework
- Google Calendar API
- Pipenv for dependency management
## 📦 Installation
1. Clone the repository:
```bash
git clone [REPOSITORY_URL]
cd pocket-google-calendar
```
2. Install dependencies using Pipenv:
```bash
pipenv install
```
## 🔑 Credentials Setup
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select an existing one
3. Enable the Google Calendar API for your project
4. Create credentials:
- Go to "APIs & Services" > "Credentials"
- Click "Create Credentials" > "OAuth client ID"
- Choose "Desktop application" as the application type
- Download the credentials file
- Rename it to `credentials.json`
- Place it in the root directory of the project
## 🌍 Environment Variables
Create a `.env` file in the root directory with the following variables:
```env
# Google Calendar API Configuration
GOOGLE_CALENDAR_ID=your_calendar_id@group.calendar.google.com
GOOGLE_APPLICATION_CREDENTIALS=credentials.json
# Application Configuration
TIMEZONE=America/Sao_Paulo # or your preferred timezone
```
## 🔧 Configuration
1. Activate the virtual environment:
```bash
pipenv shell
```
2. Run the application:
```bash
python main.py
```
## Expected Output
When running the example, you'll see an output similar to this:
```
=== Listing your calendars ===
- Primary Calendar
- Work
- Personal
=== Creating an example event ===
Event created successfully!
Event ID: abc123xyz
```
## 📁 Project Structure
```
pocket-google-calendar/
├── main.py # Application entry point
├── nodes.py # Pocket Flow node definitions
├── utils/ # Utilities and helper functions
├── Pipfile # Pipenv configuration
├── credentials.json # Google Calendar API credentials
├── .env # Environment variables
└── token.pickle # Google Calendar authentication token
```
## 🤝 Contributing
1. Fork the project
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## 📝 License
This project is under the MIT License. See the [LICENSE](LICENSE) file for more details.
## ✨ Acknowledgments
- [Pocket Flow](https://github.com/the-pocket/PocketFlow) - Framework used
- [Google Calendar API](https://developers.google.com/calendar) - Integration API
================================================
FILE: cookbook/pocketflow-google-calendar/main.py
================================================
from pocketflow import Flow
from nodes import CreateCalendarEventNode, ListCalendarEventsNode, ListCalendarsNode
from datetime import datetime, timedelta
def create_calendar_flow():
"""Creates a flow to manage calendar events."""
# Create nodes
create_event_node = CreateCalendarEventNode()
list_events_node = ListCalendarEventsNode()
# Connect nodes
create_event_node - "success" >> list_events_node
create_event_node - "error" >> None
# Create flow
return Flow(start=create_event_node)
def list_calendars_flow():
"""Creates a flow to list all user calendars."""
list_calendars_node = ListCalendarsNode()
return Flow(start=list_calendars_node)
def main():
# Example: List all calendars
print("=== Listing your calendars ===")
flow = list_calendars_flow()
shared = {}
flow.run(shared)
if 'available_calendars' in shared:
for cal in shared['available_calendars']:
print(f"- {cal.get('summary')}")
# Example: Create a simple event
print("\n=== Creating an example event ===")
flow = create_calendar_flow()
shared = {
'event_summary': 'Example Meeting',
'event_description': 'An example meeting created by PocketFlow',
'event_start_time': datetime.now() + timedelta(days=1),
'event_end_time': datetime.now() + timedelta(days=1, hours=1),
'days_to_list': 7
}
flow.run(shared)
if 'last_created_event' in shared:
print("Event created successfully!")
print(f"Event ID: {shared['last_created_event']['id']}")
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-google-calendar/nodes.py
================================================
from pocketflow import Node
from utils.google_calendar import create_event, list_events, list_calendar_lists
from datetime import datetime, timedelta
class CreateCalendarEventNode(Node):
def prep(self, shared):
"""Prepares the necessary data to create an event."""
return {
'summary': shared.get('event_summary'),
'description': shared.get('event_description'),
'start_time': shared.get('event_start_time'),
'end_time': shared.get('event_end_time')
}
def exec(self, event_data):
"""Creates a new calendar event."""
try:
event = create_event(
summary=event_data['summary'],
description=event_data['description'],
start_time=event_data['start_time'],
end_time=event_data['end_time']
)
return {'success': True, 'event': event}
except Exception as e:
return {'success': False, 'error': str(e)}
def post(self, shared, prep_res, exec_res):
"""Stores the event creation result."""
if exec_res['success']:
shared['last_created_event'] = exec_res['event']
return 'success'
else:
shared['error'] = exec_res['error']
return 'error'
class ListCalendarEventsNode(Node):
def prep(self, shared):
"""Prepares parameters to list events."""
return {
'days': shared.get('days_to_list', 7)
}
def exec(self, params):
"""Lists calendar events."""
try:
events = list_events(days=params['days'])
return {'success': True, 'events': events}
except Exception as e:
return {'success': False, 'error': str(e)}
def post(self, shared, prep_res, exec_res):
"""Stores the list of events."""
if exec_res['success']:
shared['calendar_events'] = exec_res['events']
return 'success'
else:
shared['error'] = exec_res['error']
return 'error'
class ListCalendarsNode(Node):
def prep(self, shared):
"""No special preparation needed to list calendars."""
return {}
def exec(self, params):
"""Lists all available calendars for the user."""
try:
calendars = list_calendar_lists()
return {'success': True, 'calendars': calendars}
except Exception as e:
return {'success': False, 'error': str(e)}
def post(self, shared, prep_res, exec_res):
"""Stores the list of calendars in the shared store."""
if exec_res['success']:
shared['available_calendars'] = exec_res['calendars']
return 'success'
else:
shared['error'] = exec_res['error']
return 'error'
================================================
FILE: cookbook/pocketflow-google-calendar/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-google-calendar/utils/google_calendar.py
================================================
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
import os.path
import os
import pickle
from datetime import datetime, timedelta
from dotenv import load_dotenv
load_dotenv()
CALENDAR_ID = os.getenv('GOOGLE_CALENDAR_ID')
GOOGLE_APPLICATION_CREDENTIALS = os.getenv('GOOGLE_APPLICATION_CREDENTIALS')
TIMEZONE = os.getenv('TIMEZONE')
SCOPES = ['https://www.googleapis.com/auth/calendar']
def get_calendar_service():
"""Gets the authenticated Google Calendar service."""
creds = None
if os.path.exists('token.pickle'):
with open('token.pickle', 'rb') as token:
creds = pickle.load(token)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
GOOGLE_APPLICATION_CREDENTIALS, SCOPES)
creds = flow.run_local_server(port=0)
with open('token.pickle', 'wb') as token:
pickle.dump(creds, token)
return build('calendar', 'v3', credentials=creds)
def create_event(summary, description, start_time, end_time, timezone=TIMEZONE):
"""Creates a new event in Google Calendar."""
service = get_calendar_service()
event = {
'summary': summary,
'description': description,
'start': {
'dateTime': start_time.isoformat(),
'timeZone': timezone,
},
'end': {
'dateTime': end_time.isoformat(),
'timeZone': timezone,
},
}
event = service.events().insert(calendarId=CALENDAR_ID, body=event).execute()
return event
def list_events(days=7):
"""Lists events for the next X days."""
service = get_calendar_service()
now = datetime.utcnow()
time_min = now.isoformat() + 'Z'
time_max = (now + timedelta(days=days)).isoformat() + 'Z'
events_result = service.events().list(
calendarId=CALENDAR_ID,
timeMin=time_min,
timeMax=time_max,
singleEvents=True,
orderBy='startTime'
).execute()
return events_result.get('items', [])
def create_custom_calendar(calendar_name, description=""):
"""Creates a new custom calendar in Google Calendar."""
service = get_calendar_service()
calendar = {
'summary': calendar_name,
'description': description,
'timeZone': TIMEZONE
}
created_calendar = service.calendars().insert(body=calendar).execute()
return created_calendar
def list_calendar_lists():
"""Lists all available calendars for the user."""
service = get_calendar_service()
calendar_list = service.calendarList().list().execute()
return calendar_list.get('items', [])
================================================
FILE: cookbook/pocketflow-gradio-hitl/README.md
================================================
# PocketFlow Gradio HITL Example
A web-based application that demonstrates Human-in-the-Loop (HITL) workflow orchestration using PocketFlow and Gradio. This example provides an interactive interface for users to engage with AI-powered tasks while maintaining human oversight and feedback.
## Features
- **Web-based Interface**: Built with Gradio for an accessible and user-friendly experience
- **Human-in-the-Loop Integration**: Seamless integration of human feedback into the AI workflow
- **Modern UI**: Clean and intuitive interface for better user interaction
- **Powered by LLMs**: Utilizes OpenAI's models for intelligent task processing
- **Flow Visualization**: Real-time visualization of node execution sequence and workflow progress
- **Interactive Debugging**: Monitor and understand the decision-making process through visual feedback
## Getting Started
This project is part of the PocketFlow cookbook examples. It's assumed you have already cloned the [PocketFlow repository](https://github.com/the-pocket/PocketFlow) and are in the `cookbook/pocketflow-gradio-hitl` directory.
1. **Install required dependencies**:
```bash
pip install -r requirements.txt
```
2. **Set up your OpenAI API key**:
The application uses OpenAI models for processing. You need to set your API key as an environment variable:
```bash
export OPENAI_API_KEY="your-openai-api-key-here"
```
3. **Run the Application**:
```bash
python main.py
```
This will start the Gradio web interface, typically accessible at `http://localhost:7860`
## How It Works
The system implements a PocketFlow workflow with a web interface:
```mermaid
flowchart TD
DecideAction[Decide Action Node] --> |"check-weather"| CheckWeather[Check Weather Node]
CheckWeather --> DecideAction
DecideAction --> |"book-hotel"| BookHotel[Book Hotel Node]
BookHotel --> DecideAction
DecideAction --> |"follow-up"| FollowUp[Follow Up Node]
DecideAction --> |"result-notification"| ResultNotification[Result Notification Node]
```
The workflow consists of the following nodes:
1. **Decide Action Node**: The central decision-making node that determines the next action based on user input and context
2. **Check Weather Node**: Provides weather information for specified cities and dates
3. **Book Hotel Node**: Handles hotel reservation requests with check-in and check-out dates
4. **Follow Up Node**: Manages user interactions by asking clarifying questions or handling out-of-scope requests
5. **Result Notification Node**: Delivers action results and offers additional assistance
The flow is orchestrated through a series of directed connections:
- The Decide Action node can trigger weather checks, hotel bookings, follow-ups, or result notifications
- Weather checks and hotel bookings can feed back to the Decide Action node for further processing
- Follow-up and result notification nodes provide the final steps in the workflow
### Flow Visualization
The application provides real-time visualization of the workflow execution:
- The sequence of node activations is displayed chronologically
- Users can see which decision paths are being taken
- The visualization helps in understanding the AI's decision-making process

## Sample Output
Here's an example of book hotel:

Here's an example of changing intention mid-conversation:

## Files
- [`main.py`](./main.py): Entry point for the application and Gradio interface setup
- [`flow.py`](./flow.py): Defines the PocketFlow graph and node connections
- [`nodes.py`](./nodes.py): Contains the node definitions for the workflow
- [`utils/`](./utils/): Contains utility functions and helper modules
- [`requirements.txt`](./requirements.txt): Lists project dependencies
## Requirements
- Python 3.8+
- PocketFlow >= 0.0.2
- Gradio >= 5.29.1
- OpenAI >= 1.78.1
================================================
FILE: cookbook/pocketflow-gradio-hitl/flow.py
================================================
from pocketflow import Flow
from nodes import (
DecideAction,
CheckWeather,
BookHotel,
FollowUp,
ResultNotification,
)
def create_flow():
"""
Create and connect the nodes to form a complete agent flow.
"""
decide_action = DecideAction()
check_weather = CheckWeather()
book_hotel = BookHotel()
follow_up = FollowUp()
result_notification = ResultNotification()
decide_action - "check-weather" >> check_weather
check_weather >> decide_action
decide_action - "book-hotel" >> book_hotel
book_hotel >> decide_action
decide_action - "follow-up" >> follow_up
decide_action - "result-notification" >> result_notification
return Flow(start=decide_action)
================================================
FILE: cookbook/pocketflow-gradio-hitl/main.py
================================================
import time
import uuid
from concurrent.futures import ThreadPoolExecutor
from queue import Queue
import gradio as gr
from gradio import ChatMessage
from flow import create_flow
# create global thread pool
chatflow_thread_pool = ThreadPoolExecutor(
max_workers=5,
thread_name_prefix="chatflow_worker",
)
def chat_fn(message, history, uuid):
"""
Main chat function that handles the conversation flow and message processing.
Args:
message (str): The current user message
history (list): Previous conversation history
uuid (UUID): Unique identifier for the conversation
Yields:
ChatMessage: Streams of thought process and chat responses
"""
# Log conversation details
print(f"Conversation ID: {str(uuid)}\nHistory: {history}\nQuery: {message}\n---")
# Initialize queues for chat messages and flow thoughts
chat_queue = Queue()
flow_queue = Queue()
# Create shared context for the flow
shared = {
"conversation_id": str(uuid),
"query": message,
"history": history,
"queue": chat_queue,
"flow_queue": flow_queue,
}
# Create and run the chat flow in a separate thread
chat_flow = create_flow()
chatflow_thread_pool.submit(chat_flow.run, shared)
# Initialize thought response tracking
start_time = time.time()
thought_response = ChatMessage(
content="", metadata={"title": "Flow Log", "id": 0, "status": "pending"}
)
yield thought_response
# Process and accumulate thoughts from the flow queue
accumulated_thoughts = ""
while True:
thought = flow_queue.get()
if thought is None:
break
accumulated_thoughts += f"- {thought}\n\n"
thought_response.content = accumulated_thoughts.strip()
yield thought_response
flow_queue.task_done()
# Mark thought processing as complete and record duration
thought_response.metadata["status"] = "done"
thought_response.metadata["duration"] = time.time() - start_time
yield thought_response
# Process and yield chat messages from the chat queue
while True:
msg = chat_queue.get()
if msg is None:
break
chat_response = [thought_response, ChatMessage(content=msg)]
yield chat_response
chat_queue.task_done()
def clear_fn():
print("Clearing conversation")
return uuid.uuid4()
with gr.Blocks(fill_height=True, theme="ocean") as demo:
uuid_state = gr.State(uuid.uuid4())
demo.load(clear_fn, outputs=[uuid_state])
chatbot = gr.Chatbot(type="messages", scale=1)
chatbot.clear(clear_fn, outputs=[uuid_state])
gr.ChatInterface(
fn=chat_fn,
type="messages",
additional_inputs=[uuid_state],
chatbot=chatbot,
title="PocketFlow Gradio Demo",
)
demo.launch()
================================================
FILE: cookbook/pocketflow-gradio-hitl/nodes.py
================================================
from datetime import datetime
from queue import Queue
import yaml
from pocketflow import Node
from utils.call_llm import call_llm
from utils.call_mock_api import call_book_hotel_api, call_check_weather_api
from utils.conversation import load_conversation, save_conversation
from utils.format_chat_history import format_chat_history
class DecideAction(Node):
def prep(self, shared):
conversation_id = shared["conversation_id"]
session = load_conversation(conversation_id)
return session, shared["history"], shared["query"]
def exec(self, prep_res):
session, history, query = prep_res
prompt = f"""
### INSTRUCTIONS
You are a lifestyle assistant capable of helping users book hotels and check weather conditions.
You need to decide the next action based on your last action, action execution result, chat history, and current user question.
### CHAT HISTORY
{format_chat_history(history)}
### CURRENT USER QUESTION
user: {query}
### CONTEXT
Last Action: {session.get("last_action", None)}
Last Action Result: {session.get("action_result", None)}
Current Date: {datetime.now().date()}
### ACTION SPACE
[1] check-weather
Description: When the user asks about the weather, use this tool.
Parameters:
- name: city
description: The city to check the weather
required: true
example: Beijing
- name: date
description: The date to check the weather, if not provided, use the current date
required: false
example: 2025-05-28
[2] book-hotel
Description: When the user wants to book a hotel, use this tool.
Parameters:
- name: hotel
description: The name of the hotel to be booked
required: true
example: ShanghaiHilton Hotel
- name: checkin_date
description: The check-in date
required: true
example: 2025-05-28
- name: checkout_date
description: The check-out date
required: true
example: 2025-05-29
[3] follow-up
Description: 1. When the user's question is out of the scope of booking hotels and checking weather, use this tool to guide the user; 2. When the current information cannot meet the parameter requirements of the corresponding tool, use this tool to ask the user.
Parameters:
- name: question
description: Your guidance or follow-up to the user, maintain an enthusiastic and lively language style, and use the same language as the user's question.
required: true
example: Which hotel would you like to book?😊
[4] result-notification
Description: When the booking of a hotel or checking the weather is completed, use this tool to notify the user of the result and ask if they need any other help. If you find that the user's question is not completed in the history conversation, you can guide the user to complete the intention in the last step.
Parameters:
- name: result
description: Notify the user of the result based on the Last Action Result. Maintain an enthusiastic and lively language style, and use the same language as the user's question.
required: true
example: The hotel has been successfully booked for you. 😉\n\nThe check-in date is XX, and the check-out date is XX. Thank you for using it. Would you like any other help?😀
## NEXT ACTION
Decide the next action based on the context and available actions.
Return your response in this format:
```yaml
thinking: |
action: check-weather OR book-hotel OR follow-up OR result-notification
reason:
question:
city:
hotel:
checkin_date:
checkout_date:
result:
```
IMPORTANT: Make sure to:
1. Use proper indentation (4 spaces) for all multi-line fields
2. Use the | character for multi-line text fields
3. Keep single-line fields without the | character
"""
response = call_llm(prompt.strip())
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
print(f"🤖 Agent response: \n{yaml_str}")
decision = yaml.safe_load(yaml_str)
return decision
def post(self, shared, prep_res, exec_res):
conversation_id = shared["conversation_id"]
session: dict = load_conversation(conversation_id)
"""Save the decision and determine the next step in the flow."""
# If LLM decided to search, save the search query
session["last_action"] = exec_res["action"]
flow_log: Queue = shared["flow_queue"]
for line in exec_res["thinking"].split("\n"):
line = line.replace("-", "").strip()
if line:
flow_log.put(f"🤔 {line}")
if exec_res["action"] == "check-weather":
session["check_weather_params"] = {
"city": exec_res["city"],
"date": exec_res.get("date", None),
}
flow_log.put(f"➡️ Agent decided to check weather for: {exec_res['city']}")
elif exec_res["action"] == "book-hotel":
session["book_hotel_params"] = {
"hotel": exec_res["hotel"],
"checkin_date": exec_res["checkin_date"],
"checkout_date": exec_res["checkout_date"],
}
flow_log.put(f"➡️ Agent decided to book hotel: {exec_res['hotel']}")
elif exec_res["action"] == "follow-up":
session["follow_up_params"] = {"question": exec_res["question"]}
flow_log.put(f"➡️ Agent decided to follow up: {exec_res['question']}")
elif exec_res["action"] == "result-notification":
session["result_notification_params"] = {"result": exec_res["result"]}
flow_log.put(f"➡️ Agent decided to notify the result: {exec_res['result']}")
save_conversation(conversation_id, session)
# Return the action to determine the next node in the flow
return exec_res["action"]
class CheckWeather(Node):
def prep(self, shared):
conversation_id = shared["conversation_id"]
session: dict = load_conversation(conversation_id)
city = session["check_weather_params"]["city"]
date = session["check_weather_params"].get("date", None)
return city, date
def exec(self, prep_res):
city, date = prep_res
return call_check_weather_api(city, date)
def post(self, shared, prep_res, exec_res):
flow_log: Queue = shared["flow_queue"]
flow_log.put(f"⬅️ Check weather result: {exec_res}")
conversation_id = shared["conversation_id"]
session: dict = load_conversation(conversation_id)
session["action_result"] = exec_res
save_conversation(conversation_id, session)
return "default"
class BookHotel(Node):
def prep(self, shared):
conversation_id = shared["conversation_id"]
session: dict = load_conversation(conversation_id)
hotel = session["book_hotel_params"]["hotel"]
checkin_date = session["book_hotel_params"]["checkin_date"]
checkout_date = session["book_hotel_params"]["checkout_date"]
return hotel, checkin_date, checkout_date
def exec(self, prep_res):
hotel, checkin_date, checkout_date = prep_res
return call_book_hotel_api(hotel, checkin_date, checkout_date)
def post(self, shared, prep_res, exec_res):
flow_log: Queue = shared["flow_queue"]
flow_log.put(f"⬅️ Book hotel result: {exec_res}")
conversation_id = shared["conversation_id"]
session: dict = load_conversation(conversation_id)
session["action_result"] = exec_res
save_conversation(conversation_id, session)
return "default"
class FollowUp(Node):
def prep(self, shared):
flow_log: Queue = shared["flow_queue"]
flow_log.put(None)
conversation_id = shared["conversation_id"]
session: dict = load_conversation(conversation_id)
question = session["follow_up_params"]["question"]
return question, shared["queue"]
def exec(self, prep_res):
question, queue = prep_res
queue.put(question)
queue.put(None)
return question
def post(self, shared, prep_res, exec_res):
conversation_id = shared["conversation_id"]
session: dict = load_conversation(conversation_id)
session["action_result"] = exec_res
return "done"
class ResultNotification(Node):
def prep(self, shared):
flow_log: Queue = shared["flow_queue"]
flow_log.put(None)
conversation_id = shared["conversation_id"]
session: dict = load_conversation(conversation_id)
result = session["result_notification_params"]["result"]
return result, shared["queue"]
def exec(self, prep_res):
result, queue = prep_res
queue.put(result)
queue.put(None)
return result
def post(self, shared, prep_res, exec_res):
conversation_id = shared["conversation_id"]
session: dict = load_conversation(conversation_id)
session["action_result"] = None
session["last_action"] = None
save_conversation(conversation_id, session)
return "done"
================================================
FILE: cookbook/pocketflow-gradio-hitl/requirements.txt
================================================
pocketflow>=0.0.2
gradio>=5.29.1
openai>=1.78.1
================================================
FILE: cookbook/pocketflow-gradio-hitl/utils/call_llm.py
================================================
import os
from openai import OpenAI
from openai.types.chat.chat_completion import ChatCompletion
api_key = os.getenv("OPENAI_API_KEY")
base_url = "https://api.openai.com/v1"
model = "gpt-4o"
def call_llm(message: str):
print(f"Calling LLM with message: \n{message}")
client = OpenAI(api_key=api_key, base_url=base_url)
response: ChatCompletion = client.chat.completions.create(
model=model, messages=[{"role": "user", "content": message}]
)
return response.choices[0].message.content
if __name__ == "__main__":
print(call_llm("Hello, how are you?"))
================================================
FILE: cookbook/pocketflow-gradio-hitl/utils/call_mock_api.py
================================================
import random
from datetime import date, datetime
def call_check_weather_api(city: str, date: date | None):
if date is None:
date = datetime.now().date()
current_date = datetime.now().date()
# calculate date difference
date_diff = (date - current_date).days
# check if the date is within the allowed range
if abs(date_diff) > 7:
return f"Failed to check weather: Date {date} is more than 7 days away from current date."
return f"The weather in {city} on {date} is {random.choice(['sunny', 'cloudy', 'rainy', 'snowy'])}, and the temperature is {random.randint(10, 30)}°C."
def call_book_hotel_api(hotel: str, checkin_date: date, checkout_date: date):
current_date = datetime.now().date()
# check if the checkin date is after the current date
if checkin_date <= current_date:
return (
f"Failed to book hotel {hotel}: Check-in date must be after current date."
)
# check if the checkin date is before the checkout date
if checkin_date >= checkout_date:
return f"Failed to book hotel {hotel}, because the checkin date is after the checkout date."
# check if the date difference is more than 7 days
date_diff = (checkout_date - checkin_date).days
if date_diff > 7:
return f"Failed to book hotel {hotel}: Stay duration cannot exceed 7 days."
return f"Booked hotel {hotel} from {checkin_date.strftime('%Y-%m-%d')} to {checkout_date.strftime('%Y-%m-%d')} successfully."
================================================
FILE: cookbook/pocketflow-gradio-hitl/utils/conversation.py
================================================
conversation_cache = {}
def load_conversation(conversation_id: str):
print(f"Loading conversation {conversation_id}")
return conversation_cache.get(conversation_id, {})
def save_conversation(conversation_id: str, session: dict):
print(f"Saving conversation {session}")
conversation_cache[conversation_id] = session
================================================
FILE: cookbook/pocketflow-gradio-hitl/utils/format_chat_history.py
================================================
def format_chat_history(history):
"""
Format the chat history for LLM
Args:
history (list): The chat history list, each element contains role and content
Returns:
str: The formatted chat history string
"""
if not history:
return "No history"
formatted_history = []
for message in history:
role = "user" if message["role"] == "user" else "assistant"
content = message["content"]
# filter out the thinking content
if role == "assistant":
if (
content.startswith("- 🤔")
or content.startswith("- ➡️")
or content.startswith("- ⬅️")
):
continue
formatted_history.append(f"{role}: {content}")
return "\n".join(formatted_history)
================================================
FILE: cookbook/pocketflow-hello-world/README.md
================================================
# PocketFlow Hello World
Your first PocketFlow application! This simple example demonstrates how to create a basic PocketFlow app from scratch.
## Project Structure
```
.
├── docs/ # Documentation files
├── utils/ # Utility functions
├── flow.py # PocketFlow implementation
├── main.py # Main application entry point
└── README.md # Project documentation
```
## Setup
1. Create a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Run the example:
```bash
python main.py
```
## What This Example Demonstrates
- How to create your first PocketFlow application
- Basic PocketFlow concepts and usage
- Simple example of PocketFlow's capabilities
## Additional Resources
- [PocketFlow Documentation](https://the-pocket.github.io/PocketFlow/)
================================================
FILE: cookbook/pocketflow-hello-world/docs/design.md
================================================
# Your Project Title
## Project Requirements
A description of the project requirements.
## Utility Functions
1. **Call LLM** (`utils/call_llm.py`)
## Flow Design
1. **First Node**
2. **Second Node**
3. **Third Node**
### Flow Diagram
```mermaid
flowchart TD
firstNode[First Node] --> secondNode[Second Node]
secondNode --> thirdNode[Third Node]
```
## Data Structure
The shared memory structure will be organized as follows:
```python
shared = {
"key": "value"
}
```
## Node Designs
### 1. First Node
- **Purpose**: What the node does
- **Design**: Regular Node (no Batch/Async)
- **Data Access**:
- Read: "key" from shared store
- Write: "key" to shared store
### 2. Second Node
...
### 3. Third Node
================================================
FILE: cookbook/pocketflow-hello-world/flow.py
================================================
from pocketflow import Node, Flow
from utils.call_llm import call_llm
# An example node and flow
# Please replace this with your own node and flow
class AnswerNode(Node):
def prep(self, shared):
# Read question from shared
return shared["question"]
def exec(self, question):
return call_llm(question)
def post(self, shared, prep_res, exec_res):
# Store the answer in shared
shared["answer"] = exec_res
answer_node = AnswerNode()
qa_flow = Flow(start=answer_node)
================================================
FILE: cookbook/pocketflow-hello-world/main.py
================================================
from flow import qa_flow
# Example main function
# Please replace this with your own main function
def main():
shared = {
"question": "In one sentence, what's the end of universe?",
"answer": None
}
qa_flow.run(shared)
print("Question:", shared["question"])
print("Answer:", shared["answer"])
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-hello-world/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-hello-world/utils/call_llm.py
================================================
from openai import OpenAI
def call_llm(prompt):
client = OpenAI(api_key="YOUR_API_KEY_HERE")
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
if __name__ == "__main__":
prompt = "What is the meaning of life?"
print(call_llm(prompt))
================================================
FILE: cookbook/pocketflow-llm-streaming/README.md
================================================
# LLM Streaming and Interruption
Demonstrates real-time LLM response streaming with user interrupt capability.
- Check out the [Substack Post Tutorial](https://zacharyhuang.substack.com/p/streaming-llm-responses-tutorial) for more!
## Features
- Real-time display of LLM responses as they're generated
- User interrupt with ENTER key at any time
## Run It
```bash
pip install -r requirements.txt
python main.py
```
## How It Works
StreamNode:
1. Creates interrupt listener thread
2. Fetches content chunks from LLM
3. Displays chunks in real-time
4. Handles user interruption
## API Key
By default, demo uses fake streaming responses. To use real OpenAI streaming:
1. Edit main.py to replace the fake_stream_llm with stream_llm:
```python
# Change this line:
chunks = fake_stream_llm(prompt)
# To this:
chunks = stream_llm(prompt)
```
2. Make sure your OpenAI API key is set:
```bash
export OPENAI_API_KEY="your-api-key-here"
```
## Files
- `main.py`: StreamNode implementation
- `utils.py`: Real and fake LLM streaming functions
================================================
FILE: cookbook/pocketflow-llm-streaming/main.py
================================================
import time
import threading
from pocketflow import Node, Flow
from utils import fake_stream_llm, stream_llm
class StreamNode(Node):
def prep(self, shared):
# Create interrupt event
interrupt_event = threading.Event()
# Start a thread to listen for user interrupt
def wait_for_interrupt():
input("Press ENTER at any time to interrupt streaming...\n")
interrupt_event.set()
listener_thread = threading.Thread(target=wait_for_interrupt)
listener_thread.start()
# Get prompt from shared store
prompt = shared["prompt"]
# Get chunks from LLM function
chunks = stream_llm(prompt)
return chunks, interrupt_event, listener_thread
def exec(self, prep_res):
chunks, interrupt_event, listener_thread = prep_res
for chunk in chunks:
if interrupt_event.is_set():
print("User interrupted streaming.")
break
if hasattr(chunk.choices[0].delta, 'content') and chunk.choices[0].delta.content is not None:
chunk_content = chunk.choices[0].delta.content
print(chunk_content, end="", flush=True)
time.sleep(0.1) # simulate latency
return interrupt_event, listener_thread
def post(self, shared, prep_res, exec_res):
interrupt_event, listener_thread = exec_res
# Join the interrupt listener so it doesn't linger
interrupt_event.set()
listener_thread.join()
return "default"
# Usage:
node = StreamNode()
flow = Flow(start=node)
shared = {"prompt": "What's the meaning of life?"}
flow.run(shared)
================================================
FILE: cookbook/pocketflow-llm-streaming/requirements.txt
================================================
pocketflow>=0.0.1
openai>=1.0.0
================================================
FILE: cookbook/pocketflow-llm-streaming/utils.py
================================================
from openai import OpenAI
import os
def stream_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
# Make a streaming chat completion request
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": prompt}
],
temperature=0.7,
stream=True # Enable streaming
)
return response
def fake_stream_llm(prompt, predefined_text="This is a fake response. Today is a sunny day. The sun is shining. The birds are singing. The flowers are blooming. The bees are buzzing. The wind is blowing. The clouds are drifting. The sky is blue. The grass is green. The trees are tall. The water is clear. The fish are swimming. The sun is shining. The birds are singing. The flowers are blooming. The bees are buzzing. The wind is blowing. The clouds are drifting. The sky is blue. The grass is green. The trees are tall. The water is clear. The fish are swimming."):
"""
Returns a list of simple objects that mimic the structure needed
for OpenAI streaming responses.
"""
# Split text into small chunks
chunk_size = 10
chunks = []
# Create the chunks using a simple class outside the nested structure
class SimpleObject:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
# Build the chunks
for i in range(0, len(predefined_text), chunk_size):
text_chunk = predefined_text[i:i+chunk_size]
# Create the nested structure using simple objects
delta = SimpleObject(content=text_chunk)
choice = SimpleObject(delta=delta)
chunk = SimpleObject(choices=[choice])
chunks.append(chunk)
return chunks
if __name__ == "__main__":
print("## Testing streaming LLM")
prompt = "What's the meaning of life?"
print(f"## Prompt: {prompt}")
# response = fake_stream_llm(prompt)
response = stream_llm(prompt)
print(f"## Response: ")
for chunk in response:
if hasattr(chunk.choices[0].delta, 'content') and chunk.choices[0].delta.content is not None:
chunk_content = chunk.choices[0].delta.content
# Print the incoming text without a newline (simulate real-time streaming)
print(chunk_content, end="", flush=True)
================================================
FILE: cookbook/pocketflow-majority-vote/README.md
================================================
# Majority Vote Reasoning
This project demonstrates a majority vote implementation that enables LLMs to solve complex reasoning problems by aggregating multiple independent attempts. It's designed to improve problem-solving accuracy through consensus-based reasoning.
## Features
- Improves model reliability on complex problems through multiple attempts
- Works with models like Claude 3.7 Sonnet
- Solves problems that single attempts often fail on
- Provides detailed reasoning traces for verification
- Uses a consensus approach to reduce the impact of occasional reasoning errors
## Getting Started
1. Install the required packages:
```bash
pip install -r requirements.txt
```
2. Set up your API key:
```bash
export ANTHROPIC_API_KEY="your-api-key-here"
```
3. Run a test problem to see majority voting in action:
```bash
python main.py
```
4. Try your own reasoning problem:
```bash
python main.py --problem "Your complex reasoning problem here" --tries 5
```
## How It Works
The implementation uses a MajorityVoteNode that processes multiple attempts and finds consensus:
```mermaid
flowchart LR
mv[MajorityVoteNode]
```
The MajorityVoteNode:
1. Makes multiple independent attempts to solve the same problem
2. Collects structured answers from each attempt
3. Determines the most frequent answer as the final solution
4. Returns the consensus answer
This approach helps overcome occasional reasoning errors that might occur in individual attempts.
## Example Problem
Example Problem from [Quant Interview](https://www.youtube.com/watch?v=SCP7JptxPU0):
```
You work at a shoe factory. In front of you, there are three pairs of shoes (six individual shoes) with the following sizes: two size 4s, two size 5s, and two size 6s. The factory defines an "acceptable pair" as two shoes that differ in size by a maximum of one size (e.g., a size 5 and a size 6 would be an acceptable pair). If you close your eyes and randomly pick three pairs of shoes without replacement, what is the probability that you end up drawing three acceptable pairs?
```
Below is an example of how the majority vote approach uses Claude 3.7 Sonnet to solve this complex problem:
```
========================
All structured answers: ['0.333', '0.333', '0.333', '0.6', '0.333']
Majority vote => 0.333
Frequency => 4
========================
=== Final Answer ===
0.333
====================
```
This shows that 4 out of 5 attempts yielded the same answer (0.333), which is chosen as the final solution.
## Files
- [`main.py`](./main.py): Implementation of the majority vote node and flow
- [`utils.py`](./utils.py): Simple wrapper for calling the Anthropic model
================================================
FILE: cookbook/pocketflow-majority-vote/main.py
================================================
import argparse
from pocketflow import BatchNode, Flow
import collections
from utils import call_llm
import yaml
class MajorityVoteNode(BatchNode):
def prep(self, shared):
question = shared.get("question", "(No question provided)")
attempts_count = shared.get("num_tries", 3)
return [question for _ in range(attempts_count)]
def exec(self, single_question: str):
prompt = f"""
You are a helpful assistant. Please answer the user's question below.
Question: {single_question}
Return strictly using the following YAML structure:
```yaml
thinking: |
(Your thinking process here)
answer: 0.123 # Final answer as a decimal with 3 decimal places
```"""
raw_response = call_llm(prompt)
yaml_part = raw_response.split("```yaml")[1].split("```")[0].strip()
parsed = yaml.safe_load(yaml_part)
# Validate we have at least 'answer' field
if not isinstance(parsed, dict) or 'answer' not in parsed:
raise RuntimeError(f"Missing 'answer' in YAML: {parsed}")
# Return only the 'answer' field for the majority vote.
return str(parsed['answer'])
def exec_fallback(self, prep_res, exc):
return None
def post(self, shared, prep_res, exec_res_list):
# Count frequency for non-None answers
exec_res_list = [res for res in exec_res_list if res is not None]
counter = collections.Counter(exec_res_list)
best_answer, freq = counter.most_common(1)[0]
# Store final
shared["majority_answer"] = best_answer
print("========================")
print("All structured answers:", exec_res_list)
print("Majority vote =>", best_answer)
print("Frequency =>", freq)
print("========================")
# End the flow
return "end"
if __name__ == "__main__":
# Set up argument parser
parser = argparse.ArgumentParser(description="Run majority vote reasoning on a problem")
parser.add_argument("--problem", type=str, help="Your reasoning problem to solve")
parser.add_argument("--tries", type=int, default=5, help="Number of attempts to make (default: 5)")
args = parser.parse_args()
# Default problem if none provided
default_problem = """You work at a shoe factory. In front of you, there are three pairs of shoes (six individual shoes) with the following sizes: two size 4s, two size 5s, and two size 6s. The factory defines an "acceptable pair" as two shoes that differ in size by a maximum of one size (e.g., a size 5 and a size 6 would be an acceptable pair). If you close your eyes and randomly pick three pairs of shoes without replacement, what is the probability that you end up drawing three acceptable pairs?"""
shared = {
"question": args.problem if args.problem else default_problem,
"num_tries": args.tries
}
majority_node = MajorityVoteNode()
flow = Flow(start=majority_node)
flow.run(shared)
print("\n=== Final Answer ===")
print(shared["majority_answer"])
print("====================")
================================================
FILE: cookbook/pocketflow-majority-vote/requirements.txt
================================================
pocketflow>=0.0.1
anthropic>=0.15.0
pyyaml>=6.0
================================================
FILE: cookbook/pocketflow-majority-vote/utils.py
================================================
from anthropic import Anthropic
import os
def call_llm(prompt):
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "your-api-key"))
response = client.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=10000,
messages=[
{"role": "user", "content": prompt}
]
)
return response.content[0].text
if __name__ == "__main__":
print("## Testing call_llm")
prompt = "In a few words, what is the meaning of life?"
print(f"## Prompt: {prompt}")
response = call_llm(prompt)
print(f"## Response: {response}")
================================================
FILE: cookbook/pocketflow-map-reduce/README.md
================================================
# Resume Qualification - Map Reduce Example
A PocketFlow example that demonstrates how to implement a Map-Reduce pattern for processing and evaluating resumes.
## Features
- Read and process multiple resume files using a Map-Reduce pattern
- Evaluate each resume individually using an LLM with structured YAML output
- Determine if candidates qualify for technical roles based on specific criteria
- Aggregate results to generate qualification statistics and summaries
## Getting Started
1. Install the required dependencies:
```bash
pip install -r requirements.txt
```
2. Set your OpenAI API key as an environment variable:
```bash
export OPENAI_API_KEY=your_api_key_here
```
3. Run the application:
```bash
python main.py
```
## How It Works
The workflow follows a classic Map-Reduce pattern with three sequential nodes:
```mermaid
flowchart LR
ReadResumes[Map: Read Resumese] --> EvaluateResumes[Batch: Evaluate Resumes]
EvaluateResumes --> ReduceResults[Reduce: Aggregate Results]
```
Here's what each node does:
1. **ReadResumesNode (Map Phase)**: Reads all resume files from the data directory and stores them in the shared data store
2. **EvaluateResumesNode (Batch Processing)**: Processes each resume individually using an LLM to determine if candidates qualify
3. **ReduceResultsNode (Reduce Phase)**: Aggregates evaluation results and produces a summary of qualified candidates
## Files
- [`main.py`](./main.py): Main entry point for running the resume qualification workflow
- [`flow.py`](./flow.py): Defines the flow that connects the nodes
- [`nodes.py`](./nodes.py): Contains the node classes for each step in the workflow
- [`utils.py`](./utils.py): Utility functions including the LLM wrapper
- [`requirements.txt`](./requirements.txt): Lists the required dependencies
- [`data/`](./data/): Directory containing sample resume files for evaluation
## Example Output
```
Starting resume qualification processing...
===== Resume Qualification Summary =====
Total candidates evaluated: 5
Qualified candidates: 2 (40.0%)
Qualified candidates:
- Emily Johnson
- John Smith
Detailed evaluation results:
✗ Michael Williams (resume3.txt)
✓ Emily Johnson (resume2.txt)
✗ Lisa Chen (resume4.txt)
✗ Robert Taylor (resume5.txt)
✓ John Smith (resume1.txt)
Resume processing complete!
```
================================================
FILE: cookbook/pocketflow-map-reduce/data/resume1.txt
================================================
John Smith
Software Engineer
Education:
- Master of Computer Science, Stanford University, 2018
- Bachelor of Computer Science, MIT, 2016
Experience:
- Senior Software Engineer, Google, 2019-present
* Led the development of cloud infrastructure projects
* Implemented scalable solutions using Kubernetes and Docker
* Reduced system latency by 40% through optimization
- Software Developer, Microsoft, 2016-2019
* Worked on Azure cloud services
* Built RESTful APIs for enterprise solutions
Skills:
- Programming: Python, Java, C++, JavaScript
- Technologies: Docker, Kubernetes, AWS, Azure
- Tools: Git, Jenkins, Jira
Projects:
- Developed a recommendation engine that increased user engagement by 25%
- Created a sentiment analysis tool using NLP techniques
================================================
FILE: cookbook/pocketflow-map-reduce/data/resume2.txt
================================================
Emily Johnson
Data Scientist
Education:
- Ph.D. in Statistics, UC Berkeley, 2020
- Master of Science in Mathematics, UCLA, 2016
Experience:
- Data Scientist, Netflix, 2020-present
* Developed machine learning models for content recommendation
* Implemented A/B testing frameworks to optimize user experience
* Collaborated with product teams to define metrics and KPIs
- Data Analyst, Amazon, 2016-2020
* Analyzed user behavior patterns to improve conversion rates
* Created dashboards and visualizations for executive decision-making
Skills:
- Programming: R, Python, SQL
- Machine Learning: TensorFlow, PyTorch, scikit-learn
- Data Visualization: Tableau, PowerBI, matplotlib
Publications:
- "Advances in Recommendation Systems" - Journal of Machine Learning, 2021
- "Statistical Methods for Big Data" - Conference on Data Science, 2019
================================================
FILE: cookbook/pocketflow-map-reduce/data/resume3.txt
================================================
Michael Williams
Marketing Manager
Education:
- MBA, Harvard Business School, 2015
- Bachelor of Arts in Communications, NYU, 2010
Experience:
- Marketing Director, Apple, 2018-present
* Managed a team of 15 marketing professionals
* Developed and executed global marketing campaigns
* Increased brand awareness by 30% through digital initiatives
- Marketing Manager, Coca-Cola, 2015-2018
* Led product launches across North America
* Coordinated with external agencies on advertising campaigns
Skills:
- Digital Marketing: SEO, SEM, Social Media Marketing
- Analytics: Google Analytics, Adobe Analytics
- Tools: HubSpot, Salesforce, Marketo
Achievements:
- Marketing Excellence Award, 2020
- Led campaign that won Cannes Lions Award, 2019
================================================
FILE: cookbook/pocketflow-map-reduce/data/resume4.txt
================================================
Lisa Chen
Frontend Developer
Education:
- Bachelor of Fine Arts, Rhode Island School of Design, 2019
Experience:
- UI/UX Designer, Airbnb, 2020-present
* Designed user interfaces for mobile and web applications
* Created wireframes and prototypes for new features
* Conducted user research and usability testing
- Junior Designer, Freelance, 2019-2020
* Worked with small businesses on branding and website design
* Developed responsive web designs using HTML, CSS, and JavaScript
Skills:
- Design: Figma, Sketch, Adobe XD
- Development: HTML, CSS, JavaScript, React
- Tools: Git, Zeplin
Portfolio Highlights:
- Redesigned checkout flow resulting in 15% conversion increase
- Created custom icon set for mobile application
- Designed responsive email templates
Certifications:
- UI/UX Design Certificate, Coursera, 2019
================================================
FILE: cookbook/pocketflow-map-reduce/data/resume5.txt
================================================
Robert Taylor
Sales Representative
Education:
- Bachelor of Business Administration, University of Texas, 2017
Experience:
- Account Executive, Salesforce, 2019-present
* Exceeded sales targets by 25% for three consecutive quarters
* Managed a portfolio of 50+ enterprise clients
* Developed and implemented strategic account plans
- Sales Associate, Oracle, 2017-2019
* Generated new business opportunities through cold calling
* Assisted senior sales representatives with client presentations
Skills:
- CRM Systems: Salesforce, HubSpot
- Communication: Negotiation, Public Speaking
- Tools: Microsoft Office Suite, Google Workspace
Achievements:
- Top Sales Representative Award, Q2 2020
- President's Club, 2021
Interests:
- Volunteer sales coach for local small businesses
- Member of Toastmasters International
================================================
FILE: cookbook/pocketflow-map-reduce/flow.py
================================================
from pocketflow import Flow
from nodes import ReadResumesNode, EvaluateResumesNode, ReduceResultsNode
def create_resume_processing_flow():
"""Create a map-reduce flow for processing resumes."""
# Create nodes
read_resumes_node = ReadResumesNode()
evaluate_resumes_node = EvaluateResumesNode()
reduce_results_node = ReduceResultsNode()
# Connect nodes
read_resumes_node >> evaluate_resumes_node >> reduce_results_node
# Create flow
return Flow(start=read_resumes_node)
================================================
FILE: cookbook/pocketflow-map-reduce/main.py
================================================
from flow import create_resume_processing_flow
def main():
# Initialize shared store
shared = {}
# Create the resume processing flow
resume_flow = create_resume_processing_flow()
# Run the flow
print("Starting resume qualification processing...")
resume_flow.run(shared)
# Display final summary information (additional to what's already printed in ReduceResultsNode)
if "summary" in shared:
print("\nDetailed evaluation results:")
for filename, evaluation in shared.get("evaluations", {}).items():
qualified = "✓" if evaluation.get("qualifies", False) else "✗"
name = evaluation.get("candidate_name", "Unknown")
print(f"{qualified} {name} ({filename})")
print("\nResume processing complete!")
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-map-reduce/nodes.py
================================================
from pocketflow import Node, BatchNode
from utils import call_llm
import yaml
import os
class ReadResumesNode(Node):
"""Map phase: Read all resumes from the data directory into shared storage."""
def exec(self, _):
resume_files = {}
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
for filename in os.listdir(data_dir):
if filename.endswith(".txt"):
file_path = os.path.join(data_dir, filename)
with open(file_path, 'r', encoding='utf-8') as file:
resume_files[filename] = file.read()
return resume_files
def post(self, shared, prep_res, exec_res):
shared["resumes"] = exec_res
return "default"
class EvaluateResumesNode(BatchNode):
"""Batch processing: Evaluate each resume to determine if the candidate qualifies."""
def prep(self, shared):
return list(shared["resumes"].items())
def exec(self, resume_item):
"""Evaluate a single resume."""
filename, content = resume_item
prompt = f"""
Evaluate the following resume and determine if the candidate qualifies for an advanced technical role.
Criteria for qualification:
- At least a bachelor's degree in a relevant field
- At least 3 years of relevant work experience
- Strong technical skills relevant to the position
Resume:
{content}
Return your evaluation in YAML format:
```yaml
candidate_name: [Name of the candidate]
qualifies: [true/false]
reasons:
- [First reason for qualification/disqualification]
- [Second reason, if applicable]
```
"""
response = call_llm(prompt)
# Extract YAML content
yaml_content = response.split("```yaml")[1].split("```")[0].strip() if "```yaml" in response else response
result = yaml.safe_load(yaml_content)
return (filename, result)
def post(self, shared, prep_res, exec_res_list):
shared["evaluations"] = {filename: result for filename, result in exec_res_list}
return "default"
class ReduceResultsNode(Node):
"""Reduce node: Count and print out how many candidates qualify."""
def prep(self, shared):
return shared["evaluations"]
def exec(self, evaluations):
qualified_count = 0
total_count = len(evaluations)
qualified_candidates = []
for filename, evaluation in evaluations.items():
if evaluation.get("qualifies", False):
qualified_count += 1
qualified_candidates.append(evaluation.get("candidate_name", "Unknown"))
summary = {
"total_candidates": total_count,
"qualified_count": qualified_count,
"qualified_percentage": round(qualified_count / total_count * 100, 1) if total_count > 0 else 0,
"qualified_names": qualified_candidates
}
return summary
def post(self, shared, prep_res, exec_res):
shared["summary"] = exec_res
print("\n===== Resume Qualification Summary =====")
print(f"Total candidates evaluated: {exec_res['total_candidates']}")
print(f"Qualified candidates: {exec_res['qualified_count']} ({exec_res['qualified_percentage']}%)")
if exec_res['qualified_names']:
print("\nQualified candidates:")
for name in exec_res['qualified_names']:
print(f"- {name}")
return "default"
================================================
FILE: cookbook/pocketflow-map-reduce/requirements.txt
================================================
pocketflow>=0.0.1
openai>=1.0.0
pyyaml>=6.0
================================================
FILE: cookbook/pocketflow-map-reduce/utils.py
================================================
import os
from openai import OpenAI
def call_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
# Example usage
if __name__ == "__main__":
print(call_llm("Tell me a short joke"))
================================================
FILE: cookbook/pocketflow-mcp/README.md
================================================
# PocketFlow MCP Demo
This project shows how to build an agent that performs addition using PocketFlow and Model Context Protocol (MCP). It presents a comparison between using MCP and basic function calling approaches.
This implementation is based on the tutorial: [MCP Simply Explained: Function Calling Rebranded or Genuine Breakthrough?](https://zacharyhuang.substack.com/p/mcp-simply-explained-function-calling)
## Features
- Mathematical operation tools through a simple terminal interface
- Integration with Model Context Protocol (MCP)
- Comparison between MCP and direct function calling
- **Simple toggle** between MCP and local function calling
## How to Run
1. Set your API key:
```bash
export OPENAI_API_KEY="your-api-key-here"
```
Or update it directly in `utils.py`
2. Install and run:
```bash
pip install -r requirements.txt
python main.py
```
## MCP vs Function Calling
To compare both approaches, this demo provides local function alternatives that don't require MCP:
- **Toggle with a simple flag:** Set `MCP = True` or `MCP = False` at the top of `utils.py` to switch between MCP and local implementations.
- No code changes needed! The application automatically uses either:
- MCP server tools when `MCP = True`
- Local function implementations when `MCP = False`
This allows you to see the difference between the two approaches while keeping the same workflow.
### Function Calling
- Functions are directly embedded in application code
- Each new tool requires modifying the application
- Tools are defined within the application itself
### MCP Approach
- Tools live in separate MCP servers
- Standard protocol for all tool interactions
- New tools can be added without changing the agent
- AI can interact with tools through a consistent interface
## How It Works
```mermaid
flowchart LR
tools[GetToolsNode] -->|decide| decide[DecideToolNode]
decide -->|execute| execute[ExecuteToolNode]
```
The agent uses PocketFlow to create a workflow where:
1. It takes user input about numbers
2. Connects to the MCP server for mathematical operations (or uses local functions based on the `MCP` flag)
3. Returns the result
## Files
- [`main.py`](./main.py): Implementation of the addition agent using PocketFlow
- [`utils.py`](./utils.py): Helper functions for API calls and MCP integration
- [`simple_server.py`](./simple_server.py): MCP server that provides the addition tool
================================================
FILE: cookbook/pocketflow-mcp/main.py
================================================
from pocketflow import Node, Flow
from utils import call_llm, get_tools, call_tool
import yaml
import sys
class GetToolsNode(Node):
def prep(self, shared):
"""Initialize and get tools"""
# The question is now passed from main via shared
print("🔍 Getting available tools...")
return "simple_server.py"
def exec(self, server_path):
"""Retrieve tools from the MCP server"""
tools = get_tools(server_path)
return tools
def post(self, shared, prep_res, exec_res):
"""Store tools and process to decision node"""
tools = exec_res
shared["tools"] = tools
# Format tool information for later use
tool_info = []
for i, tool in enumerate(tools, 1):
properties = tool.inputSchema.get('properties', {})
required = tool.inputSchema.get('required', [])
params = []
for param_name, param_info in properties.items():
param_type = param_info.get('type', 'unknown')
req_status = "(Required)" if param_name in required else "(Optional)"
params.append(f" - {param_name} ({param_type}): {req_status}")
tool_info.append(f"[{i}] {tool.name}\n Description: {tool.description}\n Parameters:\n" + "\n".join(params))
shared["tool_info"] = "\n".join(tool_info)
return "decide"
class DecideToolNode(Node):
def prep(self, shared):
"""Prepare the prompt for LLM to process the question"""
tool_info = shared["tool_info"]
question = shared["question"]
prompt = f"""
### CONTEXT
You are an assistant that can use tools via Model Context Protocol (MCP).
### ACTION SPACE
{tool_info}
### TASK
Answer this question: "{question}"
## NEXT ACTION
Analyze the question, extract any numbers or parameters, and decide which tool to use.
Return your response in this format:
```yaml
thinking: |
tool:
reason:
parameters:
: :
```
IMPORTANT:
1. Extract numbers from the question properly
2. Use proper indentation (4 spaces) for multi-line fields
3. Use the | character for multi-line text fields
"""
return prompt
def exec(self, prompt):
"""Call LLM to process the question and decide which tool to use"""
print("🤔 Analyzing question and deciding which tool to use...")
response = call_llm(prompt)
return response
def post(self, shared, prep_res, exec_res):
"""Extract decision from YAML and save to shared context"""
try:
yaml_str = exec_res.split("```yaml")[1].split("```")[0].strip()
decision = yaml.safe_load(yaml_str)
shared["tool_name"] = decision["tool"]
shared["parameters"] = decision["parameters"]
shared["thinking"] = decision.get("thinking", "")
print(f"💡 Selected tool: {decision['tool']}")
print(f"🔢 Extracted parameters: {decision['parameters']}")
return "execute"
except Exception as e:
print(f"❌ Error parsing LLM response: {e}")
print("Raw response:", exec_res)
return None
class ExecuteToolNode(Node):
def prep(self, shared):
"""Prepare tool execution parameters"""
return shared["tool_name"], shared["parameters"]
def exec(self, inputs):
"""Execute the chosen tool"""
tool_name, parameters = inputs
print(f"🔧 Executing tool '{tool_name}' with parameters: {parameters}")
result = call_tool("simple_server.py", tool_name, parameters)
return result
def post(self, shared, prep_res, exec_res):
print(f"\n✅ Final Answer: {exec_res}")
return "done"
if __name__ == "__main__":
# Default question
default_question = "What is 982713504867129384651 plus 73916582047365810293746529?"
# Get question from command line if provided with --
question = default_question
for arg in sys.argv[1:]:
if arg.startswith("--"):
question = arg[2:]
break
print(f"🤔 Processing question: {question}")
# Create nodes
get_tools_node = GetToolsNode()
decide_node = DecideToolNode()
execute_node = ExecuteToolNode()
# Connect nodes
get_tools_node - "decide" >> decide_node
decide_node - "execute" >> execute_node
# Create and run flow
flow = Flow(start=get_tools_node)
shared = {"question": question}
flow.run(shared)
================================================
FILE: cookbook/pocketflow-mcp/requirements.txt
================================================
pocketflow>=0.0.1
openai>=1.0.0
fastmcp
pyyaml
================================================
FILE: cookbook/pocketflow-mcp/simple_server.py
================================================
from fastmcp import FastMCP
# Create a named server
mcp = FastMCP("Math Operations Server")
# Define mathematical operation tools
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers together"""
return a + b
@mcp.tool()
def subtract(a: int, b: int) -> int:
"""Subtract b from a"""
return a - b
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two numbers together"""
return a * b
@mcp.tool()
def divide(a: int, b: int) -> float:
"""Divide a by b"""
if b == 0:
raise ValueError("Division by zero is not allowed")
return a / b
# Start the server
if __name__ == "__main__":
mcp.run()
================================================
FILE: cookbook/pocketflow-mcp/utils.py
================================================
from openai import OpenAI
import os
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
# Global flag to control whether to use MCP or local implementation
MCP = False
def call_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
def get_tools(server_script_path=None):
"""Get available tools, either from MCP server or locally based on MCP global setting."""
if MCP:
return mcp_get_tools(server_script_path)
else:
return local_get_tools(server_script_path)
def mcp_get_tools(server_script_path):
"""Get available tools from an MCP server.
"""
async def _get_tools():
server_params = StdioServerParameters(
command="python",
args=[server_script_path]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools_response = await session.list_tools()
return tools_response.tools
return asyncio.run(_get_tools())
def local_get_tools(server_script_path=None):
"""A simple dummy implementation of get_tools without MCP."""
tools = [
{
"name": "add",
"description": "Add two numbers together",
"inputSchema": {
"properties": {
"a": {"type": "integer"},
"b": {"type": "integer"}
},
"required": ["a", "b"]
}
},
{
"name": "subtract",
"description": "Subtract b from a",
"inputSchema": {
"properties": {
"a": {"type": "integer"},
"b": {"type": "integer"}
},
"required": ["a", "b"]
}
},
{
"name": "multiply",
"description": "Multiply two numbers together",
"inputSchema": {
"properties": {
"a": {"type": "integer"},
"b": {"type": "integer"}
},
"required": ["a", "b"]
}
},
{
"name": "divide",
"description": "Divide a by b",
"inputSchema": {
"properties": {
"a": {"type": "integer"},
"b": {"type": "integer"}
},
"required": ["a", "b"]
}
}
]
class DictObject(dict):
"""A simple class that behaves both as a dictionary and as an object with attributes."""
def __init__(self, data):
super().__init__(data)
for key, value in data.items():
if isinstance(value, dict):
self[key] = DictObject(value)
elif isinstance(value, list) and value and isinstance(value[0], dict):
self[key] = [DictObject(item) for item in value]
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(f"'DictObject' object has no attribute '{key}'")
return [DictObject(tool) for tool in tools]
def call_tool(server_script_path=None, tool_name=None, arguments=None):
"""Call a tool, either from MCP server or locally based on MCP global setting."""
if MCP:
return mcp_call_tool(server_script_path, tool_name, arguments)
else:
return local_call_tool(server_script_path, tool_name, arguments)
def mcp_call_tool(server_script_path=None, tool_name=None, arguments=None):
"""Call a tool on an MCP server.
"""
async def _call_tool():
server_params = StdioServerParameters(
command="python",
args=[server_script_path]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool(tool_name, arguments)
return result.content[0].text
return asyncio.run(_call_tool())
def local_call_tool(server_script_path=None, tool_name=None, arguments=None):
"""A simple dummy implementation of call_tool without MCP."""
# Simple implementation of tools
if tool_name == "add":
if "a" in arguments and "b" in arguments:
return arguments["a"] + arguments["b"]
else:
return "Error: Missing required arguments 'a' or 'b'"
elif tool_name == "subtract":
if "a" in arguments and "b" in arguments:
return arguments["a"] - arguments["b"]
else:
return "Error: Missing required arguments 'a' or 'b'"
elif tool_name == "multiply":
if "a" in arguments and "b" in arguments:
return arguments["a"] * arguments["b"]
else:
return "Error: Missing required arguments 'a' or 'b'"
elif tool_name == "divide":
if "a" in arguments and "b" in arguments:
if arguments["b"] == 0:
return "Error: Division by zero is not allowed"
return arguments["a"] / arguments["b"]
else:
return "Error: Missing required arguments 'a' or 'b'"
else:
return f"Error: Unknown tool '{tool_name}'"
if __name__ == "__main__":
print("=== Testing call_llm ===")
prompt = "In a few words, what is the meaning of life?"
print(f"Prompt: {prompt}")
response = call_llm(prompt)
print(f"Response: {response}")
# Find available tools
print("=== Finding available tools ===")
tools = get_tools("simple_server.py")
# Print tool information nicely formatted
for i, tool in enumerate(tools, 1):
print(f"\nTool {i}: {tool.name}")
print("=" * (len(tool.name) + 8))
print(f"Description: {tool.description}")
# Parameters section
print("Parameters:")
properties = tool.inputSchema.get('properties', {})
required = tool.inputSchema.get('required', [])
# No parameters case
if not properties:
print(" None")
# Print each parameter with its details
for param_name, param_info in properties.items():
param_type = param_info.get('type', 'unknown')
req_status = "(Required)" if param_name in required else "(Optional)"
print(f" • {param_name}: {param_type} {req_status}")
# Call a tool
print("\n=== Calling the add tool ===")
a, b = 5, 3
result = call_tool("simple_server.py", "add", {"a": a, "b": b})
print(f"Result of {a} + {b} = {result}")
# You can easily call with different parameters
a, b = 10, 20
result = call_tool("simple_server.py", "add", {"a": a, "b": b})
print(f"Result of {a} + {b} = {result}")
================================================
FILE: cookbook/pocketflow-multi-agent/README.md
================================================
# Multi-Agent Taboo Game
A PocketFlow example that demonstrates how to implement asynchronous multi-agent communication using the Taboo word guessing game.
## Features
- Implement asynchronous communication between two AI agents (Hinter and Guesser)
- Use AsyncNode for non-blocking agent interactions
- Create dynamic conversation flow through asyncio message queues
- Demonstrate complex turn-based game mechanics with LLMs
- Automatically terminate the game when the correct word is guessed
## Getting Started
1. Install the required dependencies:
```bash
pip install -r requirements.txt
```
2. Set your OpenAI API key as an environment variable:
```bash
export OPENAI_API_KEY=your_api_key_here
```
3. Run the application:
```bash
python main.py
```
## How It Works
The workflow follows an asynchronous multi-agent communication pattern:
```mermaid
flowchart LR
AsyncHinter[AsyncHinter Node] <--> MessageQueue{Message Queue}
MessageQueue <--> AsyncGuesser[AsyncGuesser Node]
```
Here's what each component does:
1. **AsyncHinter Node**: Generates hints about the target word while avoiding forbidden words
2. **AsyncGuesser Node**: Makes guesses based on the hints received from the Hinter
3. **Message Queue**: Facilitates asynchronous communication between the agents
## Files
- [`main.py`](./main.py): Main entry point implementing the AsyncHinter and AsyncGuesser nodes and game flow
- [`utils.py`](./utils.py): Utility functions including LLM wrappers for generating hints and guesses
- [`requirements.txt`](./requirements.txt): Lists the required dependencies
## Example Output
```
=========== Taboo Game Starting! ===========
Target word: nostalgic
Forbidden words: ['memory', 'past', 'remember', 'feeling', 'longing']
============================================
Hinter: Here's your hint - Sentiment for earlier times.
Guesser: I guess it's - Nostalgia
Hinter: Here's your hint - Sentiment for earlier times.
Guesser: I guess it's - Reminiscence
Hinter: Here's your hint - Yearning for days gone by.
Guesser: I guess it's - Sentimentality
Hinter: Here's your hint - Reliving cherished moments or experiences.
Guesser: I guess it's - Memories
Hinter: Here's your hint - Recollection of cherished experiences.
Guesser: I guess it's - Reflection
Hinter: Here's your hint - Yearning for earlier times.
Guesser: I guess it's - Longing
Hinter: Here's your hint - Sentiment for earlier times.
Guesser: I guess it's - Nostalgic
Game Over - Correct guess!
================================================
FILE: cookbook/pocketflow-multi-agent/main.py
================================================
import asyncio
from pocketflow import AsyncNode, AsyncFlow
from utils import call_llm
class AsyncHinter(AsyncNode):
async def prep_async(self, shared):
# Wait for message from guesser (or empty string at start)
guess = await shared["hinter_queue"].get()
if guess == "GAME_OVER":
return None
return shared["target_word"], shared["forbidden_words"], shared.get("past_guesses", [])
async def exec_async(self, inputs):
if inputs is None:
return None
target, forbidden, past_guesses = inputs
prompt = f"Generate hint for '{target}'\nForbidden words: {forbidden}"
if past_guesses:
prompt += f"\nPrevious wrong guesses: {past_guesses}\nMake hint more specific."
prompt += "\nUse at most 5 words."
hint = call_llm(prompt)
print(f"\nHinter: Here's your hint - {hint}")
return hint
async def post_async(self, shared, prep_res, exec_res):
if exec_res is None:
return "end"
# Send hint to guesser
await shared["guesser_queue"].put(exec_res)
return "continue"
class AsyncGuesser(AsyncNode):
async def prep_async(self, shared):
# Wait for hint from hinter
hint = await shared["guesser_queue"].get()
return hint, shared.get("past_guesses", [])
async def exec_async(self, inputs):
hint, past_guesses = inputs
prompt = f"Given hint: {hint}, past wrong guesses: {past_guesses}, make a new guess. Directly reply a single word:"
guess = call_llm(prompt)
print(f"Guesser: I guess it's - {guess}")
return guess
async def post_async(self, shared, prep_res, exec_res):
# Check if guess is correct
if exec_res.lower() == shared["target_word"].lower():
print("Game Over - Correct guess!")
await shared["hinter_queue"].put("GAME_OVER")
return "end"
# Store the guess in shared state
if "past_guesses" not in shared:
shared["past_guesses"] = []
shared["past_guesses"].append(exec_res)
# Send guess to hinter
await shared["hinter_queue"].put(exec_res)
return "continue"
async def main():
# Set up game
shared = {
"target_word": "nostalgic",
"forbidden_words": ["memory", "past", "remember", "feeling", "longing"],
"hinter_queue": asyncio.Queue(),
"guesser_queue": asyncio.Queue()
}
print("=========== Taboo Game Starting! ===========")
print(f"Target word: {shared['target_word']}")
print(f"Forbidden words: {shared['forbidden_words']}")
print("============================================")
# Initialize by sending empty guess to hinter
await shared["hinter_queue"].put("")
# Create nodes and flows
hinter = AsyncHinter()
guesser = AsyncGuesser()
# Set up flows
hinter_flow = AsyncFlow(start=hinter)
guesser_flow = AsyncFlow(start=guesser)
# Connect nodes to themselves for looping
hinter - "continue" >> hinter
guesser - "continue" >> guesser
# Run both agents concurrently
await asyncio.gather(
hinter_flow.run_async(shared),
guesser_flow.run_async(shared)
)
print("=========== Game Complete! ===========")
if __name__ == "__main__":
asyncio.run(main())
================================================
FILE: cookbook/pocketflow-multi-agent/requirements.txt
================================================
pocketflow>=0.0.1
openai>=1.0.0
pyyaml>=6.0
================================================
FILE: cookbook/pocketflow-multi-agent/utils.py
================================================
import os
from openai import OpenAI
def call_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
r = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
# Example usage
if __name__ == "__main__":
print(call_llm("Tell me a short joke"))
================================================
FILE: cookbook/pocketflow-nested-batch/README.md
================================================
# PocketFlow Nested BatchFlow Example
This example demonstrates Nested BatchFlow using a simple school grades calculator.
## What this Example Does
Calculates average grades for:
1. Each student in a class
2. Each class in the school
## Structure
```
school/
├── class_a/
│ ├── student1.txt (grades: 7.5, 8.0, 9.0)
│ └── student2.txt (grades: 8.5, 7.0, 9.5)
└── class_b/
├── student3.txt (grades: 6.5, 8.5, 7.0)
└── student4.txt (grades: 9.0, 9.5, 8.0)
```
## How it Works
1. **Outer BatchFlow (SchoolBatchFlow)**
- Processes each class folder
- Returns parameters like: `{"class": "class_a"}`
2. **Inner BatchFlow (ClassBatchFlow)**
- Processes each student file in a class
- Returns parameters like: `{"student": "student1.txt"}`
3. **Base Flow**
- Loads student grades
- Calculates average
- Saves result
## Running the Example
```bash
pip install -r requirements.txt
python main.py
```
## Expected Output
```
Processing class_a...
- student1: Average = 8.2
- student2: Average = 8.3
Class A Average: 8.25
Processing class_b...
- student3: Average = 7.3
- student4: Average = 8.8
Class B Average: 8.05
School Average: 8.15
```
## Key Concepts
1. **Nested BatchFlow**: One BatchFlow inside another
2. **Parameter Inheritance**: Inner flow gets parameters from outer flow
3. **Hierarchical Processing**: Process data in a tree-like structure
================================================
FILE: cookbook/pocketflow-nested-batch/flow.py
================================================
import os
from pocketflow import Flow, BatchFlow
from nodes import LoadGrades, CalculateAverage
def create_base_flow():
"""Create base flow for processing one student's grades."""
# Create nodes
load = LoadGrades()
calc = CalculateAverage()
# Connect nodes
load - "calculate" >> calc
# Create and return flow
return Flow(start=load)
class ClassBatchFlow(BatchFlow):
"""BatchFlow for processing all students in a class."""
def prep(self, shared):
"""Generate parameters for each student in the class."""
# Get class folder from parameters
class_folder = self.params["class"]
# List all student files
class_path = os.path.join("school", class_folder)
students = [f for f in os.listdir(class_path) if f.endswith(".txt")]
# Return parameters for each student
return [{"student": student} for student in students]
def post(self, shared, prep_res, exec_res):
"""Calculate and print class average."""
class_name = self.params["class"]
class_results = shared["results"][class_name]
class_average = sum(class_results.values()) / len(class_results)
print(f"Class {class_name.split('_')[1].upper()} Average: {class_average:.2f}\n")
return "default"
class SchoolBatchFlow(BatchFlow):
"""BatchFlow for processing all classes in the school."""
def prep(self, shared):
"""Generate parameters for each class."""
# List all class folders
classes = [d for d in os.listdir("school") if os.path.isdir(os.path.join("school", d))]
# Return parameters for each class
return [{"class": class_name} for class_name in classes]
def post(self, shared, prep_res, exec_res):
"""Calculate and print school average."""
all_grades = []
for class_results in shared["results"].values():
all_grades.extend(class_results.values())
school_average = sum(all_grades) / len(all_grades)
print(f"School Average: {school_average:.2f}")
return "default"
def create_flow():
"""Create the complete nested batch processing flow."""
# Create base flow for single student
base_flow = create_base_flow()
# Wrap in ClassBatchFlow for processing all students in a class
class_flow = ClassBatchFlow(start=base_flow)
# Wrap in SchoolBatchFlow for processing all classes
school_flow = SchoolBatchFlow(start=class_flow)
return school_flow
================================================
FILE: cookbook/pocketflow-nested-batch/main.py
================================================
import os
from flow import create_flow
def create_sample_data():
"""Create sample grade files."""
# Create directory structure
os.makedirs("school/class_a", exist_ok=True)
os.makedirs("school/class_b", exist_ok=True)
# Sample grades
data = {
"class_a": {
"student1.txt": [7.5, 8.0, 9.0],
"student2.txt": [8.5, 7.0, 9.5]
},
"class_b": {
"student3.txt": [6.5, 8.5, 7.0],
"student4.txt": [9.0, 9.5, 8.0]
}
}
# Create files
for class_name, students in data.items():
for student, grades in students.items():
file_path = os.path.join("school", class_name, student)
with open(file_path, 'w') as f:
for grade in grades:
f.write(f"{grade}\n")
def main():
"""Run the nested batch example."""
# Create sample data
create_sample_data()
print("Processing school grades...\n")
# Create and run flow
flow = create_flow()
flow.run({})
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-nested-batch/nodes.py
================================================
import os
from pocketflow import Node
class LoadGrades(Node):
"""Node that loads grades from a student's file."""
def prep(self, shared):
"""Get file path from parameters."""
class_name = self.params["class"]
student_file = self.params["student"]
return os.path.join("school", class_name, student_file)
def exec(self, file_path):
"""Load and parse grades from file."""
with open(file_path, 'r') as f:
# Each line is a grade
grades = [float(line.strip()) for line in f]
return grades
def post(self, shared, prep_res, grades):
"""Store grades in shared store."""
shared["grades"] = grades
return "calculate"
class CalculateAverage(Node):
"""Node that calculates average grade."""
def prep(self, shared):
"""Get grades from shared store."""
return shared["grades"]
def exec(self, grades):
"""Calculate average."""
return sum(grades) / len(grades)
def post(self, shared, prep_res, average):
"""Store and print result."""
# Store in results dictionary
if "results" not in shared:
shared["results"] = {}
class_name = self.params["class"]
student = self.params["student"]
if class_name not in shared["results"]:
shared["results"][class_name] = {}
shared["results"][class_name][student] = average
# Print individual result
print(f"- {student}: Average = {average:.1f}")
return "default"
================================================
FILE: cookbook/pocketflow-nested-batch/requirements.txt
================================================
pocketflow
================================================
FILE: cookbook/pocketflow-nested-batch/school/class_a/student1.txt
================================================
7.5
8.0
9.0
================================================
FILE: cookbook/pocketflow-nested-batch/school/class_a/student2.txt
================================================
8.5
7.0
9.5
================================================
FILE: cookbook/pocketflow-nested-batch/school/class_b/student3.txt
================================================
6.5
8.5
7.0
================================================
FILE: cookbook/pocketflow-nested-batch/school/class_b/student4.txt
================================================
9.0
9.5
8.0
================================================
FILE: cookbook/pocketflow-node/README.md
================================================
# PocketFlow Summarize
A practical example demonstrating how to use PocketFlow to build a robust text summarization tool with error handling and retries. This example showcases core PocketFlow concepts in a real-world application.
## Features
- Text summarization using LLMs (Large Language Models)
- Automatic retry mechanism (up to 3 attempts) on API failures
- Graceful error handling with fallback responses
- Clean separation of concerns using PocketFlow's Node architecture
## Project Structure
```
.
├── docs/ # Documentation files
├── utils/ # Utility functions (LLM API wrapper)
├── flow.py # PocketFlow implementation with Summarize Node
├── main.py # Main application entry point
└── README.md # Project documentation
```
## Implementation Details
The example implements a simple but robust text summarization workflow:
1. **Summarize Node** (`flow.py`):
- `prep()`: Retrieves text from the shared store
- `exec()`: Calls LLM to summarize text in 10 words
- `exec_fallback()`: Provides graceful error handling
- `post()`: Stores the summary back in shared store
2. **Flow Structure**:
- Single node flow for demonstration
- Configured with 3 retries for reliability
- Uses shared store for data passing
## Setup
1. Create a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Configure your environment:
- Set up your LLM API key (check utils/call_llm.py for configuration)
4. Run the example:
```bash
python main.py
```
## Example Usage
The example comes with a sample text about PocketFlow, but you can modify `main.py` to summarize your own text:
```python
shared = {"data": "Your text to summarize here..."}
flow.run(shared)
print("Summary:", shared["summary"])
```
## What You'll Learn
This example demonstrates several key PocketFlow concepts:
- **Node Architecture**: How to structure LLM tasks using prep/exec/post pattern
- **Error Handling**: Implementing retry mechanisms and fallbacks
- **Shared Store**: Using shared storage for data flow between steps
- **Flow Creation**: Setting up a basic PocketFlow workflow
## Additional Resources
- [PocketFlow Documentation](https://the-pocket.github.io/PocketFlow/)
- [Node Concept Guide](https://the-pocket.github.io/PocketFlow/node.html)
- [Flow Design Patterns](https://the-pocket.github.io/PocketFlow/flow.html)
================================================
FILE: cookbook/pocketflow-node/flow.py
================================================
from pocketflow import Node, Flow
from utils.call_llm import call_llm
class Summarize(Node):
def prep(self, shared):
"""Read and preprocess data from shared store."""
return shared["data"]
def exec(self, prep_res):
"""Execute the summarization using LLM."""
if not prep_res:
return "Empty text"
prompt = f"Summarize this text in 10 words: {prep_res}"
summary = call_llm(prompt) # might fail
return summary
def exec_fallback(self, shared, prep_res, exc):
"""Provide a simple fallback instead of crashing."""
return "There was an error processing your request."
def post(self, shared, prep_res, exec_res):
"""Store the summary in shared store."""
shared["summary"] = exec_res
# Return "default" by not returning
# Create the flow
summarize_node = Summarize(max_retries=3)
flow = Flow(start=summarize_node)
================================================
FILE: cookbook/pocketflow-node/main.py
================================================
from flow import flow
def main():
# Example text to summarize
text = """
PocketFlow is a minimalist LLM framework that models workflows as a Nested Directed Graph.
Nodes handle simple LLM tasks, connecting through Actions for Agents.
Flows orchestrate these nodes for Task Decomposition, and can be nested.
It also supports Batch processing and Async execution.
"""
# Initialize shared store
shared = {"data": text}
# Run the flow
flow.run(shared)
# Print result
print("\nInput text:", text)
print("\nSummary:", shared["summary"])
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-node/requirements.txt
================================================
pocketflow
openai>=1.0.0
================================================
FILE: cookbook/pocketflow-node/utils/call_llm.py
================================================
from openai import OpenAI
def call_llm(prompt):
client = OpenAI(api_key="YOUR_API_KEY_HERE")
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
if __name__ == "__main__":
prompt = "What is the meaning of life?"
print(call_llm(prompt))
================================================
FILE: cookbook/pocketflow-parallel-batch/README.md
================================================
# Parallel Batch Translation Process
This project demonstrates using PocketFlow's async and parallel features (`AsyncFlow`, `AsyncParallelBatchNode`) to translate a document into multiple languages concurrently.
- Check out the [Substack Post Tutorial](https://pocketflow.substack.com/p/parallel-llm-calls-from-scratch-tutorial) for more!
## Goal
Translate `../../README.md` into multiple languages (Chinese, Spanish, etc.) in parallel, saving each to a file in the `translations/` directory. The main goal is to compare execution time against a sequential process.
## Getting Started
1. Install requirements:
```bash
pip install -r requirements.txt
```
2. Set API Key:
Set the environment variable for your Anthropic API key.
```bash
export ANTHROPIC_API_KEY="your-api-key-here"
```
*(Replace `"your-api-key-here"` with your actual key)*
*(Alternatively, place `ANTHROPIC_API_KEY=your-api-key-here` in a `.env` file)*
3. Verify API Key (Optional):
Run a quick check using the utility script.
```bash
python utils.py
```
*(Note: This requires a valid API key to be set.)*
4. Run the translation process:
```bash
python main.py
```
## How It Works
The implementation uses an `AsyncParallelBatchNode` that processes translation requests concurrently. The `TranslateTextNodeParallel`:
1. Prepares batches, pairing the source text with each target language.
2. Executes translation calls to the LLM for all languages concurrently using `async` operations.
3. Saves the translated content to individual files (`translations/README_LANGUAGE.md`).
This approach leverages `asyncio` and parallel execution to speed up I/O-bound tasks like multiple API calls.
## Example Output & Comparison
Running this parallel version significantly reduces the total time compared to a sequential approach:
```
# --- Sequential Run Output (from pocketflow-batch) ---
Starting sequential translation into 8 languages...
Translated Chinese text
...
Translated Korean text
Saved translation to translations/README_CHINESE.md
...
Saved translation to translations/README_KOREAN.md
Total sequential translation time: ~1136 seconds
=== Translation Complete ===
Translations saved to: translations
============================
# --- Parallel Run Output (this example) ---
Starting parallel translation into 8 languages...
Translated French text
Translated Portuguese text
... # Messages may appear interleaved
Translated Spanish text
Saved translation to translations/README_CHINESE.md
...
Saved translation to translations/README_KOREAN.md
Total parallel translation time: ~209 seconds
=== Translation Complete ===
Translations saved to: translations
============================
```
*(Actual times will vary based on API response speed and system.)*
## Files
- [`main.py`](./main.py): Implements the parallel batch translation node and flow.
- [`utils.py`](./utils.py): Async wrapper for calling the Anthropic model.
- [`requirements.txt`](./requirements.txt): Project dependencies (includes `aiofiles`).
- [`translations/`](./translations/): Output directory (created automatically).
================================================
FILE: cookbook/pocketflow-parallel-batch/main.py
================================================
import asyncio
import time
import os
from pocketflow import AsyncFlow, AsyncParallelBatchNode
from utils import call_llm
# --- Node Definitions ---
class TranslateTextNodeParallel(AsyncParallelBatchNode):
"""Translates README into multiple languages in parallel and saves files."""
async def prep_async(self, shared):
"""Reads text and target languages from shared store."""
text = shared.get("text", "(No text provided)")
languages = shared.get("languages", [])
return [(text, lang) for lang in languages]
async def exec_async(self, data_tuple):
"""Calls the async LLM utility for each target language."""
text, language = data_tuple
prompt = f"""
Please translate the following markdown file into {language}.
But keep the original markdown format, links and code blocks.
Directly return the translated text, without any other text or comments.
Original:
{text}
Translated:"""
result = await call_llm(prompt)
print(f"Translated {language} text")
return {"language": language, "translation": result}
async def post_async(self, shared, prep_res, exec_res_list):
"""Stores the dictionary of {language: translation} pairs and writes to files."""
output_dir = shared.get("output_dir", "translations")
os.makedirs(output_dir, exist_ok=True)
for result in exec_res_list:
if isinstance(result, dict):
language = result.get("language", "unknown")
translation = result.get("translation", "")
filename = os.path.join(output_dir, f"README_{language.upper()}.md")
try:
import aiofiles
async with aiofiles.open(filename, "w", encoding="utf-8") as f:
await f.write(translation)
print(f"Saved translation to {filename}")
except ImportError:
with open(filename, "w", encoding="utf-8") as f:
f.write(translation)
print(f"Saved translation to {filename} (sync fallback)")
except Exception as e:
print(f"Error writing file {filename}: {e}")
else:
print(f"Warning: Skipping invalid result item: {result}")
return "default"
# --- Flow Creation ---
def create_parallel_translation_flow():
"""Creates and returns the parallel translation flow."""
translate_node = TranslateTextNodeParallel(max_retries=3)
return AsyncFlow(start=translate_node)
# --- Main Execution ---
async def main():
source_readme_path = "../../README.md"
try:
with open(source_readme_path, "r", encoding='utf-8') as f:
text = f.read()
except FileNotFoundError:
print(f"Error: Could not find the source README file at {source_readme_path}")
exit(1)
except Exception as e:
print(f"Error reading file {source_readme_path}: {e}")
exit(1)
shared = {
"text": text,
"languages": ["Chinese", "Spanish", "Japanese", "German", "Russian", "Portuguese", "French", "Korean"],
"output_dir": "translations"
}
translation_flow = create_parallel_translation_flow()
print(f"Starting parallel translation into {len(shared['languages'])} languages...")
start_time = time.perf_counter()
await translation_flow.run_async(shared)
end_time = time.perf_counter()
duration = end_time - start_time
print(f"\nTotal parallel translation time: {duration:.4f} seconds")
print("\n=== Translation Complete ===")
print(f"Translations saved to: {shared['output_dir']}")
print("============================")
if __name__ == "__main__":
asyncio.run(main())
================================================
FILE: cookbook/pocketflow-parallel-batch/requirements.txt
================================================
pocketflow>=0.0.2
anthropic>=0.15.0
python-dotenv
httpx
aiofiles
================================================
FILE: cookbook/pocketflow-parallel-batch/translations/README_CHINESE.md
================================================
[English](https://github.com/The-Pocket/PocketFlow/blob/main/README.md) | [中文](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_CHINESE.md) | [Español](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_SPANISH.md) | [日本語](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_JAPANESE.md) | [Deutsch](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_GERMAN.md) | [Русский](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_RUSSIAN.md) | [Português](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_PORTUGUESE.md) | Français | [한국어](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_KOREAN.md)

[](https://the-pocket.github.io/PocketFlow/)
Pocket Flow est un framework LLM minimaliste en [100 lignes](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)
- **Léger** : Seulement 100 lignes. Zéro superflu, zéro dépendance, zéro verrouillage fournisseur.
- **Expressif** : Tout ce que vous aimez — ([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agents](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), et plus encore.
- **[Programmation Agentique](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)** : Laissez les Agents IA (par exemple, Cursor AI) créer des Agents — augmentez votre productivité par 10 !
Commencer avec Pocket Flow :
- Pour installer, ```pip install pocketflow``` ou copiez simplement le [code source](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) (seulement 100 lignes).
- Pour en savoir plus, consultez la [documentation](https://the-pocket.github.io/PocketFlow/). Pour comprendre la motivation, lisez l'[histoire](https://zacharyhuang.substack.com/p/i-built-an-llm-framework-in-just).
- Des questions ? Consultez cet [Assistant IA](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant), ou [créez une issue !](https://github.com/The-Pocket/PocketFlow/issues/new)
- 🎉 Rejoignez notre [Discord](https://discord.gg/hUHHE9Sa6T) pour vous connecter avec d'autres développeurs utilisant Pocket Flow !
- 🎉 Pocket Flow est initialement en Python, mais nous avons maintenant des versions en [Typescript](https://github.com/The-Pocket/PocketFlow-Typescript), [Java](https://github.com/The-Pocket/PocketFlow-Java), [C++](https://github.com/The-Pocket/PocketFlow-CPP) et [Go](https://github.com/The-Pocket/PocketFlow-Go) !
## Pourquoi Pocket Flow ?
Les frameworks LLM actuels sont surchargés... Vous n'avez besoin que de 100 lignes pour un framework LLM !
## Comment fonctionne Pocket Flow ?
Les [100 lignes](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) capturent l'abstraction fondamentale des frameworks LLM : le Graph !
De là, il est facile d'implémenter des modèles de conception populaires comme ([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agents](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), etc.
✨ Voici des tutoriels de base :
| Nom | Difficulté | Description |
| :-------------: | :-------------: | :--------------------- |
| [Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *Débutant* | Un chatbot basique avec historique de conversation |
| [Sortie Structurée](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *Débutant* | Extraction de données structurées à partir de CV par prompt |
| [Workflow](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *Débutant* | Un workflow d'écriture qui planifie, rédige du contenu et applique un style |
| [Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *Débutant* | Un agent de recherche qui peut chercher sur le web et répondre aux questions |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *Débutant* | Un processus simple de génération augmentée par récupération |
| [Batch](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *Débutant* | Un processeur par lots qui traduit du contenu markdown en plusieurs langues |
| [Streaming](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *Débutant* | Une démo de streaming LLM en temps réel avec capacité d'interruption utilisateur |
| [Garde-fou de Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Débutant* | Un chatbot conseiller de voyage qui ne traite que les requêtes liées au voyage |
| [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ★☆☆ *Intermédiaire* | Un processeur de qualification de CV utilisant le modèle map-reduce pour l'évaluation par lots |
| [Multi-Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Intermédiaire* | Un jeu de Tabou pour la communication asynchrone entre deux agents |
| [Superviseur](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *Intermédiaire* | L'agent de recherche devient peu fiable... Construisons un processus de supervision |
| [Parallèle](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *Intermédiaire* | Une démo d'exécution parallèle montrant une accélération de 3x |
| [Flux Parallèle](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *Intermédiaire* | Une démo de traitement d'image parallèle montrant une accélération de 8x avec plusieurs filtres |
| [Vote Majoritaire](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ★☆☆ *Intermédiaire* | Améliorer la précision du raisonnement en agrégeant plusieurs tentatives de solution |
| [Réflexion](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *Intermédiaire* | Résoudre des problèmes de raisonnement complexes grâce à la Chaîne de Pensée |
| [Mémoire](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *Intermédiaire* | Un chatbot avec mémoire à court et long terme |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *Intermédiaire* | Convertir le langage naturel en requêtes SQL avec une boucle d'auto-débogage |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *Intermédiaire* | Agent utilisant le Protocole de Contexte de Modèle pour les opérations numériques |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *Intermédiaire* | Agent encapsulé avec le protocole Agent-to-Agent pour la communication inter-agent |
| [Web HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-web-hitl) | ★☆☆ *Intermédiaire* | Un service web minimal pour une boucle de révision humaine avec mises à jour SSE |
👀 Vous voulez voir d'autres tutoriels pour débutants ? [Créez une issue !](https://github.com/The-Pocket/PocketFlow/issues/new)
## Comment utiliser Pocket Flow ?
🚀 Par la **Programmation Agentique** — le paradigme de développement d'applications LLM le plus rapide — où *les humains conçoivent* et *les agents programment* !
✨ Voici des exemples d'applications LLM plus complexes :
| Nom de l'application | Difficulté | Sujets | Conception Humaine | Code Agent |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Construire Cursor avec Cursor](https://github.com/The-Pocket/Tutorial-Cursor) Nous atteindrons bientôt la singularité ... | ★★★ *Avancé* | [Agent](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [Document de conception](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [Code Flow](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [Constructeur de Connaissances de Base de Code](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) La vie est trop courte pour rester perplexe devant le code des autres | ★★☆ *Moyen* | [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [Document de conception](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [Code Flow](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [Interroger l'IA Paul Graham](https://github.com/The-Pocket/Tutorial-YC-Partner) Interrogez l'IA Paul Graham, au cas où vous ne seriez pas accepté | ★★☆ *Moyen* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [Document de conception](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [Code Flow](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [Résumeur Youtube](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) Vous explique les vidéos YouTube comme si vous aviez 5 ans | ★☆☆ *Intermédiaire* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [Document de conception](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [Code Flow](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [Générateur d'Accroche pour Email](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) Des brise-glaces instantanés qui transforment les prospects froids en prospects chauds | ★☆☆ *Intermédiaire* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [Recherche Web](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [Document de conception](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Code Flow](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- Vous voulez apprendre la **Programmation Agentique** ?
- Consultez [ma chaîne YouTube](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1) pour des tutoriels vidéo sur la façon dont certaines applications ci-dessus sont créées !
- Vous voulez créer votre propre application LLM ? Lisez cet [article](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to) ! Commencez avec [ce modèle](https://github.com/The-Pocket/PocketFlow-Template-Python) !
================================================
FILE: cookbook/pocketflow-parallel-batch/translations/README_GERMAN.md
================================================
[English](https://github.com/The-Pocket/PocketFlow/blob/main/README.md) | [中文](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_CHINESE.md) | [Español](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_SPANISH.md) | [日本語](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_JAPANESE.md) | Deutsch | [Русский](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_RUSSIAN.md) | [Português](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_PORTUGUESE.md) | [Français](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_FRENCH.md) | [한국어](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_KOREAN.md)

[](https://the-pocket.github.io/PocketFlow/)
Pocket Flow ist ein [100-zeiliges](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) minimalistisches LLM-Framework
- **Leichtgewichtig**: Nur 100 Zeilen. Kein Ballast, keine Abhängigkeiten, keine Anbieterbindung.
- **Ausdrucksstark**: Alles, was Sie lieben—([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agenten](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), und mehr.
- **[Agenten-basiertes Programmieren](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)**: Lassen Sie KI-Agenten (z.B. Cursor AI) Agenten bauen—10-fache Produktivitätssteigerung!
Erste Schritte mit Pocket Flow:
- Zur Installation, ```pip install pocketflow```oder kopieren Sie einfach den [Quellcode](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) (nur 100 Zeilen).
- Um mehr zu erfahren, schauen Sie in die [Dokumentation](https://the-pocket.github.io/PocketFlow/). Um die Motivation zu verstehen, lesen Sie die [Geschichte](https://zacharyhuang.substack.com/p/i-built-an-llm-framework-in-just).
- Haben Sie Fragen? Schauen Sie sich diesen [KI-Assistenten](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant) an, oder [erstellen Sie ein Issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
- 🎉 Treten Sie unserem [Discord](https://discord.gg/hUHHE9Sa6T) bei, um sich mit anderen Entwicklern zu vernetzen, die mit Pocket Flow arbeiten!
- 🎉 Pocket Flow ist ursprünglich in Python geschrieben, aber wir haben jetzt auch Versionen für [Typescript](https://github.com/The-Pocket/PocketFlow-Typescript), [Java](https://github.com/The-Pocket/PocketFlow-Java), [C++](https://github.com/The-Pocket/PocketFlow-CPP) und [Go](https://github.com/The-Pocket/PocketFlow-Go)!
## Warum Pocket Flow?
Aktuelle LLM-Frameworks sind aufgebläht... Sie brauchen nur 100 Zeilen für ein LLM-Framework!
## Wie funktioniert Pocket Flow?
Die [100 Zeilen](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) erfassen die Kernabstraktion von LLM-Frameworks: Graph!
Von dort aus ist es einfach, beliebte Designmuster wie ([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agenten](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), etc. zu implementieren.
✨ Hier sind grundlegende Tutorials:
| Name | Schwierigkeit | Beschreibung |
| :-------------: | :-------------: | :--------------------- |
| [Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *Anfänger* | Ein einfacher Chatbot mit Gesprächsverlauf |
| [Strukturierte Ausgabe](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *Anfänger* | Extraktion strukturierter Daten aus Lebensläufen durch Prompting |
| [Workflow](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *Anfänger* | Ein Schreib-Workflow, der gliedert, Inhalte schreibt und Formatierungen anwendet |
| [Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *Anfänger* | Ein Recherche-Agent, der im Web suchen und Fragen beantworten kann |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *Anfänger* | Ein einfacher Abrufsaugmentierter Generierungsprozess |
| [Batch](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *Anfänger* | Ein Batch-Prozessor, der Markdown-Inhalte in mehrere Sprachen übersetzt |
| [Streaming](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *Anfänger* | Eine Echtzeit-LLM-Streaming-Demo mit Benutzer-Unterbrechungsfunktion |
| [Chat-Leitplanke](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Anfänger* | Ein Reiseberater-Chatbot, der nur reisebezogene Anfragen verarbeitet |
| [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ★☆☆ *Einsteiger* | Ein Lebenslauf-Qualifikationsprozessor, der das Map-Reduce-Muster für Batch-Auswertungen verwendet |
| [Multi-Agent](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Einsteiger* | Ein Tabu-Wortspiel für asynchrone Kommunikation zwischen zwei Agenten |
| [Supervisor](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *Einsteiger* | Forschungsagent wird unzuverlässig... Bauen wir einen Überwachungsprozess auf|
| [Parallel](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *Einsteiger* | Eine parallele Ausführungsdemo, die 3-fache Beschleunigung zeigt |
| [Paralleler Flow](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *Einsteiger* | Eine parallele Bildverarbeitungsdemo, die 8-fache Beschleunigung mit mehreren Filtern zeigt |
| [Mehrheitswahl](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ★☆☆ *Einsteiger* | Verbesserte Schlussfolgerungsgenauigkeit durch Aggregation mehrerer Lösungsversuche |
| [Denken](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *Einsteiger* | Lösen komplexer Schlussfolgerungsprobleme durch Chain-of-Thought |
| [Gedächtnis](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *Einsteiger* | Ein Chatbot mit Kurz- und Langzeitgedächtnis |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *Einsteiger* | Konvertierung natürlicher Sprache in SQL-Abfragen mit Auto-Debug-Schleife |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *Einsteiger* | Agent mit Model Context Protocol für numerische Operationen |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *Einsteiger* | Agent mit Agent-to-Agent-Protokoll für Inter-Agenten-Kommunikation |
| [Web HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-web-hitl) | ★☆☆ *Einsteiger* | Ein minimaler Webdienst für eine menschliche Überprüfungsschleife mit SSE-Updates |
👀 Möchten Sie andere Tutorials für Anfänger sehen? [Erstellen Sie ein Issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
## Wie verwendet man Pocket Flow?
🚀 Durch **Agenten-basiertes Programmieren**—das schnellste LLM-App-Entwicklungsparadigma, bei dem *Menschen entwerfen* und *Agenten programmieren*!
✨ Hier sind Beispiele für komplexere LLM-Apps:
| App-Name | Schwierigkeit | Themen | Menschlicher Entwurf | Agent-Code |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Cursor mit Cursor bauen](https://github.com/The-Pocket/Tutorial-Cursor) Wir werden bald die Singularität erreichen ... | ★★★ *Fortgeschritten* | [Agent](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [Design-Dokument](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [Flow-Code](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [Codebase-Wissensgenerator](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) Das Leben ist zu kurz, um ratlos fremden Code anzustarren | ★★☆ *Mittel* | [Workflow](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [Design-Dokument](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [Flow-Code](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [Frage KI Paul Graham](https://github.com/The-Pocket/Tutorial-YC-Partner) Frage KI Paul Graham, falls du nicht reinkommst | ★★☆ *Mittel* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [Design-Dokument](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [Flow-Code](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [Youtube-Zusammenfasser](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) Erklärt YouTube-Videos so, als wärst du 5 | ★☆☆ *Einsteiger* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [Design-Dokument](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [Flow-Code](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [Cold-Opener-Generator](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) Sofortige Eisbrecher, die kalte Leads heiß machen | ★☆☆ *Einsteiger* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [Web-Suche](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [Design-Dokument](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Flow-Code](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- Möchten Sie **Agenten-basiertes Programmieren** lernen?
- Schauen Sie sich [meinen YouTube-Kanal](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1) für Video-Tutorials an, wie einige der oben genannten Apps erstellt wurden!
- Möchten Sie Ihre eigene LLM-App erstellen? Lesen Sie diesen [Beitrag](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)! Beginnen Sie mit [dieser Vorlage](https://github.com/The-Pocket/PocketFlow-Template-Python)!
================================================
FILE: cookbook/pocketflow-parallel-batch/translations/README_JAPANESE.md
================================================
## Pocket Flow는 어떻게 작동하나요?
[100줄](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)의 코드는 LLM 프레임워크의 핵심 추상화인 그래프를 구현합니다!
이를 기반으로 ([멀티-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[에이전트](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [워크플로우](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) 등의 인기 있는 디자인 패턴을 쉽게 구현할 수 있습니다.
✨ 아래는 기본 튜토리얼입니다:
| 이름 | 난이도 | 설명 |
| :-------------: | :-------------: | :--------------------- |
| [채팅](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *초보* | 대화 기록을 가진 기본 채팅봇 |
| [구조화된 출력](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *초보* | 프롬프트를 통해 이력서에서 구조화된 데이터 추출 |
| [워크플로우](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *초보* | 개요 작성, 내용 작성, 스타일 적용이 포함된 작성 워크플로우 |
| [에이전트](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *초보* | 웹을 검색하고 질문에 답할 수 있는 연구 에이전트 |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *초보* | 간단한 검색 증강 생성 프로세스 |
| [배치](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *초보* | 마크다운 콘텐츠를 여러 언어로 번역하는 배치 프로세서 |
| [스트리밍](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *초보* | 사용자 중단 기능이 있는 실시간 LLM 스트리밍 데모 |
| [채팅 가드레일](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *초보* | 여행 관련 쿼리만 처리하는 여행 상담 채팅봇 |
| [맵-리듀스](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ★☆☆ *초급* | 배치 평가를 위한 맵-리듀스 패턴을 사용하는 이력서 자격 처리기 |
| [멀티-에이전트](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *초급* | 두 에이전트 간의 비동기 통신을 위한 금지어 게임 |
| [감독자](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *초급* | 연구 에이전트가 불안정할 때... 감독 프로세스를 구축해 봅시다 |
| [병렬](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *초급* | 3배 속도 향상을 보여주는 병렬 실행 데모 |
| [병렬 플로우](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *초급* | 여러 필터를 사용한 8배 속도 향상을 보여주는 병렬 이미지 처리 데모 |
| [다수결 투표](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ★☆☆ *초급* | 여러 솔루션 시도를 집계하여 추론 정확도 향상 |
| [사고](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *초급* | Chain-of-Thought를 통한 복잡한 추론 문제 해결 |
| [메모리](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *초급* | 단기 및 장기 메모리가 있는 채팅봇 |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *초급* | 자동 디버그 루프가 있는 자연어에서 SQL 쿼리로 변환 |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *초급* | 수치 연산을 위한 모델 컨텍스트 프로토콜을 사용하는 에이전트 |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *초급* | 에이전트 간 통신을 위한 Agent-to-Agent 프로토콜로 래핑된 에이전트 |
| [웹 HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-web-hitl) | ★☆☆ *초급* | SSE 업데이트가 있는 인간 검토 루프를 위한 최소한의 웹 서비스 |
👀 더 많은 초보자용 튜토리얼을 보고 싶으신가요? [이슈를 생성하세요!](https://github.com/The-Pocket/PocketFlow/issues/new)
## Pocket Flow를 어떻게 사용하나요?
🚀 **에이전트 코딩**을 통해—가장 빠른 LLM 앱 개발 패러다임으로, *인간이 설계*하고 *에이전트가 코딩*합니다!
✨ 아래는 더 복잡한 LLM 앱의 예시입니다:
| 앱 이름 | 난이도 | 주제 | 인간 설계 | 에이전트 코드 |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Cursor로 Cursor 만들기](https://github.com/The-Pocket/Tutorial-Cursor) 곧 기술적 특이점에 도달할 것입니다... | ★★★ *고급* | [에이전트](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [설계 문서](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [플로우 코드](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [코드베이스 지식 빌더](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) 인생은 다른 사람의 코드를 혼란스럽게 바라볼 만큼 길지 않습니다 | ★★☆ *중급* | [워크플로우](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [설계 문서](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [플로우 코드](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [AI Paul Graham에게 물어보기](https://github.com/The-Pocket/Tutorial-YC-Partner) 합격하지 못한 경우를 대비해 AI Paul Graham에게 물어보세요 | ★★☆ *중급* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [맵 리듀스](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [설계 문서](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [플로우 코드](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [유튜브 요약기](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) 5살 아이에게 설명하듯 YouTube 동영상 설명 | ★☆☆ *초급* | [맵 리듀스](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [설계 문서](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [플로우 코드](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [콜드 오프너 생성기](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) 차가운 잠재 고객을 뜨겁게 만드는 즉각적인 아이스브레이커 | ★☆☆ *초급* | [맵 리듀스](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [웹 검색](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [설계 문서](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [플로우 코드](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- **에이전트 코딩**을 배우고 싶으신가요?
- 위에 소개된 앱들이 어떻게 만들어졌는지 비디오 튜토리얼을 보려면 [제 YouTube](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1)를 확인하세요!
- 자신만의 LLM 앱을 만들고 싶으신가요? 이 [포스트](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)를 읽어보세요! [이 템플릿](https://github.com/The-Pocket/PocketFlow-Template-Python)으로 시작하세요!
================================================
FILE: cookbook/pocketflow-parallel-batch/translations/README_PORTUGUESE.md
================================================
[English](https://github.com/The-Pocket/PocketFlow/blob/main/README.md) | [中文](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_CHINESE.md) | [Español](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_SPANISH.md) | [日本語](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_JAPANESE.md) | [Deutsch](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_GERMAN.md) | [Русский](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_RUSSIAN.md) | Português | [Français](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_FRENCH.md) | [한국어](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_KOREAN.md)

[](https://the-pocket.github.io/PocketFlow/)
Pocket Flow é um framework minimalista para LLM com [apenas 100 linhas](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)
- **Leve**: Apenas 100 linhas. Zero inchaço, zero dependências, zero aprisionamento a fornecedores.
- **Expressivo**: Tudo o que você adora—([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agentes](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Fluxo de Trabalho](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), e mais.
- **[Codificação Agêntica](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)**: Deixe que Agentes de IA (ex: Cursor AI) construam Agentes—aumento de produtividade de 10x!
Comece com o Pocket Flow:
- Para instalar, ```pip install pocketflow``` ou apenas copie o [código-fonte](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) (apenas 100 linhas).
- Para saber mais, consulte a [documentação](https://the-pocket.github.io/PocketFlow/). Para entender a motivação, leia a [história](https://zacharyhuang.substack.com/p/i-built-an-llm-framework-in-just).
- Tem perguntas? Consulte este [Assistente de IA](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant), ou [crie uma issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
- 🎉 Junte-se ao nosso [Discord](https://discord.gg/hUHHE9Sa6T) para se conectar com outros desenvolvedores construindo com o Pocket Flow!
- 🎉 O Pocket Flow é inicialmente em Python, mas agora temos versões em [Typescript](https://github.com/The-Pocket/PocketFlow-Typescript), [Java](https://github.com/The-Pocket/PocketFlow-Java), [C++](https://github.com/The-Pocket/PocketFlow-CPP) e [Go](https://github.com/The-Pocket/PocketFlow-Go)!
## Por que Pocket Flow?
Os frameworks LLM atuais são pesados... Você só precisa de 100 linhas para um Framework LLM!
## Como funciona o Pocket Flow?
As [100 linhas](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) capturam a abstração central dos frameworks LLM: o Grafo!
A partir daí, é fácil implementar padrões de design populares como ([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agentes](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Fluxo de Trabalho](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), etc.
✨ Abaixo estão tutoriais básicos:
| Nome | Dificuldade | Descrição |
| :-------------: | :-------------: | :--------------------- |
| [Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *Básico* | Um chatbot básico com histórico de conversação |
| [Saída Estruturada](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *Básico* | Extraindo dados estruturados de currículos por prompt |
| [Fluxo de Trabalho](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *Básico* | Um fluxo de escrita que esboça, escreve conteúdo e aplica estilo |
| [Agente](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *Básico* | Um agente de pesquisa que pode buscar na web e responder perguntas |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *Básico* | Um processo simples de Geração Aumentada por Recuperação |
| [Processamento em Lote](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *Básico* | Um processador em lote que traduz conteúdo markdown para vários idiomas |
| [Streaming](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *Básico* | Uma demonstração de streaming LLM em tempo real com capacidade de interrupção pelo usuário |
| [Guardrail de Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Básico* | Um chatbot de consultoria de viagens que processa apenas consultas relacionadas a viagens |
| [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ★☆☆ *Iniciante* | Um processador de qualificação de currículos usando o padrão map-reduce para avaliação em lote |
| [Multi-Agente](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Iniciante* | Um jogo de Tabu para comunicação assíncrona entre dois agentes |
| [Supervisor](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *Iniciante* | O agente de pesquisa está ficando pouco confiável... Vamos criar um processo de supervisão |
| [Paralelo](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *Iniciante* | Uma demonstração de execução paralela que mostra um aumento de velocidade de 3x |
| [Fluxo Paralelo](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *Iniciante* | Uma demonstração de processamento de imagem paralelo mostrando um aumento de velocidade de 8x com múltiplos filtros |
| [Voto Majoritário](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ★☆☆ *Iniciante* | Melhore a precisão de raciocínio agregando múltiplas tentativas de solução |
| [Pensamento](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *Iniciante* | Resolva problemas complexos de raciocínio através de Cadeia-de-Pensamento |
| [Memória](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *Iniciante* | Um chatbot com memória de curto e longo prazo |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *Iniciante* | Converta linguagem natural para consultas SQL com um loop de autodepuração |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *Iniciante* | Agente usando o Protocolo de Contexto de Modelo para operações numéricas |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *Iniciante* | Agente envolvido com o protocolo Agente-para-Agente para comunicação entre agentes |
| [Web HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-web-hitl) | ★☆☆ *Iniciante* | Um serviço web mínimo para um loop de revisão humana com atualizações SSE |
👀 Quer ver outros tutoriais para iniciantes? [Crie uma issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
## Como usar o Pocket Flow?
🚀 Através da **Codificação Agêntica**—o paradigma mais rápido de desenvolvimento de aplicativos LLM—onde *humanos projetam* e *agentes codificam*!
✨ Abaixo estão exemplos de aplicativos LLM mais complexos:
| Nome do Aplicativo | Dificuldade | Tópicos | Design Humano | Código do Agente |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Construir Cursor com Cursor](https://github.com/The-Pocket/Tutorial-Cursor) Logo chegaremos à singularidade ... | ★★★ *Avançado* | [Agente](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [Doc de Design](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [Código de Fluxo](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [Construtor de Conhecimento de Base de Código](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) A vida é curta demais para ficar olhando o código dos outros em confusão | ★★☆ *Médio* | [Fluxo de Trabalho](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [Doc de Design](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [Código de Fluxo](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [Pergunte à IA Paul Graham](https://github.com/The-Pocket/Tutorial-YC-Partner) Pergunte à IA Paul Graham, caso você não consiga entrar | ★★☆ *Médio* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [Doc de Design](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [Código de Fluxo](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [Resumidor de Youtube](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) Explica vídeos do YouTube para você como se você tivesse 5 anos | ★☆☆ *Iniciante* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [Doc de Design](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [Código de Fluxo](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [Gerador de Abertura a Frio](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) Quebra-gelos instantâneos que transformam leads frios em quentes | ★☆☆ *Iniciante* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [Busca Web](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [Doc de Design](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Código de Fluxo](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- Quer aprender **Codificação Agêntica**?
- Confira [meu YouTube](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1) para tutoriais em vídeo sobre como alguns dos aplicativos acima são feitos!
- Quer construir seu próprio aplicativo LLM? Leia este [post](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)! Comece com [este modelo](https://github.com/The-Pocket/PocketFlow-Template-Python)!
================================================
FILE: cookbook/pocketflow-parallel-batch/translations/README_RUSSIAN.md
================================================
[English](https://github.com/The-Pocket/PocketFlow/blob/main/README.md) | [中文](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_CHINESE.md) | [Español](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_SPANISH.md) | [日本語](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_JAPANESE.md) | [Deutsch](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_GERMAN.md) | Русский| [Português](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_PORTUGUESE.md) | [Français](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_FRENCH.md) | [한국어](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_KOREAN.md)

[](https://the-pocket.github.io/PocketFlow/)
Pocket Flow — это минималистичный фреймворк для LLM всего в [100 строк](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)
- **Легкий**: Всего 100 строк. Никакого лишнего веса, никаких зависимостей, никакой привязки к вендорам.
- **Выразительный**: Всё, что вы любите — ([Мульти-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Агенты](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Рабочие процессы](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) и многое другое.
- **[Агентское кодирование](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)**: Позвольте ИИ-агентам (например, Cursor AI) создавать других агентов — повысьте продуктивность в 10 раз!
Начало работы с Pocket Flow:
- Для установки, ```pip install pocketflow``` или просто скопируйте [исходный код](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) (всего 100 строк).
- Чтобы узнать больше, ознакомьтесь с [документацией](https://the-pocket.github.io/PocketFlow/). Чтобы понять мотивацию, прочитайте [историю](https://zacharyhuang.substack.com/p/i-built-an-llm-framework-in-just).
- Есть вопросы? Спросите этого [ИИ-ассистента](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant) или [создайте issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
- 🎉 Присоединяйтесь к нашему [Discord](https://discord.gg/hUHHE9Sa6T), чтобы общаться с другими разработчиками, использующими Pocket Flow!
- 🎉 Pocket Flow изначально написан на Python, но теперь у нас есть версии на [Typescript](https://github.com/The-Pocket/PocketFlow-Typescript), [Java](https://github.com/The-Pocket/PocketFlow-Java), [C++](https://github.com/The-Pocket/PocketFlow-CPP) и [Go](https://github.com/The-Pocket/PocketFlow-Go)!
## Почему Pocket Flow?
Современные фреймворки для LLM слишком громоздкие... Для фреймворка LLM достаточно всего 100 строк!
| | **Абстракция** | **Обертки для конкретных приложений** | **Обертки для конкретных вендоров** | **Строк** | **Размер** |
|----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:|
| LangChain | Agent, Chain | Много (напр., QA, Суммаризация) | Много (напр., OpenAI, Pinecone и т.д.) | 405K | +166MB |
| CrewAI | Agent, Chain | Много (напр., FileReadTool, SerperDevTool) | Много (напр., OpenAI, Anthropic, Pinecone и т.д.) | 18K | +173MB |
| SmolAgent | Agent | Несколько (напр., CodeAgent, VisitWebTool) | Несколько (напр., DuckDuckGo, Hugging Face и т.д.) | 8K | +198MB |
| LangGraph | Agent, Graph | Несколько (напр., Semantic Search) | Несколько (напр., PostgresStore, SqliteSaver и т.д.) | 37K | +51MB |
| AutoGen | Agent | Несколько (напр., Tool Agent, Chat Agent) | Много [Опционально] (напр., OpenAI, Pinecone и т.д.) | 7K (только ядро) | +26MB (только ядро) |
| **PocketFlow** | **Graph** | **Нет** | **Нет** | **100** | **+56KB** |
## Как работает Pocket Flow?
[100 строк](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) охватывают ключевую абстракцию фреймворков LLM: Граф!
Отсюда легко реализовать популярные шаблоны проектирования, такие как ([Мульти-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Агенты](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Рабочие процессы](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) и другие.
✨ Ниже приведены базовые руководства:
| Название | Сложность | Описание |
| :-------------: | :-------------: | :--------------------- |
| [Чат](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *Простейший* | Базовый чат-бот с историей разговора |
| [Структурированный вывод](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *Простейший* | Извлечение структурированных данных из резюме с помощью промптов |
| [Рабочий процесс](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *Простейший* | Процесс написания, который создает план, пишет контент и применяет стили |
| [Агент](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *Простейший* | Исследовательский агент, который может искать в интернете и отвечать на вопросы |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *Простейший* | Простой процесс генерации с извлечением информации |
| [Пакетная обработка](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *Простейший* | Пакетный процессор, который переводит markdown-контент на несколько языков |
| [Потоковая передача](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *Простейший* | Демонстрация потоковой передачи LLM в реальном времени с возможностью прерывания пользователем |
| [Ограничение чата](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Простейший* | Чат-бот туристического консультанта, обрабатывающий только запросы, связанные с путешествиями |
| [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ★☆☆ *Начальный* | Процессор квалификации резюме, использующий паттерн map-reduce для пакетной оценки |
| [Мульти-агент](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Начальный* | Игра Табу для асинхронного общения между двумя агентами |
| [Супервизор](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *Начальный* | Исследовательский агент становится ненадежным... Давайте создадим процесс надзора|
| [Параллельное выполнение](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *Начальный* | Демонстрация параллельного выполнения, показывающая 3-кратное ускорение |
| [Параллельный поток](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *Начальный* | Демонстрация параллельной обработки изображений, показывающая 8-кратное ускорение с несколькими фильтрами |
| [Голосование большинством](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ★☆☆ *Начальный* | Повышение точности рассуждений путем агрегации нескольких попыток решения |
| [Мышление](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *Начальный* | Решение сложных задач рассуждения с помощью цепочки размышлений |
| [Память](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *Начальный* | Чат-бот с кратковременной и долговременной памятью |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *Начальный* | Преобразование естественного языка в SQL-запросы с автоматическим циклом отладки |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *Начальный* | Агент, использующий протокол контекста модели для числовых операций |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *Начальный* | Агент, обернутый протоколом агент-к-агенту для межагентного взаимодействия |
| [Web HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-web-hitl) | ★☆☆ *Начальный* | Минимальный веб-сервис для цикла проверки человеком с обновлениями SSE |
👀 Хотите увидеть другие руководства для начинающих? [Создайте issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
## Как использовать Pocket Flow?
🚀 Через **Агентское кодирование** — самую быструю парадигму разработки LLM-приложений, где *люди проектируют*, а *агенты кодируют*!
✨ Ниже приведены примеры более сложных LLM-приложений:
| Название приложения | Сложность | Темы | Дизайн от человека | Код от агента |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Создание Cursor с помощью Cursor](https://github.com/The-Pocket/Tutorial-Cursor) Скоро достигнем сингулярности ... | ★★★ *Продвинутый* | [Агент](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [Документация по дизайну](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [Код потока](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [Конструктор знаний о кодовой базе](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) Жизнь слишком коротка, чтобы в растерянности смотреть на чужой код | ★★☆ *Средний* | [Рабочий процесс](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [Документация по дизайну](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [Код потока](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [Спроси ИИ Пола Грэма](https://github.com/The-Pocket/Tutorial-YC-Partner) Спроси ИИ Пола Грэма, если тебя не приняли | ★★☆ *Средний* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [Документация по дизайну](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [Код потока](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [Суммаризатор YouTube](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) Объясняет YouTube-видео как для 5-летнего | ★☆☆ *Начальный* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [Документация по дизайну](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [Код потока](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [Генератор холодных открытий](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) Мгновенные ледоколы, превращающие холодных лидов в горячих | ★☆☆ *Начальный* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [Веб-поиск](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [Документация по дизайну](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Код потока](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- Хотите изучить **Агентское кодирование**?
- Посмотрите [мой YouTube](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1) для видеоуроков о том, как создаются некоторые из вышеперечисленных приложений!
- Хотите создать свое собственное LLM-приложение? Прочитайте эту [статью](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)! Начните с [этого шаблона](https://github.com/The-Pocket/PocketFlow-Template-Python)!
================================================
FILE: cookbook/pocketflow-parallel-batch/translations/README_SPANISH.md
================================================
[English](https://github.com/The-Pocket/PocketFlow/blob/main/README.md) | [中文](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_CHINESE.md) | Español | [日本語](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_JAPANESE.md) | [Deutsch](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_GERMAN.md) | [Русский](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_RUSSIAN.md) | [Português](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_PORTUGUESE.md) | [Français](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_FRENCH.md) | [한국어](https://github.com/The-Pocket/PocketFlow/blob/main/cookbook/pocketflow-batch/translations/README_KOREAN.md)

[](https://the-pocket.github.io/PocketFlow/)
Pocket Flow es un framework minimalista de LLM de [100 líneas](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)
- **Ligero**: Solo 100 líneas. Cero hinchazón, cero dependencias, cero vinculación a proveedores.
- **Expresivo**: Todo lo que amas—([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agentes](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Flujo de Trabajo](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), y más.
- **[Programación mediante Agentes](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)**: Permite que los Agentes de IA (por ejemplo, Cursor AI) construyan Agentes—¡multiplicando la productividad por 10!
Comienza con Pocket Flow:
- Para instalar, ```pip install pocketflow``` o simplemente copia el [código fuente](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) (solo 100 líneas).
- Para aprender más, consulta la [documentación](https://the-pocket.github.io/PocketFlow/). Para conocer la motivación, lee la [historia](https://zacharyhuang.substack.com/p/i-built-an-llm-framework-in-just).
- ¿Tienes preguntas? Consulta este [Asistente de IA](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant), o [¡crea un issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
- 🎉 ¡Únete a nuestro [Discord](https://discord.gg/hUHHE9Sa6T) para conectar con otros desarrolladores construyendo con Pocket Flow!
- 🎉 Pocket Flow inicialmente está en Python, ¡pero ahora tenemos versiones en [Typescript](https://github.com/The-Pocket/PocketFlow-Typescript), [Java](https://github.com/The-Pocket/PocketFlow-Java), [C++](https://github.com/The-Pocket/PocketFlow-CPP) y [Go](https://github.com/The-Pocket/PocketFlow-Go)!
## ¿Por qué Pocket Flow?
Los frameworks actuales de LLM están sobrecargados... ¡Solo necesitas 100 líneas para un framework de LLM!
## ¿Cómo funciona Pocket Flow?
Las [100 líneas](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py) capturan la abstracción principal de los frameworks de LLM: ¡el Grafo!
A partir de ahí, es fácil implementar patrones de diseño populares como ([Multi-](https://the-pocket.github.io/PocketFlow/design_pattern/multi_agent.html))[Agentes](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html), [Flujo de Trabajo](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html), [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html), etc.
✨ A continuación se presentan tutoriales básicos:
| Nombre | Dificultad | Descripción |
| :-------------: | :-------------: | :--------------------- |
| [Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat) | ☆☆☆ *Principiante* | Un chatbot básico con historial de conversación |
| [Salida Estructurada](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-structured-output) | ☆☆☆ *Principiante* | Extracción de datos estructurados de currículums mediante prompts |
| [Flujo de Trabajo](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-workflow) | ☆☆☆ *Principiante* | Un flujo de escritura que esquematiza, escribe contenido y aplica estilo |
| [Agente](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-agent) | ☆☆☆ *Principiante* | Un agente de investigación que puede buscar en la web y responder preguntas |
| [RAG](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-rag) | ☆☆☆ *Principiante* | Un simple proceso de Generación aumentada por Recuperación |
| [Procesamiento por Lotes](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-batch) | ☆☆☆ *Principiante* | Un procesador por lotes que traduce contenido markdown a múltiples idiomas |
| [Streaming](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-llm-streaming) | ☆☆☆ *Principiante* | Una demostración de streaming LLM en tiempo real con capacidad de interrupción del usuario |
| [Protección de Chat](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-guardrail) | ☆☆☆ *Principiante* | Un chatbot asesor de viajes que solo procesa consultas relacionadas con viajes |
| [Map-Reduce](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-map-reduce) | ★☆☆ *Inicial* | Un procesador de calificación de currículums que utiliza el patrón map-reduce para evaluación por lotes |
| [Multi-Agente](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-multi-agent) | ★☆☆ *Inicial* | Un juego de palabras Tabú para comunicación asíncrona entre dos agentes |
| [Supervisor](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-supervisor) | ★☆☆ *Inicial* | El agente de investigación se vuelve poco fiable... Construyamos un proceso de supervisión|
| [Paralelo](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch) | ★☆☆ *Inicial* | Una demostración de ejecución paralela que muestra una aceleración de 3x |
| [Flujo Paralelo](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-parallel-batch-flow) | ★☆☆ *Inicial* | Una demostración de procesamiento de imágenes en paralelo que muestra una aceleración de 8x con múltiples filtros |
| [Voto por Mayoría](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-majority-vote) | ★☆☆ *Inicial* | Mejora de la precisión del razonamiento mediante la agregación de múltiples intentos de solución |
| [Pensamiento](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-thinking) | ★☆☆ *Inicial* | Resolver problemas de razonamiento complejos a través de Cadena de Pensamiento |
| [Memoria](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-chat-memory) | ★☆☆ *Inicial* | Un chatbot con memoria a corto y largo plazo |
| [Text2SQL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-text2sql) | ★☆☆ *Inicial* | Convertir lenguaje natural a consultas SQL con un bucle de auto-depuración |
| [MCP](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-mcp) | ★☆☆ *Inicial* | Agente que utiliza el Protocolo de Contexto de Modelo para operaciones numéricas |
| [A2A](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-a2a) | ★☆☆ *Inicial* | Agente envuelto con protocolo Agente-a-Agente para comunicación entre agentes |
| [Web HITL](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-web-hitl) | ★☆☆ *Inicial* | Un servicio web mínimo para un bucle de revisión humana con actualizaciones SSE |
👀 ¿Quieres ver otros tutoriales para principiantes? [¡Crea un issue!](https://github.com/The-Pocket/PocketFlow/issues/new)
## ¿Cómo usar Pocket Flow?
🚀 A través de la **Programación mediante Agentes**—el paradigma de desarrollo de aplicaciones LLM más rápido- donde *los humanos diseñan* y *los agentes programan*!
✨ A continuación hay ejemplos de aplicaciones LLM más complejas:
| Nombre de la App | Dificultad | Temas | Diseño Humano | Código del Agente |
| :-------------: | :-------------: | :---------------------: | :---: | :---: |
| [Construir Cursor con Cursor](https://github.com/The-Pocket/Tutorial-Cursor) Pronto alcanzaremos la singularidad ... | ★★★ *Avanzado* | [Agente](https://the-pocket.github.io/PocketFlow/design_pattern/agent.html) | [Doc de Diseño](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/docs/design.md) | [Código de Flujo](https://github.com/The-Pocket/Tutorial-Cursor/blob/main/flow.py)
| [Constructor de Conocimiento de Código Base](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge) La vida es demasiado corta para mirar el código de otros con confusión | ★★☆ *Medio* | [Flujo de Trabajo](https://the-pocket.github.io/PocketFlow/design_pattern/workflow.html) | [Doc de Diseño](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/docs/design.md) | [Código de Flujo](https://github.com/The-Pocket/Tutorial-Codebase-Knowledge/blob/main/flow.py)
| [Pregunta a AI Paul Graham](https://github.com/The-Pocket/Tutorial-YC-Partner) Pregunta a AI Paul Graham, en caso de que no entres | ★★☆ *Medio* | [RAG](https://the-pocket.github.io/PocketFlow/design_pattern/rag.html) [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [TTS](https://the-pocket.github.io/PocketFlow/utility_function/text_to_speech.html) | [Doc de Diseño](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/docs/design.md) | [Código de Flujo](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py)
| [Resumidor de Youtube](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple) Explica videos de YouTube como si tuvieras 5 años | ★☆☆ *Principiante* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) | [Doc de Diseño](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/docs/design.md) | [Código de Flujo](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple/blob/main/flow.py)
| [Generador de Introducción para Email Frío](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization) Rompehielos instantáneos que convierten leads fríos en calientes | ★☆☆ *Principiante* | [Map Reduce](https://the-pocket.github.io/PocketFlow/design_pattern/mapreduce.html) [Búsqueda Web](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [Doc de Diseño](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Código de Flujo](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/flow.py)
- ¿Quieres aprender **Programación mediante Agentes**?
- ¡Consulta [mi YouTube](https://www.youtube.com/@ZacharyLLM?sub_confirmation=1) para ver tutoriales en video sobre cómo se crearon algunas de las aplicaciones anteriores!
- ¿Quieres construir tu propia aplicación LLM? ¡Lee este [post](https://zacharyhuang.substack.com/p/agentic-coding-the-most-fun-way-to)! ¡Comienza con [esta plantilla](https://github.com/The-Pocket/PocketFlow-Template-Python)!
================================================
FILE: cookbook/pocketflow-parallel-batch/utils.py
================================================
import os
import asyncio
from anthropic import AsyncAnthropic
# Async version of the simple wrapper, using Anthropic
async def call_llm(prompt):
"""Async wrapper for Anthropic API call."""
client = AsyncAnthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "your-api-key"))
response = await client.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=20000,
thinking={
"type": "enabled",
"budget_tokens": 16000
},
messages=[
{"role": "user", "content": prompt}
],
)
return response.content[1].text
if __name__ == "__main__":
async def run_test():
print("## Testing async call_llm with Anthropic")
prompt = "In a few words, what is the meaning of life?"
print(f"## Prompt: {prompt}")
response = await call_llm(prompt)
print(f"## Response: {response}")
asyncio.run(run_test())
================================================
FILE: cookbook/pocketflow-parallel-batch-flow/README.md
================================================
# Parallel Image Processor
Demonstrates how AsyncParallelBatchFlow processes multiple images with multiple filters >8x faster than sequential processing.
## Features
```mermaid
graph TD
subgraph AsyncParallelBatchFlow[Image Processing Flow]
subgraph AsyncFlow[Per Image-Filter Flow]
A[Load Image] --> B[Apply Filter]
B --> C[Save Image]
end
end
```
- Processes images with multiple filters in parallel
- Applies three different filters (grayscale, blur, sepia)
- Shows significant speed improvement over sequential processing
- Manages system resources with semaphores
## Run It
```bash
pip install -r requirements.txt
python main.py
```
## Output
```=== Processing Images in Parallel ===
Parallel Image Processor
------------------------------
Found 3 images:
- images/bird.jpg
- images/cat.jpg
- images/dog.jpg
Running sequential batch flow...
Processing 3 images with 3 filters...
Total combinations: 9
Loading image: images/bird.jpg
Applying grayscale filter...
Saved: output/bird_grayscale.jpg
...etc
Timing Results:
Sequential batch processing: 13.76 seconds
Parallel batch processing: 1.71 seconds
Speedup: 8.04x
Processing complete! Check the output/ directory for results.
```
## Key Points
- **Sequential**: Total time = sum of all item times
- Good for: Rate-limited APIs, maintaining order
- **Parallel**: Total time ≈ longest single item time
- Good for: I/O-bound tasks, independent operations
================================================
FILE: cookbook/pocketflow-parallel-batch-flow/flow.py
================================================
"""Flow definitions for parallel image processing."""
from pocketflow import AsyncParallelBatchFlow, AsyncBatchFlow
from nodes import LoadImage, ApplyFilter, SaveImage
def create_base_flow():
"""Create flow for processing a single image with one filter."""
# Create nodes
load = LoadImage()
apply_filter = ApplyFilter()
save = SaveImage()
# Connect nodes
load - "apply_filter" >> apply_filter
apply_filter - "save" >> save
# Create flow
return load
class ImageBatchFlow(AsyncBatchFlow):
"""Flow that processes multiple images with multiple filters in batch."""
async def prep_async(self, shared):
"""Generate parameters for each image-filter combination."""
# Get list of images and filters
images = shared.get("images", [])
filters = ["grayscale", "blur", "sepia"]
# Create parameter combinations
params = []
for image_path in images:
for filter_type in filters:
params.append({
"image_path": image_path,
"filter": filter_type
})
print(f"Processing {len(images)} images with {len(filters)} filters...")
print(f"Total combinations: {len(params)}")
return params
class ImageParallelBatchFlow(AsyncParallelBatchFlow):
"""Flow that processes multiple images with multiple filters in parallel."""
async def prep_async(self, shared):
"""Generate parameters for each image-filter combination."""
# Get list of images and filters
images = shared.get("images", [])
filters = ["grayscale", "blur", "sepia"]
# Create parameter combinations
params = []
for image_path in images:
for filter_type in filters:
params.append({
"image_path": image_path,
"filter": filter_type
})
print(f"Processing {len(images)} images with {len(filters)} filters...")
print(f"Total combinations: {len(params)}")
return params
def create_flows():
"""Create the complete parallel processing flow."""
# Create base flow for single image processing
base_flow = create_base_flow()
# Wrap in parallel batch flow
return ImageBatchFlow(start=base_flow), ImageParallelBatchFlow(start=base_flow)
================================================
FILE: cookbook/pocketflow-parallel-batch-flow/main.py
================================================
import os
import asyncio
import time
from flow import create_flows
def get_image_paths():
"""Get paths of existing images in the images directory."""
images_dir = "images"
if not os.path.exists(images_dir):
raise ValueError(f"Directory '{images_dir}' not found!")
# List all jpg files in the images directory
image_paths = []
for filename in os.listdir(images_dir):
if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
image_paths.append(os.path.join(images_dir, filename))
if not image_paths:
raise ValueError(f"No images found in '{images_dir}' directory!")
print(f"Found {len(image_paths)} images:")
for path in image_paths:
print(f"- {path}")
return image_paths
async def main():
"""Run the parallel image processing example."""
print("Parallel Image Processor")
print("-" * 30)
# Get existing image paths
image_paths = get_image_paths()
# Create shared store with image paths
shared = {"images": image_paths}
# Create both flows
batch_flow, parallel_batch_flow = create_flows()
# Run and time batch flow
start_time = time.time()
print("\nRunning sequential batch flow...")
await batch_flow.run_async(shared)
batch_time = time.time() - start_time
# Run and time parallel batch flow
start_time = time.time()
print("\nRunning parallel batch flow...")
await parallel_batch_flow.run_async(shared)
parallel_time = time.time() - start_time
# Print timing results
print("\nTiming Results:")
print(f"Sequential batch processing: {batch_time:.2f} seconds")
print(f"Parallel batch processing: {parallel_time:.2f} seconds")
print(f"Speedup: {batch_time/parallel_time:.2f}x")
print("\nProcessing complete! Check the output/ directory for results.")
if __name__ == "__main__":
asyncio.run(main())
================================================
FILE: cookbook/pocketflow-parallel-batch-flow/nodes.py
================================================
"""AsyncNode implementations for image processing."""
import os
import asyncio
from PIL import Image, ImageFilter
import numpy as np
from pocketflow import AsyncNode
class LoadImage(AsyncNode):
"""Node that loads an image from file."""
async def prep_async(self, shared):
"""Get image path from parameters."""
image_path = self.params["image_path"]
print(f"Loading image: {image_path}")
return image_path
async def exec_async(self, image_path):
"""Load image using PIL."""
# Simulate I/O delay
await asyncio.sleep(0.5)
return Image.open(image_path)
async def post_async(self, shared, prep_res, exec_res):
"""Store image in shared store."""
shared["image"] = exec_res
return "apply_filter"
class ApplyFilter(AsyncNode):
"""Node that applies a filter to an image."""
async def prep_async(self, shared):
"""Get image and filter type."""
image = shared["image"]
filter_type = self.params["filter"]
print(f"Applying {filter_type} filter...")
return image, filter_type
async def exec_async(self, inputs):
"""Apply the specified filter."""
image, filter_type = inputs
# Simulate processing delay
await asyncio.sleep(0.5)
if filter_type == "grayscale":
return image.convert("L")
elif filter_type == "blur":
return image.filter(ImageFilter.BLUR)
elif filter_type == "sepia":
# Convert to array for sepia calculation
img_array = np.array(image)
sepia_matrix = np.array([
[0.393, 0.769, 0.189],
[0.349, 0.686, 0.168],
[0.272, 0.534, 0.131]
])
sepia_array = img_array.dot(sepia_matrix.T)
sepia_array = np.clip(sepia_array, 0, 255).astype(np.uint8)
return Image.fromarray(sepia_array)
else:
raise ValueError(f"Unknown filter: {filter_type}")
async def post_async(self, shared, prep_res, exec_res):
"""Store filtered image."""
shared["filtered_image"] = exec_res
return "save"
class SaveImage(AsyncNode):
"""Node that saves the processed image."""
async def prep_async(self, shared):
"""Prepare output path."""
image = shared["filtered_image"]
base_name = os.path.splitext(os.path.basename(self.params["image_path"]))[0]
filter_type = self.params["filter"]
output_path = f"output/{base_name}_{filter_type}.jpg"
# Create output directory if needed
os.makedirs("output", exist_ok=True)
return image, output_path
async def exec_async(self, inputs):
"""Save the image."""
image, output_path = inputs
# Simulate I/O delay
await asyncio.sleep(0.5)
image.save(output_path)
return output_path
async def post_async(self, shared, prep_res, exec_res):
"""Print success message."""
print(f"Saved: {exec_res}")
return "default"
================================================
FILE: cookbook/pocketflow-parallel-batch-flow/requirements.txt
================================================
pocketflow
Pillow>=10.0.0 # For image processing
numpy>=1.24.0 # For image array operations
================================================
FILE: cookbook/pocketflow-rag/README.md
================================================
# Retrieval Augmented Generation (RAG)
This project demonstrates a simplified RAG system that retrieves relevant documents based on user queries and generates answers using an LLM. This implementation is based directly on the tutorial: [Retrieval Augmented Generation (RAG) from Scratch — Tutorial For Dummies](https://zacharyhuang.substack.com/p/retrieval-augmented-generation-rag).
## Features
- Document chunking for processing long texts
- FAISS-powered vector-based document retrieval
- LLM-powered answer generation
## How to Run
1. Set your API key:
```bash
export OPENAI_API_KEY="your-api-key-here"
```
Or update it directly in `utils.py`
Let's do a quick check to make sure your API key is working properly:
```bash
python utils.py
```
2. Install and run with the default query:
```bash
pip install -r requirements.txt
python main.py
```
3. Run the application with a sample query:
```bash
python main.py --"How does the Q-Mesh protocol achieve high transaction speeds?"
```
## How It Works
The magic happens through a two-phase pipeline implemented with PocketFlow:
```mermaid
graph TD
subgraph OfflineFlow[Offline Document Indexing]
ChunkDocs[ChunkDocumentsNode] --> EmbedDocs[EmbedDocumentsNode] --> CreateIndex[CreateIndexNode]
end
subgraph OnlineFlow[Online Processing]
EmbedQuery[EmbedQueryNode] --> RetrieveDoc[RetrieveDocumentNode] --> GenerateAnswer[GenerateAnswerNode]
end
```
Here's what each part does:
1. **ChunkDocumentsNode**: Breaks documents into smaller chunks for better retrieval
2. **EmbedDocumentsNode**: Converts document chunks into vector representations
3. **CreateIndexNode**: Creates a searchable FAISS index from embeddings
4. **EmbedQueryNode**: Converts user query into the same vector space
5. **RetrieveDocumentNode**: Finds the most similar document using vector search
6. **GenerateAnswerNode**: Uses an LLM to generate an answer based on the retrieved content
## Example Output
```
✅ Created 5 chunks from 5 documents
✅ Created 5 document embeddings
🔍 Creating search index...
✅ Index created with 5 vectors
🔍 Embedding query: How to install PocketFlow?
🔎 Searching for relevant documents...
📄 Retrieved document (index: 0, distance: 0.3427)
📄 Most relevant text: "Pocket Flow is a 100-line minimalist LLM framework
Lightweight: Just 100 lines. Zero bloat, zero dependencies, zero vendor lock-in.
Expressive: Everything you love—(Multi-)Agents, Workflow, RAG, and more.
Agentic Coding: Let AI Agents (e.g., Cursor AI) build Agents—10x productivity boost!
To install, pip install pocketflow or just copy the source code (only 100 lines)."
🤖 Generated Answer:
To install PocketFlow, use the command `pip install pocketflow` or simply copy its 100 lines of source code.
```
================================================
FILE: cookbook/pocketflow-rag/flow.py
================================================
from pocketflow import Flow
from nodes import EmbedDocumentsNode, CreateIndexNode, EmbedQueryNode, RetrieveDocumentNode, ChunkDocumentsNode, GenerateAnswerNode
def get_offline_flow():
# Create offline flow for document indexing
chunk_docs_node = ChunkDocumentsNode()
embed_docs_node = EmbedDocumentsNode()
create_index_node = CreateIndexNode()
# Connect the nodes
chunk_docs_node >> embed_docs_node >> create_index_node
offline_flow = Flow(start=chunk_docs_node)
return offline_flow
def get_online_flow():
# Create online flow for document retrieval and answer generation
embed_query_node = EmbedQueryNode()
retrieve_doc_node = RetrieveDocumentNode()
generate_answer_node = GenerateAnswerNode()
# Connect the nodes
embed_query_node >> retrieve_doc_node >> generate_answer_node
online_flow = Flow(start=embed_query_node)
return online_flow
# Initialize flows
offline_flow = get_offline_flow()
online_flow = get_online_flow()
================================================
FILE: cookbook/pocketflow-rag/main.py
================================================
import sys
from flow import offline_flow, online_flow
def run_rag_demo():
"""
Run a demonstration of the RAG system.
This function:
1. Indexes a set of sample documents (offline flow)
2. Takes a query from the command line
3. Retrieves the most relevant document (online flow)
4. Generates an answer using an LLM
"""
# Sample texts - specialized/fictional content that benefits from RAG
texts = [
# PocketFlow framework
"""Pocket Flow is a 100-line minimalist LLM framework
Lightweight: Just 100 lines. Zero bloat, zero dependencies, zero vendor lock-in.
Expressive: Everything you love—(Multi-)Agents, Workflow, RAG, and more.
Agentic Coding: Let AI Agents (e.g., Cursor AI) build Agents—10x productivity boost!
To install, pip install pocketflow or just copy the source code (only 100 lines).""",
# Fictional medical device
"""NeurAlign M7 is a revolutionary non-invasive neural alignment device.
Targeted magnetic resonance technology increases neuroplasticity in specific brain regions.
Clinical trials showed 72% improvement in PTSD treatment outcomes.
Developed by Cortex Medical in 2024 as an adjunct to standard cognitive therapy.
Portable design allows for in-home use with remote practitioner monitoring.""",
# Made-up historical event
"""The Velvet Revolution of Caldonia (1967-1968) ended Generalissimo Verak's 40-year rule.
Led by poet Eliza Markovian through underground literary societies.
Culminated in the Great Silence Protest with 300,000 silent protesters.
First democratic elections held in March 1968 with 94% voter turnout.
Became a model for non-violent political transitions in neighboring regions.""",
# Fictional technology
"""Q-Mesh is QuantumLeap Technologies' instantaneous data synchronization protocol.
Utilizes directed acyclic graph consensus for 500,000 transactions per second.
Consumes 95% less energy than traditional blockchain systems.
Adopted by three central banks for secure financial data transfer.
Released in February 2024 after five years of development in stealth mode.""",
# Made-up scientific research
"""Harlow Institute's Mycelium Strain HI-271 removes 99.7% of PFAS from contaminated soil.
Engineered fungi create symbiotic relationships with native soil bacteria.
Breaks down "forever chemicals" into non-toxic compounds within 60 days.
Field tests successfully remediated previously permanently contaminated industrial sites.
Deployment costs 80% less than traditional chemical extraction methods."""
]
print("=" * 50)
print("PocketFlow RAG Document Retrieval")
print("=" * 50)
# Default query about the fictional technology
default_query = "How to install PocketFlow?"
# Get query from command line if provided with --
query = default_query
for arg in sys.argv[1:]:
if arg.startswith("--"):
query = arg[2:]
break
# Single shared store for both flows
shared = {
"texts": texts,
"embeddings": None,
"index": None,
"query": query,
"query_embedding": None,
"retrieved_document": None,
"generated_answer": None
}
# Initialize and run the offline flow (document indexing)
offline_flow.run(shared)
# Run the online flow to retrieve the most relevant document and generate an answer
online_flow.run(shared)
if __name__ == "__main__":
run_rag_demo()
================================================
FILE: cookbook/pocketflow-rag/nodes.py
================================================
from pocketflow import Node, Flow, BatchNode
import numpy as np
import faiss
from utils import call_llm, get_embedding, fixed_size_chunk
# Nodes for the offline flow
class ChunkDocumentsNode(BatchNode):
def prep(self, shared):
"""Read texts from shared store"""
return shared["texts"]
def exec(self, text):
"""Chunk a single text into smaller pieces"""
return fixed_size_chunk(text)
def post(self, shared, prep_res, exec_res_list):
"""Store chunked texts in the shared store"""
# Flatten the list of lists into a single list of chunks
all_chunks = []
for chunks in exec_res_list:
all_chunks.extend(chunks)
# Replace the original texts with the flat list of chunks
shared["texts"] = all_chunks
print(f"✅ Created {len(all_chunks)} chunks from {len(prep_res)} documents")
return "default"
class EmbedDocumentsNode(BatchNode):
def prep(self, shared):
"""Read texts from shared store and return as an iterable"""
return shared["texts"]
def exec(self, text):
"""Embed a single text"""
return get_embedding(text)
def post(self, shared, prep_res, exec_res_list):
"""Store embeddings in the shared store"""
embeddings = np.array(exec_res_list, dtype=np.float32)
shared["embeddings"] = embeddings
print(f"✅ Created {len(embeddings)} document embeddings")
return "default"
class CreateIndexNode(Node):
def prep(self, shared):
"""Get embeddings from shared store"""
return shared["embeddings"]
def exec(self, embeddings):
"""Create FAISS index and add embeddings"""
print("🔍 Creating search index...")
dimension = embeddings.shape[1]
# Create a flat L2 index
index = faiss.IndexFlatL2(dimension)
# Add the embeddings to the index
index.add(embeddings)
return index
def post(self, shared, prep_res, exec_res):
"""Store the index in shared store"""
shared["index"] = exec_res
print(f"✅ Index created with {exec_res.ntotal} vectors")
return "default"
# Nodes for the online flow
class EmbedQueryNode(Node):
def prep(self, shared):
"""Get query from shared store"""
return shared["query"]
def exec(self, query):
"""Embed the query"""
print(f"🔍 Embedding query: {query}")
query_embedding = get_embedding(query)
return np.array([query_embedding], dtype=np.float32)
def post(self, shared, prep_res, exec_res):
"""Store query embedding in shared store"""
shared["query_embedding"] = exec_res
return "default"
class RetrieveDocumentNode(Node):
def prep(self, shared):
"""Get query embedding, index, and texts from shared store"""
return shared["query_embedding"], shared["index"], shared["texts"]
def exec(self, inputs):
"""Search the index for similar documents"""
print("🔎 Searching for relevant documents...")
query_embedding, index, texts = inputs
# Search for the most similar document
distances, indices = index.search(query_embedding, k=1)
# Get the index of the most similar document
best_idx = indices[0][0]
distance = distances[0][0]
# Get the corresponding text
most_relevant_text = texts[best_idx]
return {
"text": most_relevant_text,
"index": best_idx,
"distance": distance
}
def post(self, shared, prep_res, exec_res):
"""Store retrieved document in shared store"""
shared["retrieved_document"] = exec_res
print(f"📄 Retrieved document (index: {exec_res['index']}, distance: {exec_res['distance']:.4f})")
print(f"📄 Most relevant text: \"{exec_res['text']}\"")
return "default"
class GenerateAnswerNode(Node):
def prep(self, shared):
"""Get query, retrieved document, and any other context needed"""
return shared["query"], shared["retrieved_document"]
def exec(self, inputs):
"""Generate an answer using the LLM"""
query, retrieved_doc = inputs
prompt = f"""
Briefly answer the following question based on the context provided:
Question: {query}
Context: {retrieved_doc['text']}
Answer:
"""
answer = call_llm(prompt)
return answer
def post(self, shared, prep_res, exec_res):
"""Store generated answer in shared store"""
shared["generated_answer"] = exec_res
print("\n🤖 Generated Answer:")
print(exec_res)
return "default"
================================================
FILE: cookbook/pocketflow-rag/requirements.txt
================================================
pocketflow>=0.0.1
numpy>=1.20.0
faiss-cpu>=1.7.0
openai>=1.0.0
================================================
FILE: cookbook/pocketflow-rag/utils.py
================================================
import os
import numpy as np
from openai import OpenAI
def call_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
def get_embedding(text):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
response = client.embeddings.create(
model="text-embedding-ada-002",
input=text
)
# Extract the embedding vector from the response
embedding = response.data[0].embedding
# Convert to numpy array for consistency with other embedding functions
return np.array(embedding, dtype=np.float32)
def fixed_size_chunk(text, chunk_size=2000):
chunks = []
for i in range(0, len(text), chunk_size):
chunks.append(text[i : i + chunk_size])
return chunks
if __name__ == "__main__":
print("=== Testing call_llm ===")
prompt = "In a few words, what is the meaning of life?"
print(f"Prompt: {prompt}")
response = call_llm(prompt)
print(f"Response: {response}")
print("=== Testing embedding function ===")
text1 = "The quick brown fox jumps over the lazy dog."
text2 = "Python is a popular programming language for data science."
oai_emb1 = get_embedding(text1)
oai_emb2 = get_embedding(text2)
print(f"OpenAI Embedding 1 shape: {oai_emb1.shape}")
oai_similarity = np.dot(oai_emb1, oai_emb2)
print(f"OpenAI similarity between texts: {oai_similarity:.4f}")
================================================
FILE: cookbook/pocketflow-streamlit-fsm/README.md
================================================
# PocketFlow Streamlit Image Generation HITL
Human-in-the-Loop (HITL) image generation application using PocketFlow and Streamlit. Enter text prompts, generate images with OpenAI, and approve/regenerate results.
## Features
- **Image Generation:** Uses OpenAI's `gpt-image-1` model to generate images from text prompts
- **Human Review:** Interactive interface to approve or regenerate images
- **State Machine:** Clean state-based workflow (`initial_input` → `user_feedback` → `final`)
- **PocketFlow Integration:** Uses PocketFlow `Node` and `Flow` for image generation with built-in retries
- **Session State Management:** Streamlit session state acts as PocketFlow's shared store
- **In-Memory Images:** Images stored as base64 strings, no disk storage required
## How to Run
1. **Set OpenAI API Key:**
```bash
export OPENAI_API_KEY="your-openai-api-key"
```
2. **Install Dependencies:**
```bash
pip install -r requirements.txt
```
3. **Run the Streamlit Application:**
```bash
streamlit run app.py
```
4. **Access the Web UI:**
Open the URL provided by Streamlit (usually `http://localhost:8501`).
## Usage
1. **Enter Prompt**: Describe the image you want to generate
2. **Generate**: Click "Generate Image" to create the image
3. **Review**: View the generated image and choose:
- **Approve**: Accept the image and move to final result
- **Regenerate**: Generate a new image with the same prompt
4. **Final**: View approved image and optionally start over
## Files
- [`app.py`](./app.py): Main Streamlit application with state-based UI
- [`nodes.py`](./nodes.py): PocketFlow `GenerateImageNode` definition
- [`flow.py`](./flow.py): PocketFlow `Flow` for image generation
- [`utils/generate_image.py`](./utils/generate_image.py): OpenAI image generation utility
- [`requirements.txt`](./requirements.txt): Project dependencies
- [`docs/design.md`](./docs/design.md): System design documentation
- [`README.md`](./README.md): This file
================================================
FILE: cookbook/pocketflow-streamlit-fsm/app.py
================================================
import streamlit as st
import base64
from flow import create_generation_flow
st.title("PocketFlow Image Generation HITL")
# Initialize session state for shared store
if 'stage' not in st.session_state:
st.session_state.stage = "initial_input"
st.session_state.task_input = ""
st.session_state.generated_image = ""
st.session_state.final_result = ""
st.session_state.error_message = ""
# Debug info
with st.expander("Session State"):
st.json({k: v for k, v in st.session_state.items() if not k.startswith("_")})
# State-based UI
if st.session_state.stage == "initial_input":
st.header("1. Generate Image")
prompt = st.text_area("Enter image prompt:", value=st.session_state.task_input, height=100)
if st.button("Generate Image"):
if prompt.strip():
st.session_state.task_input = prompt
st.session_state.error_message = ""
try:
with st.spinner("Generating image..."):
flow = create_generation_flow()
flow.run(st.session_state)
st.rerun()
except Exception as e:
st.session_state.error_message = str(e)
else:
st.error("Please enter a prompt")
elif st.session_state.stage == "user_feedback":
st.header("2. Review Generated Image")
if st.session_state.generated_image:
# Display image
image_bytes = base64.b64decode(st.session_state.generated_image)
st.image(image_bytes, caption=f"Prompt: {st.session_state.task_input}")
col1, col2 = st.columns(2)
with col1:
if st.button("Approve", use_container_width=True):
st.session_state.final_result = st.session_state.generated_image
st.session_state.stage = "final"
st.rerun()
with col2:
if st.button("Regenerate", use_container_width=True):
try:
with st.spinner("Regenerating image..."):
flow = create_generation_flow()
flow.run(st.session_state)
st.rerun()
except Exception as e:
st.session_state.error_message = str(e)
elif st.session_state.stage == "final":
st.header("3. Final Result")
st.success("Image approved!")
if st.session_state.final_result:
image_bytes = base64.b64decode(st.session_state.final_result)
st.image(image_bytes, caption=f"Final approved image: {st.session_state.task_input}")
if st.button("Start Over", use_container_width=True):
st.session_state.stage = "initial_input"
st.session_state.task_input = ""
st.session_state.generated_image = ""
st.session_state.final_result = ""
st.session_state.error_message = ""
st.rerun()
# Show errors
if st.session_state.error_message:
st.error(st.session_state.error_message)
================================================
FILE: cookbook/pocketflow-streamlit-fsm/docs/design.md
================================================
# Design Doc: PocketFlow Streamlit Image Generation HITL
> Human-in-the-Loop image generation application using PocketFlow and Streamlit
## Requirements
**User Story**: As a user, I want to:
1. Enter a text prompt describing an image I want to generate
2. Have the system generate an image based on my prompt using OpenAI's image generation API
3. Review the generated image in the web interface
4. Approve the image if I'm satisfied, OR regenerate with the same prompt if I want a different result
5. See the final approved image as the completed result
**Technical Requirements**:
- Use OpenAI's image generation API (via responses.create with image_generation tool)
- Keep generated images in memory (base64 format) - no disk storage
- Provide clear approve/regenerate workflow
- Handle API errors gracefully with retries
- Maintain session state between generations
## Flow Design
### Applicable Design Pattern:
**State Machine with Multiple Subflows**: Each state has its own user interface and workflow. Users interact with different UI elements in each state, and the app transitions to the next state based on user actions and feedback.
### States & User Interface:
1. **initial_input** - User sees text input field, enters prompt, clicks "Generate Image" button
2. **user_feedback** - User sees generated image, has "Approve" and "Regenerate" buttons
3. **final** - User sees final approved image and "Start Over" button
### Flow High-level Design & Transitions:
```mermaid
flowchart TD
Start([Start]) --> IS[initial_input]
IS --> GI[GenerateImage]
GI --> UF[user_feedback]
UF -->|Regenerate| GI
UF -->|Approve| F[final]
F --> IS
%% Legend
classDef stateStyle fill:#e1f5fe,stroke:#01579b,stroke-width:2px
classDef nodeStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
class IS,UF,F stateStyle
class GI nodeStyle
```
**Legend:**
- 🔷 **Blue rectangles**: User interface states (initial_input, user_feedback, final)
- 🔶 **Orange rectangles**: PocketFlow processing nodes (GenerateImage)
## Utility Functions
1. **Generate Image** (`utils/generate_image.py`)
- *Input*: prompt (str)
- *Output*: base64 image data (str)
- *Purpose*: Calls OpenAI's image generation API and returns base64 encoded image
- *Error Handling*: Includes retry logic for API failures
## Node Design
### Shared Memory
**Using Streamlit Session State as Shared Store**: We use `st.session_state` directly as the shared store for PocketFlow, eliminating the need for separate data structures.
The session state structure for the image generation workflow:
```python
st.session_state = {
# User input and workflow state
"task_input": "user's text prompt for image generation",
"stage": "current workflow stage (initial_input/user_feedback/final)",
"error_message": "any error messages for user feedback",
# Processing data
"input_used_by_process": "prompt used for generation",
"generated_image": "base64 encoded image data",
"final_result": "final approved image data",
# Streamlit built-in keys (managed automatically)
# "_streamlit_*": various internal streamlit state
}
```
### Node Steps
**Initial Input Flow Nodes:**
1. **Image Generation Node**
- *Purpose*: Generate image using OpenAI API based on the prompt
- *Type*: Regular (with retries for API reliability)
- *Steps*:
- *prep*: Read "input_used_by_process" from st.session_state
- *exec*: Call generate_image utility with the prompt, return base64 image data
- *post*: Write base64 image data to "generated_image" in st.session_state
**User Feedback Flow:**
- Reuses the same `GenerateImage` node when user clicks "Regenerate"
**Final Flow:**
- No processing nodes needed - the `final` state simply displays the approved image from `generated_image` and provides UI for starting over
================================================
FILE: cookbook/pocketflow-streamlit-fsm/flow.py
================================================
from pocketflow import Flow
from nodes import GenerateImageNode
def create_generation_flow():
"""Creates a flow for image generation (initial or regeneration)."""
generate_image_node = GenerateImageNode()
return Flow(start=generate_image_node)
================================================
FILE: cookbook/pocketflow-streamlit-fsm/nodes.py
================================================
from pocketflow import Node
from utils.generate_image import generate_image
class GenerateImageNode(Node):
"""Generates image from text prompt using OpenAI API."""
def prep(self, shared):
return shared.get("task_input", "")
def exec(self, prompt):
return generate_image(prompt)
def post(self, shared, prep_res, exec_res):
shared["input_used_by_process"] = prep_res
shared["generated_image"] = exec_res
shared["stage"] = "user_feedback"
return "default"
================================================
FILE: cookbook/pocketflow-streamlit-fsm/requirements.txt
================================================
streamlit
pocketflow
openai
================================================
FILE: cookbook/pocketflow-streamlit-fsm/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-streamlit-fsm/utils/generate_image.py
================================================
from openai import OpenAI
import os
import base64
def generate_image(prompt: str) -> str:
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
response = client.images.generate(
model="gpt-image-1",
prompt=prompt,
n=1,
size="1024x1024"
)
image_b64 = response.data[0].b64_json
print(f"Generated image ({len(image_b64)} chars)")
return image_b64
if __name__ == "__main__":
test_prompt = "A gray tabby cat hugging an otter with an orange scarf"
print(f"Generating image for prompt: {test_prompt[:50]}...")
image_base64 = generate_image(test_prompt)
print(f"Success! Generated {len(image_base64)} characters of base64 data")
# Write image to local file for testing
image_bytes = base64.b64decode(image_base64)
with open("test_generated_image.png", "wb") as f:
f.write(image_bytes)
print("Test image saved as test_generated_image.png")
================================================
FILE: cookbook/pocketflow-structured-output/README.md
================================================
# Structured Output Demo
A minimal demo application showing how to use PocketFlow to extract structured data from a resume using direct prompting and YAML formatting. Why YAML? Check out the [doc](https://the-pocket.github.io/PocketFlow/design_pattern/structure.html).
This implementation is based on: [Structured Output for Beginners: 3 Must-Know Prompting Tips](https://zacharyhuang.substack.com/p/structured-output-for-beginners-3).
## Features
- Extracts structured data using prompt engineering
- Validates output structure before processing
## Run It
1. Install the packages you need with this simple command:
```bash
pip install -r requirements.txt
```
2. Make sure your OpenAI API key is set:
```bash
export OPENAI_API_KEY="your-api-key-here"
```
Alternatively, you can edit the [`utils.py`](./utils.py) file to include your API key directly.
Let's do a quick check to make sure your API key is working properly:
```bash
python utils.py
```
3. Edit [data.txt](./data.txt) with the resume you want to parse (a sample resume is already included)
4. Run the application:
```bash
python main.py
```
## How It Works
```mermaid
flowchart LR
parser[ResumeParserNode]
```
The Resume Parser application uses a single node that:
1. Takes resume text from the shared state (loaded from data.txt)
2. Sends the resume to an LLM with a prompt that requests YAML formatted output
3. Extracts and validates the structured YAML data
4. Outputs the structured result
## Files
- [`main.py`](./main.py): Implementation of the ResumeParserNode
- [`utils.py`](./utils.py): LLM utilities
- [`data.txt`](./data.txt): Sample resume text file
## Example Output
```
=== Resume Parser - Structured Output with Indexes & Comments ===
=== STRUCTURED RESUME DATA (Comments & Skill Index List) ===
name: JOHN SMTIH
email: johnsmtih1983@gnail.com
experience:
- {title: SALES MANAGER, company: ABC Corportaion}
- {title: ASST. MANAGER, company: XYZ Industries}
- {title: CUSTOMER SERVICE REPRESENTATIVE, company: Fast Solutions Inc}
skill_indexes: [0, 1, 2, 3, 4]
============================================================
✅ Extracted resume information.
--- Found Target Skills (from Indexes) ---
- Team leadership & management (Index: 0)
- CRM software (Index: 1)
- Project management (Index: 2)
- Public speaking (Index: 3)
- Microsoft Office (Index: 4)
----------------------------------------
```
================================================
FILE: cookbook/pocketflow-structured-output/data.txt
================================================
# JOHN SMTIH
**Email:** johnsmtih1983@gnail.com
**Phone:** (555) 123-4556
**Address:** 123 Main st, Anytown, USA
## PROFFESIONAL SUMMARY
Dedicated and hardworking professional with over 10 years of exprience in business manegement. Known for finding creatve solutions to complex problems and excelent communication skills. Seeking new opportunites to leverage my expertise in a dynamic environment.
## WORK EXPERENCE
### SALES MANAGER
**ABC Corportaion** | Anytown, USA | June 2018 - Present
- Oversee a team of 12 sales represenatives and achieve quarterly targets
- Increased departmnet revenue by 24% in fiscal year 2019-2020
- Implemneted new CRM system that improved efficiency by 15%
- Collabarate with Marketing team on product launch campaigns
- Developed training materials for new hiers
### ASST. MANAGER
**XYZ Industries** | Somewhere Else, USA | March 2015 - may 2018
- Assisted the Regional Manager in daily operations and reporting
- managed inventory and vendor relations
- Trained and mentored junior staff members
- Recieved "Employee of the Month" award 4 times
### CUSTOMER SERVICE REPRESENTATIVE
**Fast Solutions Inc** | Another City, USA | January 2010 - February 2015
* Responded to customer inquiries via phone email, and in-person
* Resolved customer complaints and escalated issues when necessary
* Maintained a 95% customer satsfaction rating
## EDUCATIONS
**Bachelor of Buisness Administration**
University of Somewhere | 2006 - 2010
GPA: 3.6/4.0
**Assosiate Degree in Communications**
Community College | 2004-2006
## SKILSS
- Microsoft Office: *Excel, Word, Powerpoint* (Advanced)
- Customer relationship management (CRM) software
- Team leadership & managment
- Project management
- Public speking
- Time managemant
## REFERENCES
Available upon reqeust
### OTHER ACTVITIES
- Volunteer at the local food bank (2016-present)
- Member of Toastmasters International
- Enjoy hiking and photografy
================================================
FILE: cookbook/pocketflow-structured-output/main.py
================================================
import yaml
import os # Needed for the utils import below
from pocketflow import Node, Flow
from utils import call_llm # Assumes utils.py with call_llm exists
class ResumeParserNode(Node):
def prep(self, shared):
"""Return resume text and target skills from shared state."""
return {
"resume_text": shared["resume_text"],
"target_skills": shared.get("target_skills", [])
}
def exec(self, prep_res):
"""Extract structured data from resume using prompt engineering.
Requests YAML output with comments and skill indexes as a list.
"""
resume_text = prep_res["resume_text"]
target_skills = prep_res["target_skills"]
# Format skills with indexes for the prompt
skill_list_for_prompt = "\n".join([f"{i}: {skill}" for i, skill in enumerate(target_skills)])
# Simplified Prompt focusing on key instructions and format
prompt = f"""
Analyze the resume below. Output ONLY the requested information in YAML format.
**Resume:**
```
{resume_text}
```
**Target Skills (use these indexes):**
```
{skill_list_for_prompt}
```
**YAML Output Requirements:**
- Extract `name` (string).
- Extract `email` (string).
- Extract `experience` (list of objects with `title` and `company`).
- Extract `skill_indexes` (list of integers found from the Target Skills list).
- **Add a YAML comment (`#`) explaining the source BEFORE `name`, `email`, `experience`, each item in `experience`, and `skill_indexes`.**
**Example Format:**
```yaml
# Found name at top
name: Jane Doe
# Found email in contact info
email: jane@example.com
# Experience section analysis
experience:
# First job listed
- title: Manager
company: Corp A
# Second job listed
- title: Assistant
company: Corp B
# Skills identified from the target list based on resume content
skill_indexes:
# Found 0 at top
- 0
# Found 2 in experience
- 2
```
Generate the YAML output now:
"""
response = call_llm(prompt)
# --- Minimal YAML Extraction ---
# Assumes LLM correctly uses ```yaml blocks
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
structured_result = yaml.safe_load(yaml_str)
# --- End Minimal Extraction ---
# --- Basic Validation ---
assert structured_result is not None, "Validation Failed: Parsed YAML is None"
assert "name" in structured_result, "Validation Failed: Missing 'name'"
assert "email" in structured_result, "Validation Failed: Missing 'email'"
assert "experience" in structured_result, "Validation Failed: Missing 'experience'"
assert isinstance(structured_result.get("experience"), list), "Validation Failed: 'experience' is not a list"
assert "skill_indexes" in structured_result, "Validation Failed: Missing 'skill_indexes'"
skill_indexes_val = structured_result.get("skill_indexes")
assert skill_indexes_val is None or isinstance(skill_indexes_val, list), "Validation Failed: 'skill_indexes' is not a list or None"
if isinstance(skill_indexes_val, list):
for index in skill_indexes_val:
assert isinstance(index, int), f"Validation Failed: Skill index '{index}' is not an integer"
# --- End Basic Validation ---
return structured_result
def post(self, shared, prep_res, exec_res):
"""Store structured data and print it."""
shared["structured_data"] = exec_res
print("\n=== STRUCTURED RESUME DATA (Comments & Skill Index List) ===\n")
# Dump YAML ensuring block style for readability
print(yaml.dump(exec_res, sort_keys=False, allow_unicode=True, default_flow_style=None))
print("\n============================================================\n")
print("✅ Extracted resume information.")
# === Main Execution Logic ===
if __name__ == "__main__":
print("=== Resume Parser - Structured Output with Indexes & Comments ===\n")
# --- Configuration ---
target_skills_to_find = [
"Team leadership & management", # 0
"CRM software", # 1
"Project management", # 2
"Public speaking", # 3
"Microsoft Office", # 4
"Python", # 5
"Data Analysis" # 6
]
resume_file = 'data.txt' # Assumes data.txt contains the resume
# --- Prepare Shared State ---
shared = {}
try:
with open(resume_file, 'r') as file:
shared["resume_text"] = file.read()
except FileNotFoundError:
print(f"Error: Resume file '{resume_file}' not found.")
exit(1) # Exit if resume file is missing
shared["target_skills"] = target_skills_to_find
# --- Define and Run Flow ---
parser_node = ResumeParserNode(max_retries=3, wait=10)
flow = Flow(start=parser_node)
flow.run(shared) # Execute the parsing node
# --- Display Found Skills ---
if "structured_data" in shared and "skill_indexes" in shared["structured_data"]:
print("\n--- Found Target Skills (from Indexes) ---")
found_indexes = shared["structured_data"]["skill_indexes"]
if found_indexes: # Check if the list is not empty or None
for index in found_indexes:
if 0 <= index < len(target_skills_to_find):
print(f"- {target_skills_to_find[index]} (Index: {index})")
else:
print(f"- Warning: Found invalid skill index {index}")
else:
print("No target skills identified from the list.")
print("----------------------------------------\n")
================================================
FILE: cookbook/pocketflow-structured-output/requirements.txt
================================================
pocketflow>=0.0.1
openai>=1.0.0
================================================
FILE: cookbook/pocketflow-structured-output/utils.py
================================================
import os
from openai import OpenAI
def call_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
# Example usage
if __name__ == "__main__":
print(call_llm("Tell me a short joke"))
================================================
FILE: cookbook/pocketflow-supervisor/README.md
================================================
# Research Supervisor
This project demonstrates a supervisor that oversees an unreliable [research agent](../pocketflow-agent) to ensure high-quality answers.
## Features
- Evaluates responses for quality and relevance
- Rejects nonsensical or unreliable answers
- Requests new answers until a quality response is produced
## Getting Started
1. Install the packages you need with this simple command:
```bash
pip install -r requirements.txt
```
2. Let's get your OpenAI API key ready:
```bash
export OPENAI_API_KEY="your-api-key-here"
```
3. Let's do a quick check to make sure your API key is working properly:
```bash
python utils.py
```
This will test both the LLM call and web search features. If you see responses, you're good to go!
4. Try out the agent with the default question (about Nobel Prize winners):
```bash
python main.py
```
5. Got a burning question? Ask anything you want by using the `--` prefix:
```bash
python main.py --"What is quantum computing?"
```
## How It Works?
The magic happens through a simple but powerful graph structure with these main components:
```mermaid
graph TD
subgraph InnerAgent[Inner Research Agent]
DecideAction -->|"search"| SearchWeb
DecideAction -->|"answer"| UnreliableAnswerNode
SearchWeb -->|"decide"| DecideAction
end
InnerAgent --> SupervisorNode
SupervisorNode -->|"retry"| InnerAgent
```
Here's what each part does:
1. **DecideAction**: The brain that figures out whether to search or answer based on current context
2. **SearchWeb**: The researcher that goes out and finds information using web search
3. **UnreliableAnswerNode**: Generates answers (with a 50% chance of being unreliable)
4. **SupervisorNode**: Quality control that validates answers and rejects nonsensical ones
## Example Output
```
🤔 Processing question: Who won the Nobel Prize in Physics 2024?
🤔 Agent deciding what to do next...
🔍 Agent decided to search for: Nobel Prize in Physics 2024 winner
🌐 Searching the web for: Nobel Prize in Physics 2024 winner
📚 Found information, analyzing results...
🤔 Agent deciding what to do next...
💡 Agent decided to answer the question
🤪 Generating unreliable dummy answer...
✅ Answer generated successfully
🔍 Supervisor checking answer quality...
❌ Supervisor rejected answer: Answer appears to be nonsensical or unhelpful
🤔 Agent deciding what to do next...
💡 Agent decided to answer the question
✍️ Crafting final answer...
✅ Answer generated successfully
🔍 Supervisor checking answer quality...
✅ Supervisor approved answer: Answer appears to be legitimate
🎯 Final Answer:
The Nobel Prize in Physics for 2024 was awarded jointly to John J. Hopfield and Geoffrey Hinton. They were recognized "for foundational discoveries and inventions that enable machine learning with artificial neural networks." Their work has been pivotal in the field of artificial intelligence, specifically in developing the theories and technologies that support machine learning using artificial neural networks. John Hopfield is associated with Princeton University, while Geoffrey Hinton is connected to the University of Toronto. Their achievements have laid essential groundwork for advancements in AI and its widespread application across various domains.
```
## Files
- [`main.py`](./main.py): The starting point - runs the whole show!
- [`flow.py`](./flow.py): Connects everything together into a smart agent with supervision
- [`nodes.py`](./nodes.py): The building blocks that make decisions, take actions, and validate answers
- [`utils.py`](./utils.py): Helper functions for talking to the LLM and searching the web
================================================
FILE: cookbook/pocketflow-supervisor/flow.py
================================================
from pocketflow import Flow
from nodes import DecideAction, SearchWeb, UnreliableAnswerNode, SupervisorNode
def create_agent_inner_flow():
"""
Create the inner research agent flow without supervision.
This flow handles the research cycle:
1. DecideAction node decides whether to search or answer
2. If search, go to SearchWeb node and return to decide
3. If answer, go to UnreliableAnswerNode
Returns:
Flow: A research agent flow
"""
# Create instances of each node
decide = DecideAction()
search = SearchWeb()
answer = UnreliableAnswerNode()
# Connect the nodes
# If DecideAction returns "search", go to SearchWeb
decide - "search" >> search
# If DecideAction returns "answer", go to UnreliableAnswerNode
decide - "answer" >> answer
# After SearchWeb completes and returns "decide", go back to DecideAction
search - "decide" >> decide
# Create and return the inner flow, starting with the DecideAction node
return Flow(start=decide)
def create_agent_flow():
"""
Create a supervised agent flow by treating the entire agent flow as a node
and placing the supervisor outside of it.
The flow works like this:
1. Inner agent flow does research and generates an answer
2. SupervisorNode checks if the answer is valid
3. If answer is valid, flow completes
4. If answer is invalid, restart the inner agent flow
Returns:
Flow: A complete research agent flow with supervision
"""
# Create the inner flow
agent_flow = create_agent_inner_flow()
# Create the supervisor node
supervisor = SupervisorNode()
# Connect the components
# After agent_flow completes, go to supervisor
agent_flow >> supervisor
# If supervisor rejects the answer, go back to agent_flow
supervisor - "retry" >> agent_flow
# Create and return the outer flow, starting with the agent_flow
return Flow(start=agent_flow)
================================================
FILE: cookbook/pocketflow-supervisor/main.py
================================================
import sys
from flow import create_agent_flow
def main():
"""Simple function to process a question with supervised answers."""
# Default question
default_question = "Who won the Nobel Prize in Physics 2024?"
# Get question from command line if provided with --
question = default_question
for arg in sys.argv[1:]:
if arg.startswith("--"):
question = arg[2:]
break
# Create the agent flow with supervision
agent_flow = create_agent_flow()
# Process the question
shared = {"question": question}
print(f"🤔 Processing question: {question}")
agent_flow.run(shared)
print("\n🎯 Final Answer:")
print(shared.get("answer", "No answer found"))
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-supervisor/nodes.py
================================================
from pocketflow import Node
from utils import call_llm, search_web
import yaml
import random
class DecideAction(Node):
def prep(self, shared):
"""Prepare the context and question for the decision-making process."""
# Get the current context (default to "No previous search" if none exists)
context = shared.get("context", "No previous search")
# Get the question from the shared store
question = shared["question"]
# Return both for the exec step
return question, context
def exec(self, inputs):
"""Call the LLM to decide whether to search or answer."""
question, context = inputs
print(f"🤔 Agent deciding what to do next...")
# Create a prompt to help the LLM decide what to do next
prompt = f"""
### CONTEXT
You are a research assistant that can search the web.
Question: {question}
Previous Research: {context}
### ACTION SPACE
[1] search
Description: Look up more information on the web
Parameters:
- query (str): What to search for
[2] answer
Description: Answer the question with current knowledge
Parameters:
- answer (str): Final answer to the question
## NEXT ACTION
Decide the next action based on the context and available actions.
Return your response in this format:
```yaml
thinking: |
action: search OR answer
reason:
search_query:
```"""
# Call the LLM to make a decision
response = call_llm(prompt)
# Parse the response to get the decision
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
decision = yaml.safe_load(yaml_str)
return decision
def post(self, shared, prep_res, exec_res):
"""Save the decision and determine the next step in the flow."""
# If LLM decided to search, save the search query
if exec_res["action"] == "search":
shared["search_query"] = exec_res["search_query"]
print(f"🔍 Agent decided to search for: {exec_res['search_query']}")
else:
print(f"💡 Agent decided to answer the question")
# Return the action to determine the next node in the flow
return exec_res["action"]
class SearchWeb(Node):
def prep(self, shared):
"""Get the search query from the shared store."""
return shared["search_query"]
def exec(self, search_query):
"""Search the web for the given query."""
# Call the search utility function
print(f"🌐 Searching the web for: {search_query}")
results = search_web(search_query)
return results
def post(self, shared, prep_res, exec_res):
"""Save the search results and go back to the decision node."""
# Add the search results to the context in the shared store
previous = shared.get("context", "")
shared["context"] = previous + "\n\nSEARCH: " + shared["search_query"] + "\nRESULTS: " + exec_res
print(f"📚 Found information, analyzing results...")
# Always go back to the decision node after searching
return "decide"
class UnreliableAnswerNode(Node):
def prep(self, shared):
"""Get the question and context for answering."""
return shared["question"], shared.get("context", "")
def exec(self, inputs):
"""Call the LLM to generate a final answer with 50% chance of returning a dummy answer."""
question, context = inputs
# 50% chance to return a dummy answer
if random.random() < 0.5:
print(f"🤪 Generating unreliable dummy answer...")
return "Sorry, I'm on a coffee break right now. All information I provide is completely made up anyway. The answer to your question is 42, or maybe purple unicorns. Who knows? Certainly not me!"
print(f"✍️ Crafting final answer...")
# Create a prompt for the LLM to answer the question
prompt = f"""
### CONTEXT
Based on the following information, answer the question.
Question: {question}
Research: {context}
## YOUR ANSWER:
Provide a comprehensive answer using the research results.
"""
# Call the LLM to generate an answer
answer = call_llm(prompt)
return answer
def post(self, shared, prep_res, exec_res):
"""Save the final answer and complete the flow."""
# Save the answer in the shared store
shared["answer"] = exec_res
print(f"✅ Answer generated successfully")
class SupervisorNode(Node):
def prep(self, shared):
"""Get the current answer for evaluation."""
return shared["answer"]
def exec(self, answer):
"""Check if the answer is valid or nonsensical."""
print(f" 🔍 Supervisor checking answer quality...")
# Check for obvious markers of the nonsense answers
nonsense_markers = [
"coffee break",
"purple unicorns",
"made up",
"42",
"Who knows?"
]
# Check if the answer contains any nonsense markers
is_nonsense = any(marker in answer for marker in nonsense_markers)
if is_nonsense:
return {"valid": False, "reason": "Answer appears to be nonsensical or unhelpful"}
else:
return {"valid": True, "reason": "Answer appears to be legitimate"}
def post(self, shared, prep_res, exec_res):
"""Decide whether to accept the answer or restart the process."""
if exec_res["valid"]:
print(f" ✅ Supervisor approved answer: {exec_res['reason']}")
else:
print(f" ❌ Supervisor rejected answer: {exec_res['reason']}")
# Clean up the bad answer
shared["answer"] = None
# Add a note about the rejected answer
context = shared.get("context", "")
shared["context"] = context + "\n\nNOTE: Previous answer attempt was rejected by supervisor."
return "retry"
================================================
FILE: cookbook/pocketflow-supervisor/requirements.txt
================================================
pocketflow>=0.0.1
aiohttp>=3.8.0 # For async HTTP requests
openai>=1.0.0 # For async LLM calls
duckduckgo-search>=7.5.2 # For web search
================================================
FILE: cookbook/pocketflow-supervisor/utils.py
================================================
from openai import OpenAI
import os
from duckduckgo_search import DDGS
def call_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
def search_web(query):
results = DDGS().text(query, max_results=5)
# Convert results to a string
results_str = "\n\n".join([f"Title: {r['title']}\nURL: {r['href']}\nSnippet: {r['body']}" for r in results])
return results_str
if __name__ == "__main__":
print("## Testing call_llm")
prompt = "In a few words, what is the meaning of life?"
print(f"## Prompt: {prompt}")
response = call_llm(prompt)
print(f"## Response: {response}")
print("## Testing search_web")
query = "Who won the Nobel Prize in Physics 2024?"
print(f"## Query: {query}")
results = search_web(query)
print(f"## Results: {results}")
================================================
FILE: cookbook/pocketflow-tao/README.md
================================================
# PocketFlow TAO (Thought-Action-Observation)
A powerful pattern that enables AI agents to solve complex problems through structured thinking, action execution, and result observation. This example demonstrates how to implement the TAO pattern using PocketFlow.
## Project Structure
```
.
├── flow.py # PocketFlow implementation of TAO pattern
├── main.py # Main application entry point
├── nodes.py # TAO node definitions
├── requirements.txt # Project dependencies
└── README.md # Project documentation
```
## Overview
The TAO pattern consists of three key steps:
1. **Thought**: The agent deeply analyzes the problem and forms a solution strategy
2. **Action**: Concrete actions are executed based on the thinking
3. **Observation**: Results are evaluated and feedback is gathered
This cycle continues until the problem is solved or termination conditions are met.
## Setup
1. Create a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Set API key (if using specific LLM services):
```bash
export OPENAI_API_KEY="your-api-key-here"
# Or set in code
```
## How to Run
Execute the example:
```bash
python main.py
```
## How It Works
The TAO pattern is implemented as a flow in PocketFlow, with each step handled by specialized nodes:
```mermaid
graph TD
Problem[Problem Input] --> ThoughtNode
ThoughtNode[Thought Node] --> ActionNode[Action Node]
ActionNode --> ObservationNode[Observation Node]
ObservationNode --> DecisionNode{Problem Solved?}
DecisionNode -->|Yes| Solution[Solution]
DecisionNode -->|No| ThoughtNode
```
Each TAO cycle generates new insights for the problem-solving process, allowing the AI to iteratively approach an optimal solution.
## Use Cases
- Complex problem solving
- Multi-step reasoning tasks
- Projects requiring iterative improvement
- Reinforcement learning-style AI applications
## Example Output
```
Query: I need to understand the latest developments in artificial intelligence
🤔 Thought 1: Decided to execute search
🚀 Executing action: search, input: latest developments in artificial intelligence 2023
✅ Action completed, result obtained
👁️ Observation: The search result indicates that information was r...
🎯 Final Answer: As of October 2023, some of the latest developments in artificial intelligence include advances in large language models like GPT-4, increased focus on AI alignment and safety, improvements in reinforcement learning, and the integration of AI into more industries such as healthcare, finance, and autonomous vehicles. Researchers are also exploring ethical considerations and regulatory frameworks to ensure responsible AI deployment. For the most current updates beyond this date, I recommend checking recent publications, official AI research organization releases, or news sources specializing in technology.
Flow ended, thank you for using!
Final Answer:
As of October 2023, some of the latest developments in artificial intelligence include advances in large language models like GPT-4, increased focus on AI alignment and safety, improvements in reinforcement learning, and the integration of AI into more industries such as healthcare, finance, and autonomous vehicles. Researchers are also exploring ethical considerations and regulatory frameworks to ensure responsible AI deployment. For the most current updates beyond this date, I recommend checking recent publications, official AI research organization releases, or news sources specializing in technology.
```
## Advanced Usage
The TAO pattern can be extended by:
- Adding memory components to store past thoughts and observations.
- Implementing adaptive action selection strategies.
- Integrating external tools and APIs.
- Adding human feedback loops.
- Adding max attempt to control the iteration.
## Additional Resources
- [PocketFlow Documentation](https://the-pocket.github.io/PocketFlow/)
- [Understanding AI Agents through the Thought-Action-Observation Cycle](https://huggingface.co/learn/agents-course/en/unit1/agent-steps-and-structure)
================================================
FILE: cookbook/pocketflow-tao/flow.py
================================================
# flow.py
from pocketflow import Flow
from nodes import ThinkNode, ActionNode, ObserveNode, EndNode
def create_tao_flow():
"""
Create a Thought-Action-Observation loop flow
How the flow works:
1. ThinkNode decides the next action
2. ActionNode executes the action
3. ObserveNode observes the action result
4. Return to ThinkNode to continue thinking, or end the flow
Returns:
Flow: Complete TAO loop flow
"""
# Create node instances
think = ThinkNode()
action = ActionNode()
observe = ObserveNode()
end = EndNode()
# Connect nodes
# If ThinkNode returns "action", go to ActionNode
think - "action" >> action
# If ThinkNode returns "end", end the flow
think - "end" >> end
# After ActionNode completes, go to ObserveNode
action - "observe" >> observe
# After ObserveNode completes, return to ThinkNode
observe - "think" >> think
# Create and return flow, starting from ThinkNode
return Flow(start=think)
================================================
FILE: cookbook/pocketflow-tao/main.py
================================================
# main.py
from flow import create_tao_flow
def main():
query = """I need to understand the latest developments in artificial intelligence"""
# Create shared data
shared = {
"query": query,
"thoughts": [],
"observations": [],
"current_thought_number": 0
}
# Create and run flow
tao_flow = create_tao_flow()
tao_flow.run(shared)
# Print final result
if "final_answer" in shared:
print("\nFinal Answer:")
print(shared["final_answer"])
else:
print("\nFlow did not produce a final answer")
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-tao/nodes.py
================================================
# nodes.py
from pocketflow import Node
import yaml
from utils import call_llm
class ThinkNode(Node):
def prep(self, shared):
"""Prepare the context needed for thinking"""
query = shared.get("query", "")
observations = shared.get("observations", [])
thoughts = shared.get("thoughts", [])
current_thought_number = shared.get("current_thought_number", 0)
# Update thought count
shared["current_thought_number"] = current_thought_number + 1
# Format previous observations
observations_text = "\n".join([f"Observation {i+1}: {obs}" for i, obs in enumerate(observations)])
if not observations_text:
observations_text = "No observations yet."
return {
"query": query,
"observations_text": observations_text,
"thoughts": thoughts,
"current_thought_number": current_thought_number + 1
}
def exec(self, prep_res):
"""Execute the thinking process, decide the next action"""
query = prep_res["query"]
observations_text = prep_res["observations_text"]
current_thought_number = prep_res["current_thought_number"]
# Build the prompt
prompt = f"""
You are an AI assistant solving a problem. Based on the user's query and previous observations, think about what action to take next.
User query: {query}
Previous observations:
{observations_text}
Please think about the next action and return your thinking process and decision in YAML format:
```yaml
thinking: |
action:
action_input:
is_final:
```
"""
# Call LLM to get thinking result
response = call_llm(prompt)
# Parse YAML response
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
thought_data = yaml.safe_load(yaml_str)
# Add thought number
thought_data["thought_number"] = current_thought_number
return thought_data
def post(self, shared, prep_res, exec_res):
"""Save the thinking result and decide the next step in the flow"""
# Save thinking result
if "thoughts" not in shared:
shared["thoughts"] = []
shared["thoughts"].append(exec_res)
# Save action information
shared["current_action"] = exec_res["action"]
shared["current_action_input"] = exec_res["action_input"]
# If it's the final answer, end the flow
if exec_res.get("is_final", False):
shared["final_answer"] = exec_res["action_input"]
print(f"🎯 Final Answer: {exec_res['action_input']}")
return "end"
# Otherwise continue with the action
print(f"🤔 Thought {exec_res['thought_number']}: Decided to execute {exec_res['action']}")
return "action"
class ActionNode(Node):
def prep(self, shared):
"""Prepare to execute action"""
action = shared["current_action"]
action_input = shared["current_action_input"]
return action, action_input
def exec(self, inputs):
"""Execute action and return result"""
action, action_input = inputs
print(f"🚀 Executing action: {action}, input: {action_input}")
# Execute different operations based on action type
if action == "search":
# Simulate search operation
result = self.search_web(action_input)
elif action == "calculate":
# Simulate calculation operation
result = self.calculate(action_input)
elif action == "answer":
# Direct return answer
result = action_input
else:
# Unknown action type
result = f"Unknown action type: {action}"
return result
def post(self, shared, prep_res, exec_res):
"""Save action result"""
# Save the current action result
shared["current_action_result"] = exec_res
print(f"✅ Action completed, result obtained")
# Continue to observation node
return "observe"
# Simulated tool functions
def search_web(self, query):
# This should be actual search logic
return f"Search results: Information about '{query}'..."
def calculate(self, expression):
# This should be actual calculation logic
try:
return f"Calculation result: {eval(expression)}"
except:
return f"Unable to calculate expression: {expression}"
class ObserveNode(Node):
def prep(self, shared):
"""Prepare observation data"""
action = shared["current_action"]
action_input = shared["current_action_input"]
action_result = shared["current_action_result"]
return action, action_input, action_result
def exec(self, inputs):
"""Analyze action results, generate observation"""
action, action_input, action_result = inputs
# Build prompt
prompt = f"""
You are an observer, needing to analyze action results and provide objective observations.
Action: {action}
Action input: {action_input}
Action result: {action_result}
Please provide a concise observation of this result. Don't make decisions, just describe what you see.
"""
# Call LLM to get observation result
observation = call_llm(prompt)
print(f"👁️ Observation: {observation[:50]}...")
return observation
def post(self, shared, prep_res, exec_res):
"""Save observation result and decide next flow step"""
# Save observation result
if "observations" not in shared:
shared["observations"] = []
shared["observations"].append(exec_res)
# Continue thinking
return "think"
class EndNode(Node):
def prep(self, shared):
"""Prepare end node"""
return {}
def exec(self, prep_res):
"""Execute end operation"""
print("Flow ended, thank you for using!")
return None
def post(self, shared, prep_res, exec_res):
"""End flow"""
return None
================================================
FILE: cookbook/pocketflow-tao/utils.py
================================================
# utils.py
from openai import OpenAI
import os
def call_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "Your Key Here"),base_url=os.environ.get("OPENAI_API_BASE", "Your API Base Here"))
r = client.chat.completions.create(
model=os.environ.get("OPENAI_MODEL", "openai/gpt-4.1-nano"),
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
if __name__ == "__main__":
print("## Testing call_llm")
prompt = "In a few words, what is the meaning of life?"
print(f"## Prompt: {prompt}")
response = call_llm(prompt)
print(f"## Response: {response}")
================================================
FILE: cookbook/pocketflow-text2sql/README.md
================================================
# Text-to-SQL Workflow
A PocketFlow example demonstrating a text-to-SQL workflow that converts natural language questions into executable SQL queries for an SQLite database, including an LLM-powered debugging loop for failed queries.
- Check out the [Substack Post Tutorial](https://zacharyhuang.substack.com/p/text-to-sql-from-scratch-tutorial) for more!
## Features
- **Schema Awareness**: Automatically retrieves the database schema to provide context to the LLM.
- **LLM-Powered SQL Generation**: Uses an LLM (GPT-4o) to translate natural language questions into SQLite queries (using YAML structured output).
- **Automated Debugging Loop**: If SQL execution fails, an LLM attempts to correct the query based on the error message. This process repeats up to a configurable number of times.
## Getting Started
1. **Install Packages:**
```bash
pip install -r requirements.txt
```
2. **Set API Key:**
Set the environment variable for your OpenAI API key.
```bash
export OPENAI_API_KEY="your-api-key-here"
```
*(Replace `"your-api-key-here"` with your actual key)*
3. **Verify API Key (Optional):**
Run a quick check using the utility script. If successful, it will print a short joke.
```bash
python utils.py
```
*(Note: This requires a valid API key to be set.)*
4. **Run Default Example:**
Execute the main script. This will create the sample `ecommerce.db` if it doesn't exist and run the workflow with a default query.
```bash
python main.py
```
The default query is:
> Show me the names and email addresses of customers from New York
5. **Run Custom Query:**
Provide your own natural language query as command-line arguments after the script name.
```bash
python main.py What is the total stock quantity for products in the 'Accessories' category?
```
Or, for queries with spaces, ensure they are treated as a single argument by the shell if necessary (quotes might help depending on your shell):
```bash
python main.py "List orders placed in the last 30 days with status 'shipped'"
```
## How It Works
The workflow uses several nodes connected in a sequence, with a loop for debugging failed SQL queries.
```mermaid
graph LR
A[Get Schema] --> B[Generate SQL]
B --> C[Execute SQL]
C -- Success --> E[End]
C -- SQLite Error --> D{Debug SQL Attempt}
D -- Corrected SQL --> C
C -- Max Retries Reached --> F[End with Error]
style E fill:#dff,stroke:#333,stroke-width:2px
style F fill:#fdd,stroke:#333,stroke-width:2px
```
**Node Descriptions:**
1. **`GetSchema`**: Connects to the SQLite database (`ecommerce.db` by default) and extracts the schema (table names and columns).
2. **`GenerateSQL`**: Takes the natural language query and the database schema, prompts the LLM to generate an SQLite query (expecting YAML output with the SQL), and parses the result.
3. **`ExecuteSQL`**: Attempts to run the generated SQL against the database.
* If successful, the results are stored, and the flow ends successfully.
* If an `sqlite3.Error` occurs (e.g., syntax error), it captures the error message and triggers the debug loop.
4. **`DebugSQL`**: If `ExecuteSQL` failed, this node takes the original query, schema, failed SQL, and error message, prompts the LLM to generate a *corrected* SQL query (again, expecting YAML).
5. **(Loop)**: The corrected SQL from `DebugSQL` is passed back to `ExecuteSQL` for another attempt.
6. **(End Conditions)**: The loop continues until `ExecuteSQL` succeeds or the maximum number of debug attempts (default: 3) is reached.
## Files
- [`main.py`](./main.py): Main entry point to run the workflow. Handles command-line arguments for the query.
- [`flow.py`](./flow.py): Defines the PocketFlow `Flow` connecting the different nodes, including the debug loop logic.
- [`nodes.py`](./nodes.py): Contains the `Node` classes for each step (`GetSchema`, `GenerateSQL`, `ExecuteSQL`, `DebugSQL`).
- [`utils.py`](./utils.py): Contains the minimal `call_llm` utility function.
- [`populate_db.py`](./populate_db.py): Script to create and populate the sample `ecommerce.db` SQLite database.
- [`requirements.txt`](./requirements.txt): Lists Python package dependencies.
- [`README.md`](./README.md): This file.
## Example Output (Successful Run)
```
=== Starting Text-to-SQL Workflow ===
Query: 'total products per category'
Database: ecommerce.db
Max Debug Retries on SQL Error: 3
=============================================
===== DB SCHEMA =====
Table: customers
- customer_id (INTEGER)
- first_name (TEXT)
- last_name (TEXT)
- email (TEXT)
- registration_date (DATE)
- city (TEXT)
- country (TEXT)
Table: sqlite_sequence
- name ()
- seq ()
Table: products
- product_id (INTEGER)
- name (TEXT)
- description (TEXT)
- category (TEXT)
- price (REAL)
- stock_quantity (INTEGER)
Table: orders
- order_id (INTEGER)
- customer_id (INTEGER)
- order_date (TIMESTAMP)
- status (TEXT)
- total_amount (REAL)
- shipping_address (TEXT)
Table: order_items
- order_item_id (INTEGER)
- order_id (INTEGER)
- product_id (INTEGER)
- quantity (INTEGER)
- price_per_unit (REAL)
=====================
===== GENERATED SQL (Attempt 1) =====
SELECT category, COUNT(*) AS total_products
FROM products
GROUP BY category
====================================
SQL executed in 0.000 seconds.
===== SQL EXECUTION SUCCESS =====
category | total_products
-------------------------
Accessories | 3
Apparel | 1
Electronics | 3
Home Goods | 2
Sports | 1
=== Workflow Completed Successfully ===
====================================
```
================================================
FILE: cookbook/pocketflow-text2sql/docs/design.md
================================================
# Design Doc: Text-to-SQL Agent
> Please DON'T remove notes for AI
## Requirements
> Notes for AI: Keep it simple and clear.
> If the requirements are abstract, write concrete user stories
The system should take a natural language query and a path to an SQLite database as input. It should then:
1. Extract the schema from the database.
2. Generate an SQL query based on the natural language query and the schema.
3. Execute the SQL query against the database.
4. If the SQL execution fails, attempt to debug and retry the SQL generation and execution up to a specified maximum number of attempts.
5. Return the final results of the SQL query or an error message if the process fails.
## Flow Design
> Notes for AI:
> 1. Consider the design patterns of agent, map-reduce, rag, and workflow. Apply them if they fit.
> 2. Present a concise, high-level description of the workflow.
### Applicable Design Pattern:
The primary design pattern is a **Workflow** with an embedded **Agent**-like behavior for debugging.
- **Workflow**: The process follows a sequence: Get Schema -> Generate SQL -> Execute SQL.
- **Agent (for Debugging)**: If `ExecuteSQL` fails, the `DebugSQL` node acts like an agent, taking the error and previous SQL as context to generate a revised SQL query. This forms a loop back to `ExecuteSQL`.
### Flow high-level Design:
1. **`GetSchema`**: Retrieves the database schema.
2. **`GenerateSQL`**: Generates an SQL query from a natural language question and the schema.
3. **`ExecuteSQL`**: Executes the generated SQL. If successful, the flow ends. If an error occurs, it transitions to `DebugSQL`.
4. **`DebugSQL`**: Attempts to correct the failed SQL query based on the error message. It then transitions back to `ExecuteSQL` to try the corrected query.
```mermaid
flowchart TD
A[GetSchema] --> B[GenerateSQL]
B --> C{ExecuteSQL}
C -- Success --> D[End]
C -- Error --> E[DebugSQL]
E --> C
```
## Utility Functions
> Notes for AI:
> 1. Understand the utility function definition thoroughly by reviewing the doc.
> 2. Include only the necessary utility functions, based on nodes in the flow.
1. **Call LLM** (`utils/call_llm.py`)
* *Input*: `prompt` (str)
* *Output*: `response` (str)
* *Necessity*: Used by `GenerateSQL` and `DebugSQL` nodes to interact with the language model for SQL generation and correction.
*Database interaction (e.g., `sqlite3.connect`, `cursor.execute`) is handled directly within the nodes and is not abstracted into separate utility functions in this implementation.*
## Node Design
### Shared Store
> Notes for AI: Try to minimize data redundancy
The shared store structure is organized as follows:
```python
shared = {
"db_path": "path/to/database.db", # Input: Path to the SQLite database
"natural_query": "User's question", # Input: Natural language query from the user
"max_debug_attempts": 3, # Input: Max retries for the debug loop
"schema": None, # Output of GetSchema: String representation of DB schema
"generated_sql": None, # Output of GenerateSQL/DebugSQL: The SQL query string
"execution_error": None, # Output of ExecuteSQL (on failure): Error message
"debug_attempts": 0, # Internal: Counter for debug attempts
"final_result": None, # Output of ExecuteSQL (on success): Query results
"result_columns": None, # Output of ExecuteSQL (on success): Column names for results
"final_error": None # Output: Overall error message if flow fails after retries
}
```
### Node Steps
> Notes for AI: Carefully decide whether to use Batch/Async Node/Flow.
1. **`GetSchema`**
* *Purpose*: To extract and store the schema of the target SQLite database.
* *Type*: Regular
* *Steps*:
* *`prep`*: Reads `db_path` from the shared store.
* *`exec`*: Connects to the SQLite database, inspects `sqlite_master` and `PRAGMA table_info` to build a string representation of all tables and their columns.
* *`post`*: Writes the extracted `schema` string to the shared store.
2. **`GenerateSQL`**
* *Purpose*: To generate an SQL query based on the user's natural language query and the database schema.
* *Type*: Regular
* *Steps*:
* *`prep`*: Reads `natural_query` and `schema` from the shared store.
* *`exec`*: Constructs a prompt for the LLM, including the schema and the natural language query, asking for an SQL query in YAML format. Calls the `call_llm` utility. Parses the YAML response to extract the SQL query.
* *`post`*: Writes the `generated_sql` to the shared store. Resets `debug_attempts` to 0.
3. **`ExecuteSQL`**
* *Purpose*: To execute the generated SQL query against the database and handle results or errors.
* *Type*: Regular
* *Steps*:
* *`prep`*: Reads `db_path` and `generated_sql` from the shared store.
* *`exec`*: Connects to the SQLite database and executes the `generated_sql`. It determines if the query is a SELECT or an DML/DDL statement to fetch results or commit changes. Returns a tuple `(success_boolean, result_or_error_message, column_names_list)`.
* *`post`*:
* If successful: Stores `final_result` and `result_columns` in the shared store. Returns no action (ends the flow path).
* If failed: Stores `execution_error` in the shared store. Increments `debug_attempts`. If `debug_attempts` is less than `max_debug_attempts`, returns `"error_retry"` action to trigger the `DebugSQL` node. Otherwise, sets `final_error` and returns no action.
4. **`DebugSQL`**
* *Purpose*: To attempt to correct a failed SQL query using LLM based on the error message.
* *Type*: Regular
* *Steps*:
* *`prep`*: Reads `natural_query`, `schema`, `generated_sql` (the failed one), and `execution_error` from the shared store.
* *`exec`*: Constructs a prompt for the LLM, providing the failed SQL, the original query, the schema, and the error message, asking for a corrected SQL query in YAML format. Calls the `call_llm` utility. Parses the YAML response to extract the corrected SQL query.
* *`post`*: Overwrites `generated_sql` in the shared store with the corrected SQL. Removes `execution_error` from the shared store. Returns a default action to go back to `ExecuteSQL`.
================================================
FILE: cookbook/pocketflow-text2sql/flow.py
================================================
from pocketflow import Flow, Node
from nodes import GetSchema, GenerateSQL, ExecuteSQL, DebugSQL
def create_text_to_sql_flow():
"""Creates the text-to-SQL workflow with a debug loop."""
get_schema_node = GetSchema()
generate_sql_node = GenerateSQL()
execute_sql_node = ExecuteSQL()
debug_sql_node = DebugSQL()
# Define the main flow sequence using the default transition operator
get_schema_node >> generate_sql_node >> execute_sql_node
# --- Define the debug loop connections using the correct operator ---
# If ExecuteSQL returns "error_retry", go to DebugSQL
execute_sql_node - "error_retry" >> debug_sql_node
# If DebugSQL returns "default", go back to ExecuteSQL
# debug_sql_node - "default" >> execute_sql_node # Explicitly for "default"
# OR using the shorthand for default:
debug_sql_node >> execute_sql_node
# Create the flow
text_to_sql_flow = Flow(start=get_schema_node)
return text_to_sql_flow
================================================
FILE: cookbook/pocketflow-text2sql/main.py
================================================
import sys
import os
from flow import create_text_to_sql_flow
from populate_db import populate_database, DB_FILE
def run_text_to_sql(natural_query, db_path=DB_FILE, max_debug_retries=3):
if not os.path.exists(db_path) or os.path.getsize(db_path) == 0:
print(f"Database at {db_path} missing or empty. Populating...")
populate_database(db_path)
shared = {
"db_path": db_path,
"natural_query": natural_query,
"max_debug_attempts": max_debug_retries,
"debug_attempts": 0,
"final_result": None,
"final_error": None
}
print(f"\n=== Starting Text-to-SQL Workflow ===")
print(f"Query: '{natural_query}'")
print(f"Database: {db_path}")
print(f"Max Debug Retries on SQL Error: {max_debug_retries}")
print("=" * 45)
flow = create_text_to_sql_flow()
flow.run(shared) # Let errors inside the loop be handled by the flow logic
# Check final state based on shared data
if shared.get("final_error"):
print("\n=== Workflow Completed with Error ===")
print(f"Error: {shared['final_error']}")
elif shared.get("final_result") is not None:
print("\n=== Workflow Completed Successfully ===")
# Result already printed by ExecuteSQL node
else:
# Should not happen if flow logic is correct and covers all end states
print("\n=== Workflow Completed (Unknown State) ===")
print("=" * 36)
return shared
if __name__ == "__main__":
if len(sys.argv) > 1:
query = " ".join(sys.argv[1:])
else:
query = "total products per category"
run_text_to_sql(query)
================================================
FILE: cookbook/pocketflow-text2sql/nodes.py
================================================
import sqlite3
import time
import yaml # Import yaml here as nodes use it
from pocketflow import Node
from utils.call_llm import call_llm
class GetSchema(Node):
def prep(self, shared):
return shared["db_path"]
def exec(self, db_path):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = cursor.fetchall()
schema = []
for table_name_tuple in tables:
table_name = table_name_tuple[0]
schema.append(f"Table: {table_name}")
cursor.execute(f"PRAGMA table_info({table_name});")
columns = cursor.fetchall()
for col in columns:
schema.append(f" - {col[1]} ({col[2]})")
schema.append("")
conn.close()
return "\n".join(schema).strip()
def post(self, shared, prep_res, exec_res):
shared["schema"] = exec_res
print("\n===== DB SCHEMA =====\n")
print(exec_res)
print("\n=====================\n")
# return "default"
class GenerateSQL(Node):
def prep(self, shared):
return shared["natural_query"], shared["schema"]
def exec(self, prep_res):
natural_query, schema = prep_res
prompt = f"""
Given SQLite schema:
{schema}
Question: "{natural_query}"
Respond ONLY with a YAML block containing the SQL query under the key 'sql':
```yaml
sql: |
SELECT ...
```"""
llm_response = call_llm(prompt)
yaml_str = llm_response.split("```yaml")[1].split("```")[0].strip()
structured_result = yaml.safe_load(yaml_str)
sql_query = structured_result["sql"].strip().rstrip(';')
return sql_query
def post(self, shared, prep_res, exec_res):
# exec_res is now the parsed SQL query string
shared["generated_sql"] = exec_res
# Reset debug attempts when *successfully* generating new SQL
shared["debug_attempts"] = 0
print(f"\n===== GENERATED SQL (Attempt {shared.get('debug_attempts', 0) + 1}) =====\n")
print(exec_res)
print("\n====================================\n")
# return "default"
class ExecuteSQL(Node):
def prep(self, shared):
return shared["db_path"], shared["generated_sql"]
def exec(self, prep_res):
db_path, sql_query = prep_res
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
start_time = time.time()
cursor.execute(sql_query)
is_select = sql_query.strip().upper().startswith(("SELECT", "WITH"))
if is_select:
results = cursor.fetchall()
column_names = [desc[0] for desc in cursor.description] if cursor.description else []
else:
conn.commit()
results = f"Query OK. Rows affected: {cursor.rowcount}"
column_names = []
conn.close()
duration = time.time() - start_time
print(f"SQL executed in {duration:.3f} seconds.")
return (True, results, column_names)
except sqlite3.Error as e:
print(f"SQLite Error during execution: {e}")
if 'conn' in locals() and conn:
try:
conn.close()
except Exception:
pass
return (False, str(e), [])
def post(self, shared, prep_res, exec_res):
success, result_or_error, column_names = exec_res
if success:
shared["final_result"] = result_or_error
shared["result_columns"] = column_names
print("\n===== SQL EXECUTION SUCCESS =====\n")
# (Same result printing logic as before)
if isinstance(result_or_error, list):
if column_names: print(" | ".join(column_names)); print("-" * (sum(len(str(c)) for c in column_names) + 3 * (len(column_names) -1)))
if not result_or_error: print("(No results found)")
else:
for row in result_or_error: print(" | ".join(map(str, row)))
else: print(result_or_error)
print("\n=================================\n")
return
else:
# Execution failed (SQLite error caught in exec)
shared["execution_error"] = result_or_error # Store the error message
shared["debug_attempts"] = shared.get("debug_attempts", 0) + 1
max_attempts = shared.get("max_debug_attempts", 3) # Get max attempts from shared
print(f"\n===== SQL EXECUTION FAILED (Attempt {shared['debug_attempts']}) =====\n")
print(f"Error: {shared['execution_error']}")
print("=========================================\n")
if shared["debug_attempts"] >= max_attempts:
print(f"Max debug attempts ({max_attempts}) reached. Stopping.")
shared["final_error"] = f"Failed to execute SQL after {max_attempts} attempts. Last error: {shared['execution_error']}"
return
else:
print("Attempting to debug the SQL...")
return "error_retry" # Signal to go to DebugSQL
class DebugSQL(Node):
def prep(self, shared):
return (
shared.get("natural_query"),
shared.get("schema"),
shared.get("generated_sql"),
shared.get("execution_error")
)
def exec(self, prep_res):
natural_query, schema, failed_sql, error_message = prep_res
prompt = f"""
The following SQLite SQL query failed:
```sql
{failed_sql}
```
It was generated for: "{natural_query}"
Schema:
{schema}
Error: "{error_message}"
Provide a corrected SQLite query.
Respond ONLY with a YAML block containing the corrected SQL under the key 'sql':
```yaml
sql: |
SELECT ... -- corrected query
```"""
llm_response = call_llm(prompt)
yaml_str = llm_response.split("```yaml")[1].split("```")[0].strip()
structured_result = yaml.safe_load(yaml_str)
corrected_sql = structured_result["sql"].strip().rstrip(';')
return corrected_sql
def post(self, shared, prep_res, exec_res):
# exec_res is the corrected SQL string
shared["generated_sql"] = exec_res # Overwrite with the new attempt
shared.pop("execution_error", None) # Clear the previous error for the next ExecuteSQL attempt
print(f"\n===== REVISED SQL (Attempt {shared.get('debug_attempts', 0) + 1}) =====\n")
print(exec_res)
print("\n====================================\n")
================================================
FILE: cookbook/pocketflow-text2sql/populate_db.py
================================================
import sqlite3
import os
import random
from datetime import datetime, timedelta
DB_FILE = "ecommerce.db"
def populate_database(db_file=DB_FILE):
"""Creates and populates the SQLite database."""
if os.path.exists(db_file):
os.remove(db_file)
print(f"Removed existing database: {db_file}")
conn = sqlite3.connect(db_file)
cursor = conn.cursor()
# Create Tables
cursor.execute("""
CREATE TABLE customers (
customer_id INTEGER PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
registration_date DATE NOT NULL,
city TEXT,
country TEXT DEFAULT 'USA'
);
""")
print("Created 'customers' table.")
cursor.execute("""
CREATE TABLE products (
product_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL,
price REAL NOT NULL CHECK (price > 0),
stock_quantity INTEGER NOT NULL DEFAULT 0 CHECK (stock_quantity >= 0)
);
""")
print("Created 'products' table.")
cursor.execute("""
CREATE TABLE orders (
order_id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id INTEGER NOT NULL,
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status TEXT NOT NULL CHECK (status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled')),
total_amount REAL,
shipping_address TEXT,
FOREIGN KEY (customer_id) REFERENCES customers (customer_id)
);
""")
print("Created 'orders' table.")
cursor.execute("""
CREATE TABLE order_items (
order_item_id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
price_per_unit REAL NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders (order_id),
FOREIGN KEY (product_id) REFERENCES products (product_id)
);
""")
print("Created 'order_items' table.")
# Insert Sample Data
customers_data = [
('Alice', 'Smith', 'alice.s@email.com', '2023-01-15', 'New York', 'USA'),
('Bob', 'Johnson', 'b.johnson@email.com', '2023-02-20', 'Los Angeles', 'USA'),
('Charlie', 'Williams', 'charlie.w@email.com', '2023-03-10', 'Chicago', 'USA'),
('Diana', 'Brown', 'diana.b@email.com', '2023-04-05', 'Houston', 'USA'),
('Ethan', 'Davis', 'ethan.d@email.com', '2023-05-12', 'Phoenix', 'USA'),
('Fiona', 'Miller', 'fiona.m@email.com', '2023-06-18', 'Philadelphia', 'USA'),
('George', 'Wilson', 'george.w@email.com', '2023-07-22', 'San Antonio', 'USA'),
('Hannah', 'Moore', 'hannah.m@email.com', '2023-08-30', 'San Diego', 'USA'),
('Ian', 'Taylor', 'ian.t@email.com', '2023-09-05', 'Dallas', 'USA'),
('Julia', 'Anderson', 'julia.a@email.com', '2023-10-11', 'San Jose', 'USA')
]
cursor.executemany("INSERT INTO customers (first_name, last_name, email, registration_date, city, country) VALUES (?, ?, ?, ?, ?, ?)", customers_data)
print(f"Inserted {len(customers_data)} customers.")
products_data = [
('Laptop Pro', 'High-end laptop for professionals', 'Electronics', 1200.00, 50),
('Wireless Mouse', 'Ergonomic wireless mouse', 'Accessories', 25.50, 200),
('Mechanical Keyboard', 'RGB backlit mechanical keyboard', 'Accessories', 75.00, 150),
('4K Monitor', '27-inch 4K UHD Monitor', 'Electronics', 350.00, 80),
('Smartphone X', 'Latest generation smartphone', 'Electronics', 999.00, 120),
('Coffee Maker', 'Drip coffee maker', 'Home Goods', 50.00, 300),
('Running Shoes', 'Comfortable running shoes', 'Apparel', 90.00, 250),
('Yoga Mat', 'Eco-friendly yoga mat', 'Sports', 30.00, 400),
('Desk Lamp', 'Adjustable LED desk lamp', 'Home Goods', 45.00, 180),
('Backpack', 'Durable backpack for travel', 'Accessories', 60.00, 220)
]
cursor.executemany("INSERT INTO products (name, description, category, price, stock_quantity) VALUES (?, ?, ?, ?, ?)", products_data)
print(f"Inserted {len(products_data)} products.")
orders_data = []
start_date = datetime.now() - timedelta(days=60)
order_statuses = ['pending', 'processing', 'shipped', 'delivered', 'cancelled']
for i in range(1, 21): # Create 20 orders
customer_id = random.randint(1, 10)
order_date = start_date + timedelta(days=random.randint(0, 59), hours=random.randint(0, 23))
status = random.choice(order_statuses)
shipping_address = f"{random.randint(100, 999)} Main St, Anytown"
orders_data.append((customer_id, order_date.strftime('%Y-%m-%d %H:%M:%S'), status, None, shipping_address)) # Total amount calculated later
cursor.executemany("INSERT INTO orders (customer_id, order_date, status, total_amount, shipping_address) VALUES (?, ?, ?, ?, ?)", orders_data)
print(f"Inserted {len(orders_data)} orders.")
order_items_data = []
order_totals = {} # Keep track of totals per order
for order_id in range(1, 21):
num_items = random.randint(1, 4)
order_total = 0
for _ in range(num_items):
product_id = random.randint(1, 10)
quantity = random.randint(1, 5)
# Get product price
cursor.execute("SELECT price FROM products WHERE product_id = ?", (product_id,))
price_per_unit = cursor.fetchone()[0]
order_items_data.append((order_id, product_id, quantity, price_per_unit))
order_total += quantity * price_per_unit
order_totals[order_id] = round(order_total, 2)
cursor.executemany("INSERT INTO order_items (order_id, product_id, quantity, price_per_unit) VALUES (?, ?, ?, ?)", order_items_data)
print(f"Inserted {len(order_items_data)} order items.")
# Update order totals
for order_id, total_amount in order_totals.items():
cursor.execute("UPDATE orders SET total_amount = ? WHERE order_id = ?", (total_amount, order_id))
print("Updated order totals.")
conn.commit()
conn.close()
print(f"Database '{db_file}' created and populated successfully.")
if __name__ == "__main__":
populate_database()
================================================
FILE: cookbook/pocketflow-text2sql/requirements.txt
================================================
pocketflow>=0.0.1
openai>=1.0.0
pyyaml>=6.0
sqlite3>=3.0
================================================
FILE: cookbook/pocketflow-text2sql/utils/call_llm.py
================================================
import os
from openai import OpenAI
def call_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
# Example usage
if __name__ == "__main__":
print(call_llm("Tell me a short joke"))
================================================
FILE: cookbook/pocketflow-thinking/README.md
================================================
# Chain-of-Thought
This project demonstrates an implementation that orchestrates a Chain-of-Thought process, enabling LLMs to solve complex reasoning problems by thinking step-by-step. It's designed to improve problem-solving accuracy through deliberate, structured reasoning managed externally.
This implementation is based on: [Build Chain-of-Thought From Scratch - Tutorial for Dummies](https://zacharyhuang.substack.com/p/build-chain-of-thought-from-scratch).
## Features
- Improves model reasoning on complex problems.
- Leverages capable instruction-following models (e.g., Claude 3.7 Sonnet, GPT-4 series) to perform structured Chain-of-Thought reasoning.
- Solves problems that direct prompting often fails on by breaking them down systematically.
- Provides detailed reasoning traces, including step-by-step evaluation and planning, for verification.
## Getting Started
1. **Install Packages:**
```bash
pip install -r requirements.txt
```
2. **Set API Key:**
```bash
export ANTHROPIC_API_KEY="your-api-key-here"
```
3. **Verify API Key (Optional):**
Run a quick check to ensure your key and environment are set up correctly.
```bash
python utils.py
```
4. **Run Default Example:**
Execute the main script to see the process in action with the default Jane Street problem.
```bash
python main.py
```
The default question is:
> You keep rolling a fair die until you roll three, four, five in that order consecutively on three rolls. What is the probability that you roll the die an odd number of times?
5. **Run Custom Problem:**
Provide your own reasoning problem using the `--` argument.
```bash
python main.py --"Your complex reasoning problem here"
```
## How It Works
The implementation uses a self-looping PocketFlow node (`ChainOfThoughtNode`) that guides an LLM through a structured problem-solving process:
```mermaid
flowchart LR
cot[ChainOfThoughtNode] -->|"continue"| cot
```
In each loop (thought step), the node directs the LLM to:
1. Evaluate the previous thought's reasoning and results.
2. Execute the next pending step according to a maintained plan.
3. Update the plan, marking the step done (with results) or noting issues.
4. Refine the plan if steps need breaking down or errors require correction.
5. Decide if further thinking (`next_thought_needed`) is required based on the plan state.
This external orchestration enforces a systematic approach, helping models tackle problems that are difficult with a single prompt.
## Comparison with Different Approaches
- **Standard Prompting**: Techniques like asking the model to "think step by step" within a single prompt can help, but the reasoning might lack depth or structure, and the model can easily lose track or make unrecoverable errors.
- **Native Extended Thinking Modes**: Some models (like Claude 3.7, GPT-o1, etc.) offer dedicated modes or features explicitly for extended reasoning, often yielding strong results directly via API calls.
- **This Implementation**: Demonstrates how to orchestrate a structured Chain-of-Thought process using standard LLMs (even those without a specific native 'extended thinking' mode), managing the steps, planning, and evaluation externally via prompt engineering and flow control.
## Example Thinking Process
Let's try out this challenging [Jane Street Quant Trading Interview Question](https://www.youtube.com/watch?v=gQJTkuEVPrU):
> **Problem**: You keep rolling a fair die until you roll three, four, five in that order consecutively on three rolls. What is the probability that you roll the die an odd number of times?
This problem demonstrates why structured Chain-of-Thought is valuable:
- **Standard models (single prompt)**: Often get the wrong answer or provide flawed reasoning.
- **Models using native thinking modes**: Can find the correct answer (216/431 ≈ 0.5012), though performance and reasoning clarity may vary.
- **This implementation (orchestrating a capable LLM)**: Can guide the model towards the correct answer by enforcing a step-by-step plan, evaluation, and refinement loop.
For comparison:
- [Claude 3.7 Sonnet (single prompt)](https://claude.ai/share/da139326-42fe-42d9-9d7b-35870daa5c1b): Wrong answer
- [Claude 3.7 Sonnet (using built-in thinking)](https://claude.ai/share/6f4140ed-f33c-4949-8778-a57719498e40): Correct answer after 3m, 45s
- [GPT-o1 (using built-in thinking)](https://chatgpt.com/share/67fee0fd-2600-8000-bcdf-76e40a986ee4): Correct answer after 2m, 0s
- [GPT-o1 pro (using built-in thinking)](https://chatgpt.com/share/67fee11b-530c-8000-92d1-609b6ca49c9c): Correct answer after 4m, 24s
Below is an example output trace showing how **this implementation guides Claude 3.7 Sonnet** through the problem-solving process:
> **Note:** Even with structured thinking orchestration, models don't always get the right answer, especially on very complex or novel problems. However, this approach significantly improves the robustness of the reasoning process and provides a traceable path for verification and debugging.
```
🤔 Processing question: You keep rolling a fair die until you roll three, four, five in that order consecutively on three rolls. What is the probability that you roll the die an odd number of times?
Thought 1:
Let me think through this problem by setting up a clear approach.
We need to find the probability that the total number of die rolls will be odd, given we stop when we roll "3,4,5" consecutively in that order.
This is a problem about a stochastic process that ends when a specific pattern occurs. To solve it, I'll:
1. Set up a Markov chain model to track progress toward the target sequence
2. Calculate expected lengths and probabilities for different outcomes
3. Determine if the total number of rolls has equal probability of being odd vs. even
First, I need to understand what states we can be in while rolling the die:
- State 0: Haven't rolled any of the sequence yet
- State 1: Just rolled a 3, waiting for 4
- State 2: Rolled 3 followed by 4, waiting for 5
- State 3: Success! Rolled the full "3,4,5" sequence
When the die is rolled, we transition between these states with certain probabilities. Each state depends only on the previous 0-2 rolls, making this a Markov process.
Current Plan Status:
- [Done] Understand the problem structure: Identified that we need the probability of an odd number of total rolls until getting '3,4,5' consecutively. Set up a Markov chain with states representing progress toward target sequence.
- [Pending] Set up the Markov model with transition probabilities
- [Pending] Define relevant states in the Markov chain
- [Pending] Calculate transition probabilities between states
- [Pending] Calculate generating functions for the process
- [Pending] Determine probability of odd number of rolls
- [Pending] Extract coefficient information from generating function
- [Pending] Compute final probability value
- [Pending] Verify solution with alternative approach
- [Pending] Conclusion
--------------------------------------------------
Thought 2:
Evaluation of Thought 1: Correct. The approach of using a Markov chain to model this problem is appropriate. Identifying the states (0: no part of sequence, 1: just rolled 3, 2: rolled 3,4, 3: success with 3,4,5) correctly captures the progression toward our target sequence.
I'll now set up the Markov model with transition probabilities. Each state represents our progress toward the target sequence "3,4,5".
State 0: Haven't rolled any relevant part of the sequence yet
- If we roll a 3: Move to State 1 (probability 1/6)
- If we roll anything else: Stay in State 0 (probability 5/6)
State 1: Just rolled a 3, waiting for a 4
- If we roll a 4: Move to State 2 (probability 1/6)
- If we roll a 3: Stay in State 1 (probability 1/6)
- If we roll anything else: Return to State 0 (probability 4/6)
State 2: Rolled 3 followed by 4, waiting for a 5
- If we roll a 5: Move to State 3 (success - probability 1/6)
- If we roll a 3: Move to State 1 (probability 1/6)
- If we roll anything else: Return to State 0 (probability 4/6)
State 3: Success! Rolled the sequence "3,4,5"
- This is an absorbing state; once we reach it, we stop rolling.
These transition probabilities can be represented as a matrix:
P = [
[5/6, 1/6, 0, 0 ],
[4/6, 1/6, 1/6, 0 ],
[4/6, 1/6, 0, 1/6],
[0, 0, 0, 1 ]
]
This completes the setup of the Markov model with transition probabilities.
Current Plan Status:
- [Done] Understand the problem structure: Identified that we need the probability of an odd number of total rolls until getting '3,4,5' consecutively. Set up a Markov chain with states representing progress toward target sequence.
- [Done] Set up the Markov model with transition probabilities: Defined 4 states (0: no sequence, 1: just rolled 3, 2: rolled 3,4, 3: success with 3,4,5) and calculated transition probabilities between all states in matrix form.
- [Pending] Calculate generating functions for the process
- [Pending] Set up generating function equations for each state
- [Pending] Solve the system of equations to find generating functions
- [Pending] Determine probability of odd number of rolls
- [Pending] Extract coefficient information from generating function
- [Pending] Compute final probability value
- [Pending] Verify solution with alternative approach
- [Pending] Conclusion
--------------------------------------------------
Thought 3:
Evaluation of Thought 2: Correct. The Markov chain setup with the four states and the transition probability matrix accurately represents our problem. Now I'll proceed to calculating the generating functions for this process.
To calculate the generating functions, I'll use the variable z to represent a single die roll. For each state, the generating function gives us information about the number of rolls until we reach our success state.
Let G₀(z), G₁(z), G₂(z), and G₃(z) be the generating functions for reaching the success state (State 3) when starting from State 0, 1, 2, and 3 respectively.
G₃(z) = 1 (since we're already in the success state)
For the other states, we can write equations based on the first roll and subsequent state:
G₀(z) = z·(5/6·G₀(z) + 1/6·G₁(z))
This means: we roll once (factor z), then with probability 5/6 we stay in State 0, or with probability 1/6 we move to State 1.
G₁(z) = z·(4/6·G₀(z) + 1/6·G₁(z) + 1/6·G₂(z))
When in State 1, we roll once (factor z), then either move to State 0, stay in State 1, or advance to State 2.
G₂(z) = z·(4/6·G₀(z) + 1/6·G₁(z) + 1/6·G₃(z))
= z·(4/6·G₀(z) + 1/6·G₁(z) + 1/6·1)
= z·(4/6·G₀(z) + 1/6·G₁(z) + 1/6)
Now I can solve this system of equations for G₀(z):
From G₀(z) = z·(5/6·G₀(z) + 1/6·G₁(z)):
G₀(z) - 5z/6·G₀(z) = z/6·G₁(z)
G₀(z)(1 - 5z/6) = z/6·G₁(z)
G₀(z) = (z/6·G₁(z))/(1 - 5z/6) ... (1)
From G₁(z) = z·(4/6·G₀(z) + 1/6·G₁(z) + 1/6·G₂(z)):
G₁(z) - z/6·G₁(z) = 4z/6·G₀(z) + z/6·G₂(z)
G₁(z)(1 - z/6) = 4z/6·G₀(z) + z/6·G₂(z) ... (2)
From G₂(z) = z·(4/6·G₀(z) + 1/6·G₁(z) + 1/6):
G₂(z) = 4z/6·G₀(z) + z/6·G₁(z) + z/6 ... (3)
Substituting (3) into (2):
G₁(z)(1 - z/6) = 4z/6·G₀(z) + z/6·(4z/6·G₀(z) + z/6·G₁(z) + z/6)
G₁(z)(1 - z/6) = 4z/6·G₀(z) + 4z²/36·G₀(z) + z²/36·G₁(z) + z²/36
G₁(z)(1 - z/6 - z²/36) = 4z/6·G₀(z)(1 + z/6) + z²/36
G₁(z) = (4z/6·G₀(z)(1 + z/6) + z²/36)/(1 - z/6 - z²/36) ... (4)
Substituting (4) into (1) gives us G₀(z) in terms of z only, which I can then simplify to find the closed form.
This is getting algebraically intricate. Let me take a step back and consider a different approach using first-step analysis with parity.
Current Plan Status:
- [Done] Understand the problem structure: Identified that we need probability of an odd number of total rolls until getting '3,4,5' consecutively. Set up a Markov chain with states representing progress toward target sequence.
- [Done] Set up the Markov model with transition probabilities: Defined 4 states (0: no sequence, 1: just rolled 3, 2: rolled 3,4, 3: success with 3,4,5) and calculated transition probabilities between all states in matrix form.
- [Verification Needed] Calculate generating functions for the process (The generating function approach became algebraically complicated. Consider a more direct approach using first-step analysis with parity.)
- [Done] Set up generating function equations for each state: Established equations G₀(z), G₁(z), G₂(z), and G₃(z) to track rolls until success.
- [Verification Needed] Solve the system of equations to find generating functions (Equations become unwieldy; may not be most efficient approach.)
- [Pending] Use first-step analysis with parity
- [Pending] Define probability variables with parity information
- [Pending] Set up equations relating odd/even outcome probabilities
- [Pending] Solve the system of equations
- [Pending] Determine probability of odd number of rolls
- [Pending] Extract final probability from analysis
- [Pending] Verify the mathematical result
- [Pending] Verify solution with alternative approach
- [Pending] Conclusion
--------------------------------------------------
Thought 4:
Evaluation of Thought 3: Correct, but pivoting is wise. The generating function approach, while theoretically sound, is becoming algebraically complex. The decision to switch to a first-step analysis with parity is appropriate for this problem.
I'll now use first-step analysis focusing specifically on the parity of the number of rolls.
Let's define:
- P₀: Probability of rolling an odd number of times when starting in State 0
- P₁: Probability of rolling an odd number of times when starting in State 1
- P₂: Probability of rolling an odd number of times when starting in State 2
Using first-step analysis, we can write equations for each state:
For State 0:
- If we roll a 3 (prob 1/6), we go to State 1 with 1 roll used (odd). The total number of rolls will be odd if the remaining rolls from State 1 are even.
- If we roll anything else (prob 5/6), we stay in State 0 with 1 roll used (odd). The total will be odd if the remaining rolls from State 0 are even.
Therefore: P₀ = (1/6)*(1-P₁) + (5/6)*(1-P₀) = 5/6 - 5P₀/6 + 1/6 - P₁/6
For State 1:
- If we roll a 4 (prob 1/6), we go to State 2 with 1 roll used (odd). The total will be odd if remaining rolls from State 2 are even.
- If we roll a 3 (prob 1/6), we stay in State 1 with 1 roll used (odd). The total will be odd if remaining rolls from State 1 are even.
- If we roll anything else (prob 4/6), we go to State 0 with 1 roll used (odd). The total will be odd if remaining rolls from State 0 are even.
Therefore: P₁ = (1/6)*(1-P₂) + (1/6)*(1-P₁) + (4/6)*(1-P₀) = 1 - P₂/6 - P₁/6 - 4P₀/6
For State 2:
- If we roll a 5 (prob 1/6), we're done with 1 more roll (odd). Total is definitely odd.
- If we roll a 3 (prob 1/6), we go to State 1 with 1 roll used (odd). The total will be odd if remaining rolls from State 1 are even.
- If we roll anything else (prob 4/6), we go to State 0 with 1 roll used (odd). The total will be odd if remaining rolls from State 0 are even.
Therefore: P₂ = (1/6)*1 + (1/6)*(1-P₁) + (4/6)*(1-P₀) = 1/6 + 1/6 - P₁/6 + 4/6 - 4P₀/6 = 6/6 - P₁/6 - 4P₀/6
Now I have a system of 3 equations with 3 unknowns:
P₀ = 1 - 5P₀/6 - P₁/6
P₁ = 1 - P₁/6 - P₂/6 - 4P₀/6
P₂ = 1 - P₁/6 - 4P₀/6
Simplifying:
(6/6)P₀ = 6/6 - 5P₀/6 - P₁/6
(6/6)P₁ = 6/6 - P₁/6 - P₂/6 - 4P₀/6
(6/6)P₂ = 6/6 - P₁/6 - 4P₀/6
Further simplifying:
11P₀/6 + P₁/6 = 1
4P₀/6 + 7P₁/6 + P₂/6 = 1
4P₀/6 + P₁/6 + 6P₂/6 = 1
This is a linear system of equations that I can solve.
Current Plan Status:
- [Done] Understand the problem structure: Identified that we need probability of an odd number of total rolls until getting '3,4,5' consecutively. Set up a Markov chain with states representing progress toward target sequence.
- [Done] Set up the Markov model with transition probabilities: Defined 4 states (0: no sequence, 1: just rolled 3, 2: rolled 3,4, 3: success with 3,4,5) and calculated transition probabilities between all states in matrix form.
- [Verification Needed] Calculate generating functions for the process (Approach became algebraically complicated; shifted to more direct first-step analysis with parity.)
- [Done] Set up generating function equations for each state: Established equations G₀(z), G₁(z), G₂(z), and G₃(z) to track rolls until success.
- [Verification Needed] Solve the system of equations to find generating functions (Equations became unwieldy; more efficient to use first-step analysis with parity instead.)
- [Done] Use first-step analysis with parity: Defined variables P₀, P₁, P₂ for probability of odd rolls from each state. Set up system of equations tracking odd/even outcomes.
- [Done] Define probability variables with parity information: Defined P₀, P₁, P₂ as probabilities of odd number of rolls starting from respective states.
- [Done] Set up equations relating odd/even outcome probabilities: Derived equations: 11P₀/6 + P₁/6 = 1, 4P₀/6 + 7P₁/6 + P₂/6 = 1, 4P₀/6 + P₁/6 + 6P₂/6 = 1.
- [Pending] Solve the system of equations
- [Pending] Determine probability of odd number of rolls
- [Pending] Extract final probability from analysis
- [Pending] Verify the mathematical result
- [Pending] Verify solution with alternative approach
- [Pending] Conclusion
--------------------------------------------------
Thought 5:
Evaluation of Thought 4: Correct. The first-step analysis with parity is well set up, with appropriate variables P₀, P₁, and P₂ representing the probability of rolling an odd number of times from each state. The system of equations is correctly derived, though I notice it's written in slightly different forms across the thought. Let me solve this system now.
Taking the system of equations from the end of Thought 4:
11P₀/6 + P₁/6 = 1
4P₀/6 + 7P₁/6 + P₂/6 = 1
4P₀/6 + P₁/6 + 6P₂/6 = 1
Let's multiply all equations by 6 to eliminate fractions:
11P₀ + P₁ = 6
4P₀ + 7P₁ + P₂ = 6
4P₀ + P₁ + 6P₂ = 6
Subtracting the third equation from the second:
(4P₀ + 7P₁ + P₂) - (4P₀ + P₁ + 6P₂) = 6 - 6
6P₁ - 5P₂ = 0
6P₁ = 5P₂
P₁ = 5P₂/6
From the first equation:
11P₀ + P₁ = 6
11P₀ + 5P₂/6 = 6
66P₀ + 5P₂ = 36
From the third equation:
4P₀ + P₁ + 6P₂ = 6
4P₀ + 5P₂/6 + 6P₂ = 6
24P₀ + 5P₂ + 36P₂ = 36
24P₀ + 41P₂ = 36
Now solving for P₀ in terms of P₂:
24P₀ + 41P₂ = 36
24P₀ = 36 - 41P₂
P₀ = (36 - 41P₂)/24 = 3/2 - 41P₂/24
Substituting back into 66P₀ + 5P₂ = 36:
66(3/2 - 41P₂/24) + 5P₂ = 36
99/2 - 66*41P₂/24 + 5P₂ = 36
99/2 - 2706P₂/24 + 5P₂ = 36
99/2 - 2706P₂/24 + 120P₂/24 = 36
99/2 - (2706-120)P₂/24 = 36
99/2 - 2586P₂/24 = 36
99 - 2586P₂/12 = 72
99 - 72 = 2586P₂/12
27 = 2586P₂/12
27*12 = 2586P₂
324 = 2586P₂
P₂ = 324/2586 = 108/862 = 54/431
Now we can find P₁:
P₁ = 5P₂/6 = 5(54/431)/6 = 270/2586 = 45/431
And P₀:
P₀ = 3/2 - 41P₂/24 = 3/2 - 41(54/431)/24 = 3/2 - 2214/10344 = 3/2 - 1107/5172
Let's calculate:
P₀ = 3/2 - 1107/5172 = 7758/5172 - 1107/5172 = 6651/5172 = 2217/1724
Now we have:
P₀ = 2217/1724
P₁ = 45/431
P₂ = 54/431
Wait, I see an error in my calculation. Let me restart with the system of equations.
From the equations:
11P₀/6 + P₁/6 = 1
4P₀/6 + 7P₁/6 + P₂/6 = 1
4P₀/6 + P₁/6 + 6P₂/6 = 1
Multiplying by 6:
11P₀ + P₁ = 6
4P₀ + 7P₁ + P₂ = 6
4P₀ + P₁ + 6P₂ = 6
From the first: P₁ = 6 - 11P₀
Substituting into the second:
4P₀ + 7(6 - 11P₀) + P₂ = 6
4P₀ + 42 - 77P₀ + P₂ = 6
4P₀ - 77P₀ + P₂ = 6 - 42
-73P₀ + P₂ = -36
P₂ = -36 + 73P₀
Substituting P₁ and P₂ into the third equation:
4P₀ + (6 - 11P₀) + 6(-36 + 73P₀) = 6
4P₀ + 6 - 11P₀ - 216 + 438P₀ = 6
4P₀ - 11P₀ + 438P₀ = 6 - 6 + 216
431P₀ = 216
P₀ = 216/431 = 8/16 = 1/2
Now we can find:
P₁ = 6 - 11P₀ = 6 - 11(216/431) = 6 - 2376/431 = 2586/431 - 2376/431 = 210/431
P₂ = -36 + 73P₀ = -36 + 73(216/431) = -36 + 15768/431 = -15552/431 + 15768/431 = 216/431
The answer to our original question is P₀, which is 216/431.
Actually, I notice the fraction simplified to 1/2 above is incorrect (216/431 ≠ 8/16). Let's double-check with the equations:
From 11P₀ + P₁ = 6 and P₁ = 210/431, we get:
11(216/431) + 210/431 = 6
2376/431 + 210/431 = 6
2586/431 = 6
2586 = 2586
This confirms our answer: P₀ = 216/431, which is the probability of rolling the die an odd number of times.
Current Plan Status:
- [Done] Understand the problem structure: Identified that we need probability of an odd number of total rolls until getting '3,4,5' consecutively. Set up a Markov chain with states representing progress toward target sequence.
- [Done] Set up the Markov model with transition probabilities: Defined 4 states (0: no sequence, 1: just rolled 3, 2: rolled 3,4, 3: success with 3,4,5) and calculated transition probabilities between all states in matrix form.
- [Verification Needed] Calculate generating functions for the process (The generating function approach was algebraically complex; pivoted to first-step analysis with parity instead.)
- [Done] Set up generating function equations for each state: Established equations G₀(z), G₁(z), G₂(z), and G₃(z) to track rolls until success.
- [Verification Needed] Solve the system of equations to find generating functions (Equations became unwieldy; more efficient to use first-step analysis with parity instead.)
- [Done] Use first-step analysis with parity: Successfully applied first-step analysis with parity to solve the problem directly.
- [Done] Define probability variables with parity information: Defined P₀, P₁, P₂ as probabilities of odd number of rolls starting from respective states.
- [Done] Set up equations relating odd/even outcome probabilities: Derived equations: 11P₀/6 + P₁/6 = 1, 4P₀/6 + 7P₁/6 + P₂/6 = 1, 4P₀/6 + P₁/6 + 6P₂/6 = 1.
- [Done] Solve the system of equations: Found P₀ = 216/431, P₁ = 210/431, P₂ = 216/431.
- [Done] Determine probability of odd number of rolls: The probability of rolling the die an odd number of times is 216/431 ≈ 0.501
- [Done] Extract final probability from analysis: Identified P₀ = 216/431 as the answer to our original question.
- [Done] Verify the mathematical result: Verified solution by substituting back into original equations.
- [Pending] Verify solution with alternative approach
- [Pending] Conclusion
--------------------------------------------------
Thought 6:
Evaluation of Thought 5: Correct. The calculation was methodical and accurate, resulting in the solution P₀ = 216/431. The self-correction when an arithmetic error was spotted was appropriate. The substitution back into the original equation verified that the answer is correct.
Let me now verify our solution using an alternative approach. The problem asks for the probability of an odd number of rolls when stopping at the sequence "3,4,5". An alternative way to approach this is to consider the complementary probability - the probability of an even number of rolls.
Since P₀ = 216/431 is the probability of an odd number of rolls, the probability of an even number of rolls must be 1 - 216/431 = 215/431.
Let me verify this with the equations we derived:
- Let Q₀, Q₁, Q₂ be the probabilities of rolling an even number of times from states 0, 1, and 2 respectively.
- Since the events "odd number of rolls" and "even number of rolls" are complementary: Q₀ = 1-P₀, Q₁ = 1-P₁, Q₂ = 1-P₂
- Therefore: Q₀ = 1 - 216/431 = 215/431, Q₁ = 1 - 210/431 = 221/431, Q₂ = 1 - 216/431 = 215/431
Now, I can derive the equations for Q₀, Q₁, Q₂ directly:
For State 0:
- If we roll a 3 (prob 1/6), we transition to State 1 after 1 roll (odd). The total will be even if the remaining rolls from State 1 are odd.
- If we roll anything else (prob 5/6), we stay in State 0 after 1 roll (odd). The total will be even if the remaining rolls from State 0 are odd.
Therefore: Q₀ = (1/6)(P₁) + (5/6)(P₀) = P₁/6 + 5P₀/6 = 210/431/6 + 5(216/431)/6 = 210/2586 + 1080/2586 = 1290/2586 = 215/431
For State 1:
- If we roll a 4 (prob 1/6), we go to State 2 after 1 roll (odd). The total will be even if the remaining rolls from State 2 are odd.
- If we roll a 3 (prob 1/6), we stay in State 1 after 1 roll (odd). The total will be even if the remaining rolls from State 1 are odd.
- If we roll anything else (prob 4/6), we go to State 0 after 1 roll (odd). The total will be even if the remaining rolls from State 0 are odd.
Therefore: Q₁ = (1/6)(P₂) + (1/6)(P₁) + (4/6)(P₀) = P₂/6 + P₁/6 + 4P₀/6 = 216/431/6 + 210/431/6 + 4(216/431)/6 = 216/2586 + 210/2586 + 864/2586 = 1290/2586 = 215/431
Wait, that doesn't match our expected Q₁ = 221/431. Let me double-check...
The direct calculation of Q₁ gives a different result than 1-P₁. This suggests an error somewhere. Let me reconsider our system:
If P₀ + Q₀ = 1, P₁ + Q₁ = 1, P₂ + Q₂ = 1, then our system should be consistent. Let me verify our values from Thought 5:
From 11P₀ + P₁ = 6 and P₀ = 216/431:
11(216/431) + P₁ = 6
2376/431 + P₁ = 6
P₁ = 6 - 2376/431 = 2586/431 - 2376/431 = 210/431
From 4P₀ + P₁ + 6P₂ = 6:
4(216/431) + 210/431 + 6P₂ = 6
864/431 + 210/431 + 6P₂ = 6
1074/431 + 6P₂ = 6
6P₂ = 6 - 1074/431 = 2586/431 - 1074/431 = 1512/431
P₂ = 1512/431/6 = 1512/2586 = 252/431
This doesn't match our calculated P₂ = 216/431 from Thought 5. Let me solve the system again, more carefully.
From the system:
11P₀ + P₁ = 6
4P₀ + 7P₁ + P₂ = 6
4P₀ + P₁ + 6P₂ = 6
Subtracting the first from the third:
(4P₀ + P₁ + 6P₂) - (11P₀ + P₁) = 6 - 6
4P₀ - 11P₀ + 6P₂ = 0
-7P₀ + 6P₂ = 0
6P₂ = 7P₀
P₂ = 7P₀/6
Subtracting the first from the second:
(4P₀ + 7P₁ + P₂) - (11P₀ + P₁) = 6 - 6
4P₀ - 11P₀ + 6P₁ + P₂ = 0
-7P₀ + 6P₁ + P₂ = 0
Substituting P₂ = 7P₀/6:
-7P₀ + 6P₁ + 7P₀/6 = 0
-42P₀ + 36P₁ + 7P₀ = 0
-35P₀ + 36P₁ = 0
36P₁ = 35P₀
P₁ = 35P₀/36
Now, from the first equation:
11P₀ + P₁ = 6
11P₀ + 35P₀/36 = 6
396P₀ + 35P₀ = 216
431P₀ = 216
P₀ = 216/431
Therefore:
P₁ = 35P₀/36 = 35(216/431)/36 = 35(6)/36(431/6) = 35/6 ÷ 431/6 = 35/431
P₂ = 7P₀/6 = 7(216/431)/6 = 7(6)/6(431/6) = 7/431/6 × 6 = 7/431 × 6 = 42/431
Hmm, these values are different from what we calculated in Thought 5. Let me check these values in the original equations:
11(216/431) + 35/431 = 6?
2376/431 + 35/431 = 6?
2411/431 = 6?
2411 = 2586?
This doesn't check out. Let me solve the system one more time using a different approach.
From the original system:
11P₀ + P₁ = 6 (1)
4P₀ + 7P₁ + P₂ = 6 (2)
4P₀ + P₁ + 6P₂ = 6 (3)
From (3) - (1):
4P₀ + P₁ + 6P₂ - 11P₀ - P₁ = 6 - 6
-7P₀ + 6P₂ = 0
6P₂ = 7P₀
P₂ = 7P₀/6 (4)
From (2) - (1):
4P₀ + 7P₁ + P₂ - 11P₀ - P₁ = 6 - 6
-7P₀ + 6P₁ + P₂ = 0
Substituting (4):
-7P₀ + 6P₁ + 7P₀/6 = 0
-42P₀ + 36P₁ + 7P₀ = 0
-35P₀ + 36P₁ = 0
36P₁ = 35P₀
P₁ = 35P₀/36 (5)
Substituting (5) into (1):
11P₀ + 35P₀/36 = 6
(11×36)P₀ + 35P₀ = 6×36
396P₀ + 35P₀ = 216
431P₀ = 216
P₀ = 216/431
Now calculate:
P₁ = 35P₀/36 = 35(216/431)/36 = 7560/15516 = 35/72 × 216/431 = 35×216/36×431 = 7560/15516 = 35/72 × 216/431 = 7560/15516 = 1260/2586 = 210/431
Let me verify:
11(216/431) + 210/431 = 6?
2376/431 + 210/431 = 6?
2586/431 = 6?
2586 = 2586 ✓
Now for P₂:
P₂ = 7P₀/6 = 7(216/431)/6 = 1512/2586 = 252/431
Checking:
4(216/431) + 210/431 + 6(252/431) = 6?
864/431 + 210/431 + 1512/431 = 6?
2586/431 = 6?
2586 = 2586 ✓
Our final answer is: P₀ = 216/431, P₁ = 210/431, P₂ = 252/431
This confirms our result that the probability of rolling an odd number of times is 216/431 ≈ 0.5012.
Current Plan Status:
- [Done] Understand the problem structure: Identified that we need probability of an odd number of total rolls until getting '3,4,5' consecutively. Set up a Markov chain with states representing progress toward target sequence.
- [Done] Set up the Markov model with transition probabilities: Defined 4 states (0: no sequence, 1: just rolled 3, 2: rolled 3,4, 3: success with 3,4,5) and calculated transition probabilities between all states in matrix form.
- [Verification Needed] Calculate generating functions for the process (The generating function approach was algebraically complex; pivoted to first-step analysis with parity instead.)
- [Done] Set up generating function equations for each state: Established equations G₀(z), G₁(z), G₂(z), and G₃(z) to track rolls until success.
- [Verification Needed] Solve the system of equations to find generating functions (Equations became unwieldy; more efficient to use first-step analysis with parity instead.)
- [Done] Use first-step analysis with parity: Successfully applied first-step analysis with parity to solve the problem directly.
- [Done] Define probability variables with parity information: Defined P₀, P₁, P₂ as probabilities of odd number of rolls starting from respective states.
- [Done] Set up equations relating odd/even outcome probabilities: Derived equations: 11P₀/6 + P₁/6 = 1, 4P₀/6 + 7P₁/6 + P₂/6 = 1, 4P₀/6 + P₁/6 + 6P₂/6 = 1.
- [Done] Solve the system of equations: Found P₀ = 216/431, P₁ = 210/431, P₂ = 252/431.
- [Done] Determine probability of odd number of rolls: The probability of rolling the die an odd number of times is 216/431 ≈ 0.5012.
- [Done] Extract final probability from analysis: Identified P₀ = 216/431 as the answer to our original question.
- [Done] Verify the mathematical result: Verified solution by substituting back into original equations.
- [Done] Verify solution with alternative approach: Double-checked our solution by re-solving the system and verifying that the complementary probability approach gives consistent results.
- [Pending] Conclusion
--------------------------------------------------
Thought 7 (Conclusion):
Evaluation of Thought 6: Correct. The analysis carefully re-solved the system of equations to verify our answer and found P₀ = 216/431, P₁ = 210/431, and P₂ = 252/431. The verification process was thorough, checking the solution by substituting back into the original equations, confirming that the probability of rolling an odd number of times is 216/431 ≈ 0.5012.
Now I'll provide a conclusion that summarizes the problem, our approach, and the final answer.
We set out to find the probability that we roll a fair die an odd number of times until getting the sequence "3,4,5" consecutively in that order.
Our approach used a Markov chain model with states representing progress toward the target sequence:
- State 0: Haven't rolled any part of the sequence yet
- State 1: Just rolled a 3, waiting for 4
- State 2: Rolled 3 followed by 4, waiting for 5
- State 3: Success! Rolled the full "3,4,5" sequence
Initially, we considered a generating function approach, but pivoted to a more direct first-step analysis with parity. We defined variables P₀, P₁, and P₂ representing the probabilities of an odd number of total rolls when starting from each respective state.
Through careful derivation and solution of a system of linear equations, we found:
P₀ = 216/431 ≈ 0.5012
P₁ = 210/431 ≈ 0.4872
P₂ = 252/431 ≈ 0.5847
Since we start from State 0 (no part of the sequence rolled yet), the answer to our original question is P₀ = 216/431.
To verify this result, we solved the system multiple times and checked that our values satisfied the original equations.
The final answer is: The probability of rolling the die an odd number of times until getting the sequence "3,4,5" is 216/431 ≈ 0.5012.
This probability is slightly greater than 1/2, meaning it's slightly more likely to roll an odd number of times than an even number of times before completing the sequence.
Final Plan Status:
- [Done] Understand the problem structure: Identified that we need probability of an odd number of total rolls until getting '3,4,5' consecutively. Set up a Markov chain with states representing progress toward target sequence.
- [Done] Set up the Markov model with transition probabilities: Defined 4 states (0: no sequence, 1: just rolled 3, 2: rolled 3,4, 3: success with 3,4,5) and calculated transition probabilities between all states in matrix form.
- [Verification Needed] Calculate generating functions for the process (The generating function approach became algebraically complex; pivoted to first-step analysis with parity instead.)
- [Done] Set up generating function equations for each state: Established equations G₀(z), G₁(z), G₂(z), and G₃(z) to track rolls until success.
- [Verification Needed] Solve the system of equations to find generating functions (Equations became unwieldy; more efficient to use first-step analysis with parity instead.)
- [Done] Use first-step analysis with parity: Successfully applied first-step analysis with parity to solve the problem directly.
- [Done] Define probability variables with parity information: Defined P₀, P₁, P₂ as probabilities of odd number of rolls starting from respective states.
- [Done] Set up equations relating odd/even outcome probabilities: Derived equations: 11P₀/6 + P₁/6 = 1, 4P₀/6 + 7P₁/6 + P₂/6 = 1, 4P₀/6 + P₁/6 + 6P₂/6 = 1.
- [Done] Solve the system of equations: Found P₀ = 216/431, P₁ = 210/431, P₂ = 252/431.
- [Done] Determine probability of odd number of rolls: The probability of rolling the die an odd number of times is 216/431 ≈ 0.5012.
- [Done] Extract final probability from analysis: Identified P₀ = 216/431 as the answer to our original question.
- [Done] Verify the mathematical result: Verified solution by substituting back into original equations.
- [Done] Verify solution with alternative approach: Double-checked our solution by re-solving the system and verifying that the values satisfy all original equations.
- [Done] Conclusion: The probability of rolling the die an odd number of times until getting the sequence '3,4,5' is 216/431 ≈ 0.5012, which is slightly greater than 1/2.
=== FINAL SOLUTION ===
Evaluation of Thought 6: Correct. The analysis carefully re-solved the system of equations to verify our answer and found P₀ = 216/431, P₁ = 210/431, and P₂ = 252/431. The verification process was thorough, checking the solution by substituting back into the original equations, confirming that the probability of rolling an odd number of times is 216/431 ≈ 0.5012.
Now I'll provide a conclusion that summarizes the problem, our approach, and the final answer.
We set out to find the probability that we roll a fair die an odd number of times until getting the sequence "3,4,5" consecutively in that order.
Our approach used a Markov chain model with states representing progress toward the target sequence:
- State 0: Haven't rolled any part of the sequence yet
- State 1: Just rolled a 3, waiting for 4
- State 2: Rolled 3 followed by 4, waiting for 5
- State 3: Success! Rolled the full "3,4,5" sequence
Initially, we considered a generating function approach, but pivoted to a more direct first-step analysis with parity. We defined variables P₀, P₁, and P₂ representing the probabilities of an odd number of total rolls when starting from each respective state.
Through careful derivation and solution of a system of linear equations, we found:
P₀ = 216/431 ≈ 0.5012
P₁ = 210/431 ≈ 0.4872
P₂ = 252/431 ≈ 0.5847
Since we start from State 0 (no part of the sequence rolled yet), the answer to our original question is P₀ = 216/431.
To verify this result, we solved the system multiple times and checked that our values satisfied the original equations.
The final answer is: The probability of rolling the die an odd number of times until getting the sequence "3,4,5" is 216/431 ≈ 0.5012.
This probability is slightly greater than 1/2, meaning it's slightly more likely to roll an odd number of times than an even number of times before completing the sequence.
======================
```
================================================
FILE: cookbook/pocketflow-thinking/design.md
================================================
# Chain of Thought Node Design
## 1. Requirements
Create a self-looping Chain of Thought node that can:
- Solve a problem step-by-step by maintaining and executing a structured plan.
- Critically evaluate the previous step's reasoning and results before proceeding.
- Refine the plan by breaking down complex steps into nested sub-steps.
- Update the status of plan steps (`Pending`, `Done`, `Verification Needed`) and record concise results.
- Handle potential errors identified during evaluation by adjusting the plan.
- Provide a detailed trace of the thinking process and plan evolution.
- Generate a final conclusion summarizing the solution when the plan is complete.
## 2. Flow Design
This will be a simple flow with a single node that can call itself repeatedly based on whether more thinking is needed according to the plan:
```mermaid
flowchart LR
cot[ChainOfThoughtNode] -->|"continue"| cot
```
## 3. Utilities
We'll need one primary utility function:
- `call_llm`: Call the LLM to generate the next thought (including evaluation, thinking, and updated plan) based on the problem, previous thoughts, and the current plan state. Helper functions (`format_plan`, `format_plan_for_prompt`) assist in presenting the plan.
## 4. Node Design
### Shared Store Design
```python
shared = {
"problem": str, # The problem statement.
"thoughts": list[dict], # List of thought dictionaries generated so far.
"current_thought_number": int, # Counter for the current thought being generated.
"solution": str | None # Stores the final conclusion text when finished.
}
```
Each thought dictionary added to the `shared["thoughts"]` list will contain the structured output from the LLM's execution step, plus the thought number:
```python
{
"thought_number": int, # The sequence number of this thought.
"current_thinking": str, # Detailed text of the evaluation and thinking for this step.
"planning": list[dict], # The updated plan structure (list of dictionaries).
"next_thought_needed": bool # Flag indicating if the loop should continue.
}
```
The `planning` list contains dictionaries representing steps, which can be nested:
```python
# Example structure for a plan step dictionary
{
"description": str, # Description of the step.
"status": str, # "Pending", "Done", "Verification Needed".
"result": str | None, # Optional: Concise result when status is "Done".
"mark": str | None, # Optional: Reason for "Verification Needed".
"sub_steps": list[dict] | None # Optional: Nested list for sub-steps.
}
```
### Chain of Thought Node (`ChainOfThoughtNode`)
- **`type`**: Regular (self-looping node).
- **`prep`**:
- Reads the problem statement and the list of previous thoughts from the shared store.
- Formats the history of thoughts and the *last known plan structure* into a text representation suitable for the LLM prompt.
- Determines if this is the first thought to adjust prompt instructions.
- Increments and updates `shared["current_thought_number"]`.
- **`exec`**:
- Constructs a detailed prompt for the LLM, including:
- The problem statement.
- The formatted history of previous thoughts and plans.
- Specific instructions for evaluating the previous thought, executing the next pending step, updating the plan structure (using the dictionary format), handling sub-steps, managing statuses/results, and indicating completion.
- The required YAML output format (`current_thinking`, `planning`, `next_thought_needed`).
- Calls the `call_llm` utility with the prompt.
- Parses the LLM's YAML response.
- Validates the presence and basic types of required keys (`current_thinking`, `planning`, `next_thought_needed`) using `assert`.
- Adds the `thought_number` to the parsed data.
- **`post`**:
- Appends the result dictionary from `exec` to the `shared["thoughts"]` list.
- Checks the `next_thought_needed` flag from the execution result.
- If `False`:
- Extracts the `current_thinking` content as the final `shared["solution"]`.
- Prints the final thought, plan, and solution.
- Returns `"end"` to terminate the flow loop.
- If `True`:
- Prints the current thought number, thinking content, and formatted current plan status.
- Returns `"continue"` to trigger the next iteration of the node.
================================================
FILE: cookbook/pocketflow-thinking/flow.py
================================================
from pocketflow import Flow
from nodes import ChainOfThoughtNode
def create_chain_of_thought_flow():
# Create a ChainOfThoughtNode
cot_node = ChainOfThoughtNode(max_retries=3, wait=10)
# Connect the node to itself for the "continue" action
cot_node - "continue" >> cot_node
# Create the flow
cot_flow = Flow(start=cot_node)
return cot_flow
================================================
FILE: cookbook/pocketflow-thinking/main.py
================================================
import sys
from flow import create_chain_of_thought_flow
def main():
# Default question
default_question = "You keep rolling a fair die until you roll three, four, five in that order consecutively on three rolls. What is the probability that you roll the die an odd number of times?"
# Get question from command line if provided with --
question = default_question
for arg in sys.argv[1:]:
if arg.startswith("--"):
question = arg[2:]
break
print(f"🤔 Processing question: {question}")
# Create the flow
cot_flow = create_chain_of_thought_flow()
# Set up shared state
shared = {
"problem": question,
"thoughts": [],
"current_thought_number": 0,
"total_thoughts_estimate": 10,
"solution": None
}
# Run the flow
cot_flow.run(shared)
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-thinking/nodes.py
================================================
# cookbook/pocketflow-thinking/nodes.py
from pocketflow import Node
import yaml
from utils import call_llm
import textwrap
# Helper function to format structured plan for printing
def format_plan(plan_items, indent_level=0):
indent = " " * indent_level
output = []
if isinstance(plan_items, list):
for item in plan_items:
if isinstance(item, dict):
status = item.get('status', 'Unknown')
desc = item.get('description', 'No description')
result = item.get('result', '')
mark = item.get('mark', '') # For verification etc.
# Format the main step line
line = f"{indent}- [{status}] {desc}"
if result:
line += f": {result}"
if mark:
line += f" ({mark})"
output.append(line)
# Recursively format sub-steps if they exist
sub_steps = item.get('sub_steps')
if sub_steps:
output.append(format_plan(sub_steps, indent_level + 1))
elif isinstance(item, str): # Basic fallback for string items
output.append(f"{indent}- {item}")
else: # Fallback for unexpected types
output.append(f"{indent}- {str(item)}")
elif isinstance(plan_items, str): # Handle case where plan is just an error string
output.append(f"{indent}{plan_items}")
else:
output.append(f"{indent}# Invalid plan format: {type(plan_items)}")
return "\n".join(output)
# Helper function to format structured plan for the prompt (simplified view)
def format_plan_for_prompt(plan_items, indent_level=0):
indent = " " * indent_level
output = []
# Simplified formatting for prompt clarity
if isinstance(plan_items, list):
for item in plan_items:
if isinstance(item, dict):
status = item.get('status', 'Unknown')
desc = item.get('description', 'No description')
line = f"{indent}- [{status}] {desc}"
output.append(line)
sub_steps = item.get('sub_steps')
if sub_steps:
# Indicate nesting without full recursive display in prompt
output.append(format_plan_for_prompt(sub_steps, indent_level + 1))
else: # Fallback
output.append(f"{indent}- {str(item)}")
else:
output.append(f"{indent}{str(plan_items)}")
return "\n".join(output)
class ChainOfThoughtNode(Node):
def prep(self, shared):
problem = shared.get("problem", "")
thoughts = shared.get("thoughts", [])
current_thought_number = shared.get("current_thought_number", 0)
shared["current_thought_number"] = current_thought_number + 1
# Format previous thoughts and extract last plan structure
thoughts_text = ""
last_plan_structure = None # Will store the list of dicts
if thoughts:
thoughts_text_list = []
for i, t in enumerate(thoughts):
thought_block = f"Thought {t.get('thought_number', i+1)}:\n"
thinking = textwrap.dedent(t.get('current_thinking', 'N/A')).strip()
thought_block += f" Thinking:\n{textwrap.indent(thinking, ' ')}\n"
plan_list = t.get('planning', [])
# Use the recursive helper for display formatting
plan_str_formatted = format_plan(plan_list, indent_level=2)
thought_block += f" Plan Status After Thought {t.get('thought_number', i+1)}:\n{plan_str_formatted}"
if i == len(thoughts) - 1:
last_plan_structure = plan_list # Keep the actual structure
thoughts_text_list.append(thought_block)
thoughts_text = "\n--------------------\n".join(thoughts_text_list)
else:
thoughts_text = "No previous thoughts yet."
# Suggest an initial plan structure using dictionaries
last_plan_structure = [
{'description': "Understand the problem", 'status': "Pending"},
{'description': "Develop a high-level plan", 'status': "Pending"},
{'description': "Conclusion", 'status': "Pending"}
]
# Format the last plan structure for the prompt context using the specific helper
last_plan_text_for_prompt = format_plan_for_prompt(last_plan_structure) if last_plan_structure else "# No previous plan available."
return {
"problem": problem,
"thoughts_text": thoughts_text,
"last_plan_text": last_plan_text_for_prompt,
"last_plan_structure": last_plan_structure, # Pass the raw structure too if needed for complex updates
"current_thought_number": current_thought_number + 1,
"is_first_thought": not thoughts
}
def exec(self, prep_res):
problem = prep_res["problem"]
thoughts_text = prep_res["thoughts_text"]
last_plan_text = prep_res["last_plan_text"]
# last_plan_structure = prep_res["last_plan_structure"] # Can use if needed
current_thought_number = prep_res["current_thought_number"]
is_first_thought = prep_res["is_first_thought"]
# --- Construct Prompt ---
# Instructions updated for dictionary structure
instruction_base = textwrap.dedent(f"""
Your task is to generate the next thought (Thought {current_thought_number}).
Instructions:
1. **Evaluate Previous Thought:** If not the first thought, start `current_thinking` by evaluating Thought {current_thought_number - 1}. State: "Evaluation of Thought {current_thought_number - 1}: [Correct/Minor Issues/Major Error - explain]". Address errors first.
2. **Execute Step:** Execute the first step in the plan with `status: Pending`.
3. **Maintain Plan (Structure):** Generate an updated `planning` list. Each item should be a dictionary with keys: `description` (string), `status` (string: "Pending", "Done", "Verification Needed"), and optionally `result` (string, concise summary when Done) or `mark` (string, reason for Verification Needed). Sub-steps are represented by a `sub_steps` key containing a *list* of these dictionaries.
4. **Update Current Step Status:** In the updated plan, change the `status` of the executed step to "Done" and add a `result` key with a concise summary. If verification is needed based on evaluation, change status to "Verification Needed" and add a `mark`.
5. **Refine Plan (Sub-steps):** If a "Pending" step is complex, add a `sub_steps` key to its dictionary containing a list of new step dictionaries (status: "Pending") breaking it down. Keep the parent step's status "Pending" until all sub-steps are "Done".
6. **Refine Plan (Errors):** Modify the plan logically based on evaluation findings (e.g., change status, add correction steps).
7. **Final Step:** Ensure the plan progresses towards a final step dictionary like `{{'description': "Conclusion", 'status': "Pending"}}`.
8. **Termination:** Set `next_thought_needed` to `false` ONLY when executing the step with `description: "Conclusion"`.
""")
# Context remains largely the same
if is_first_thought:
instruction_context = textwrap.dedent("""
**This is the first thought:** Create an initial plan as a list of dictionaries (keys: description, status). Include sub-steps via the `sub_steps` key if needed. Then, execute the first step in `current_thinking` and provide the updated plan (marking step 1 `status: Done` with a `result`).
""")
else:
instruction_context = textwrap.dedent(f"""
**Previous Plan (Simplified View):**
{last_plan_text}
Start `current_thinking` by evaluating Thought {current_thought_number - 1}. Then, proceed with the first step where `status: Pending`. Update the plan structure (list of dictionaries) reflecting evaluation, execution, and refinements.
""")
# Output format example updated for dictionary structure
instruction_format = textwrap.dedent("""
Format your response ONLY as a YAML structure enclosed in ```yaml ... ```:
```yaml
current_thinking: |
# Evaluation of Thought N: [Assessment] ... (if applicable)
# Thinking for the current step...
planning:
# List of dictionaries (keys: description, status, Optional[result, mark, sub_steps])
- description: "Step 1"
status: "Done"
result: "Concise result summary"
- description: "Step 2 Complex Task" # Now broken down
status: "Pending" # Parent remains Pending
sub_steps:
- description: "Sub-task 2a"
status: "Pending"
- description: "Sub-task 2b"
status: "Verification Needed"
mark: "Result from Thought X seems off"
- description: "Step 3"
status: "Pending"
- description: "Conclusion"
status: "Pending"
next_thought_needed: true # Set to false ONLY when executing the Conclusion step.
```
""")
# Combine prompt parts
prompt = textwrap.dedent(f"""
You are a meticulous AI assistant solving a complex problem step-by-step using a structured plan. You critically evaluate previous steps, refine the plan with sub-steps if needed, and handle errors logically. Use the specified YAML dictionary structure for the plan.
Problem: {problem}
Previous thoughts:
{thoughts_text}
--------------------
{instruction_base}
{instruction_context}
{instruction_format}
""")
# --- End Prompt Construction ---
response = call_llm(prompt)
# Simple YAML extraction
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
thought_data = yaml.safe_load(yaml_str) # Can raise YAMLError
# --- Validation (using assert) ---
assert thought_data is not None, "YAML parsing failed, result is None"
assert "current_thinking" in thought_data, "LLM response missing 'current_thinking'"
assert "next_thought_needed" in thought_data, "LLM response missing 'next_thought_needed'"
assert "planning" in thought_data, "LLM response missing 'planning'"
assert isinstance(thought_data.get("planning"), list), "LLM response 'planning' is not a list"
# Optional: Add deeper validation of list items being dicts if needed
# --- End Validation ---
# Add thought number
thought_data["thought_number"] = current_thought_number
return thought_data
def post(self, shared, prep_res, exec_res):
# Add the new thought to the list
if "thoughts" not in shared:
shared["thoughts"] = []
shared["thoughts"].append(exec_res)
# Extract plan for printing using the updated recursive helper function
plan_list = exec_res.get("planning", ["Error: Planning data missing."])
plan_str_formatted = format_plan(plan_list, indent_level=1)
thought_num = exec_res.get('thought_number', 'N/A')
current_thinking = exec_res.get('current_thinking', 'Error: Missing thinking content.')
dedented_thinking = textwrap.dedent(current_thinking).strip()
# Determine if this is the conclusion step based on description
is_conclusion = False
if isinstance(plan_list, list):
# Check if the currently executed step (likely the last 'Done' or the current 'Pending' if evaluation failed) is Conclusion
# This logic is approximate - might need refinement based on how LLM handles status updates
for item in reversed(plan_list): # Check recent items first
if isinstance(item, dict) and item.get('description') == "Conclusion":
# If Conclusion is Done or it's Pending and we are ending, consider it conclusion
if item.get('status') == "Done" or (item.get('status') == "Pending" and not exec_res.get("next_thought_needed", True)):
is_conclusion = True
break
# Simple check, might need nested search if Conclusion could be a sub-step
# Use is_conclusion flag OR the next_thought_needed flag for termination
if not exec_res.get("next_thought_needed", True): # Primary termination signal
shared["solution"] = dedented_thinking # Solution is the thinking content of the final step
print(f"\nThought {thought_num} (Conclusion):")
print(f"{textwrap.indent(dedented_thinking, ' ')}")
print("\nFinal Plan Status:")
print(textwrap.indent(plan_str_formatted, ' '))
print("\n=== FINAL SOLUTION ===")
print(dedented_thinking)
print("======================\n")
return "end"
# Otherwise, continue the chain
print(f"\nThought {thought_num}:")
print(f"{textwrap.indent(dedented_thinking, ' ')}")
print("\nCurrent Plan Status:")
print(textwrap.indent(plan_str_formatted, ' '))
print("-" * 50)
return "continue"
================================================
FILE: cookbook/pocketflow-thinking/requirements.txt
================================================
pocketflow>=0.0.1
anthropic>=0.15.0 # For Claude API access
================================================
FILE: cookbook/pocketflow-thinking/utils.py
================================================
from anthropic import Anthropic
import os
def call_llm(prompt):
client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", "your-api-key"))
response = client.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=6000,
messages=[
{"role": "user", "content": prompt}
]
)
return response.content[0].text
if __name__ == "__main__":
print("## Testing call_llm")
prompt = "In a few words, what is the meaning of life?"
print(f"## Prompt: {prompt}")
response = call_llm(prompt)
print(f"## Response: {response}")
================================================
FILE: cookbook/pocketflow-tool-crawler/README.md
================================================
# Web Crawler with Content Analysis
A web crawler tool built with PocketFlow that crawls websites and analyzes content using LLM.
## Features
- Crawls websites while respecting domain boundaries
- Extracts text content and links from pages
- Analyzes content using GPT-4 to generate:
- Page summaries
- Main topics/keywords
- Content type classification
- Processes pages in batches for efficiency
- Generates a comprehensive analysis report
## Installation
1. Clone the repository
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Set your OpenAI API key:
```bash
export OPENAI_API_KEY='your-api-key'
```
## Usage
Run the crawler:
```bash
python main.py
```
You will be prompted to:
1. Enter the website URL to crawl
2. Specify maximum number of pages to crawl (default: 10)
The tool will then:
1. Crawl the specified website
2. Extract and analyze content using GPT-4
3. Generate a report with findings
## Project Structure
```
pocketflow-tool-crawler/
├── tools/
│ ├── crawler.py # Web crawling functionality
│ └── parser.py # Content analysis using LLM
├── utils/
│ └── call_llm.py # LLM API wrapper
├── nodes.py # PocketFlow nodes
├── flow.py # Flow configuration
├── main.py # Main script
└── requirements.txt # Dependencies
```
## Limitations
- Only crawls within the same domain
- Text content only (no images/media)
- Rate limited by OpenAI API
- Basic error handling
## Dependencies
- pocketflow: Flow-based processing
- requests: HTTP requests
- beautifulsoup4: HTML parsing
- openai: GPT-4 API access
================================================
FILE: cookbook/pocketflow-tool-crawler/flow.py
================================================
from pocketflow import Flow
from nodes import CrawlWebsiteNode, AnalyzeContentBatchNode, GenerateReportNode
def create_flow() -> Flow:
"""Create and configure the crawling flow
Returns:
Flow: Configured flow ready to run
"""
# Create nodes
crawl = CrawlWebsiteNode()
analyze = AnalyzeContentBatchNode()
report = GenerateReportNode()
# Connect nodes
crawl >> analyze >> report
# Create flow starting with crawl
return Flow(start=crawl)
================================================
FILE: cookbook/pocketflow-tool-crawler/main.py
================================================
import os
from flow import create_flow
def main():
"""Run the web crawler flow"""
# Get website URL from user
url = input("Enter website URL to crawl (e.g., https://example.com): ")
if not url:
print("Error: URL is required")
return
# Initialize shared data
shared = {
"base_url": url,
"max_pages": 1
}
# Create and run flow
flow = create_flow()
flow.run(shared)
# Results are in shared["report"]
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-tool-crawler/nodes.py
================================================
from pocketflow import Node, BatchNode
from tools.crawler import WebCrawler
from tools.parser import analyze_site
from typing import List, Dict
class CrawlWebsiteNode(Node):
"""Node to crawl a website and extract content"""
def prep(self, shared):
return shared.get("base_url"), shared.get("max_pages", 10)
def exec(self, inputs):
base_url, max_pages = inputs
if not base_url:
return []
crawler = WebCrawler(base_url, max_pages)
return crawler.crawl()
def post(self, shared, prep_res, exec_res):
shared["crawl_results"] = exec_res
return "default"
class AnalyzeContentBatchNode(BatchNode):
"""Node to analyze crawled content in batches"""
def prep(self, shared):
results = shared.get("crawl_results", [])
# Process in batches of 5 pages
batch_size = 5
return [results[i:i+batch_size] for i in range(0, len(results), batch_size)]
def exec(self, batch):
return analyze_site(batch)
def post(self, shared, prep_res, exec_res_list):
# Flatten results from all batches
all_results = []
for batch_results in exec_res_list:
all_results.extend(batch_results)
shared["analyzed_results"] = all_results
return "default"
class GenerateReportNode(Node):
"""Node to generate a summary report of the analysis"""
def prep(self, shared):
return shared.get("analyzed_results", [])
def exec(self, results):
if not results:
return "No results to report"
report = []
report.append(f"Analysis Report\n")
report.append(f"Total pages analyzed: {len(results)}\n")
for page in results:
report.append(f"\nPage: {page['url']}")
report.append(f"Title: {page['title']}")
analysis = page.get("analysis", {})
report.append(f"Summary: {analysis.get('summary', 'N/A')}")
report.append(f"Topics: {', '.join(analysis.get('topics', []))}")
report.append(f"Content Type: {analysis.get('content_type', 'unknown')}")
report.append("-" * 80)
return "\n".join(report)
def post(self, shared, prep_res, exec_res):
shared["report"] = exec_res
print("\nReport generated:")
print(exec_res)
return "default"
================================================
FILE: cookbook/pocketflow-tool-crawler/requirements.txt
================================================
pocketflow>=0.1.0
requests>=2.31.0
beautifulsoup4>=4.12.0
openai>=1.0.0 # for content analysis
================================================
FILE: cookbook/pocketflow-tool-crawler/tools/crawler.py
================================================
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
from typing import Dict, List, Set
class WebCrawler:
"""Simple web crawler that extracts content and follows links"""
def __init__(self, base_url: str, max_pages: int = 10):
self.base_url = base_url
self.max_pages = max_pages
self.visited: Set[str] = set()
def is_valid_url(self, url: str) -> bool:
"""Check if URL belongs to the same domain"""
base_domain = urlparse(self.base_url).netloc
url_domain = urlparse(url).netloc
return base_domain == url_domain
def extract_page_content(self, url: str) -> Dict:
"""Extract content from a single page"""
try:
response = requests.get(url)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
# Extract main content
content = {
"url": url,
"title": soup.title.string if soup.title else "",
"text": soup.get_text(separator="\n", strip=True),
"links": []
}
# Extract links
for link in soup.find_all("a"):
href = link.get("href")
if href:
absolute_url = urljoin(url, href)
if self.is_valid_url(absolute_url):
content["links"].append(absolute_url)
return content
except Exception as e:
print(f"Error crawling {url}: {str(e)}")
return None
def crawl(self) -> List[Dict]:
"""Crawl website starting from base_url"""
to_visit = [self.base_url]
results = []
while to_visit and len(self.visited) < self.max_pages:
url = to_visit.pop(0)
if url in self.visited:
continue
print(f"Crawling: {url}")
content = self.extract_page_content(url)
if content:
self.visited.add(url)
results.append(content)
# Add new URLs to visit
new_urls = [url for url in content["links"]
if url not in self.visited
and url not in to_visit]
to_visit.extend(new_urls)
return results
================================================
FILE: cookbook/pocketflow-tool-crawler/tools/parser.py
================================================
from typing import Dict, List
from utils.call_llm import call_llm
def analyze_content(content: Dict) -> Dict:
"""Analyze webpage content using LLM
Args:
content (Dict): Webpage content with url, title and text
Returns:
Dict: Analysis results including summary and topics
"""
prompt = f"""
Analyze this webpage content:
Title: {content['title']}
URL: {content['url']}
Content: {content['text'][:2000]} # Limit content length
Please provide:
1. A brief summary (2-3 sentences)
2. Main topics/keywords (up to 5)
3. Content type (article, product page, etc)
Output in YAML format:
```yaml
summary: >
brief summary here
topics:
- topic 1
- topic 2
content_type: type here
```
"""
try:
response = call_llm(prompt)
# Extract YAML between code fences
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
import yaml
analysis = yaml.safe_load(yaml_str)
# Validate required fields
assert "summary" in analysis
assert "topics" in analysis
assert "content_type" in analysis
assert isinstance(analysis["topics"], list)
return analysis
except Exception as e:
print(f"Error analyzing content: {str(e)}")
return {
"summary": "Error analyzing content",
"topics": [],
"content_type": "unknown"
}
def analyze_site(crawl_results: List[Dict]) -> List[Dict]:
"""Analyze all crawled pages
Args:
crawl_results (List[Dict]): List of crawled page contents
Returns:
List[Dict]: Original content with added analysis
"""
analyzed_results = []
for content in crawl_results:
if content and content.get("text"):
analysis = analyze_content(content)
content["analysis"] = analysis
analyzed_results.append(content)
return analyzed_results
================================================
FILE: cookbook/pocketflow-tool-crawler/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-tool-crawler/utils/call_llm.py
================================================
from openai import OpenAI
import os
# Initialize OpenAI client
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def call_llm(prompt: str) -> str:
"""Call OpenAI API to analyze text
Args:
prompt (str): Input prompt for the model
Returns:
str: Model response
"""
try:
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
except Exception as e:
print(f"Error calling LLM API: {str(e)}")
return ""
if __name__ == "__main__":
# Test LLM call
response = call_llm("What is web crawling?")
print("Response:", response)
================================================
FILE: cookbook/pocketflow-tool-database/README.md
================================================
# SQLite Database with PocketFlow
This example demonstrates how to properly integrate SQLite database operations with PocketFlow, focusing on:
1. Clean code organization with separation of concerns:
- Tools layer for database operations (`tools/database.py`)
- Node implementation for PocketFlow integration (`nodes.py`)
- Flow configuration (`flow.py`)
- Safe SQL query execution with parameter binding
2. Best practices for database operations:
- Connection management with proper closing
- SQL injection prevention using parameterized queries
- Error handling and resource cleanup
- Simple schema management
3. Example task management system:
- Database initialization
- Task creation
- Task listing
- Status tracking
## Project Structure
```
pocketflow-tool-database/
├── tools/
│ └── database.py # SQLite database operations
├── nodes.py # PocketFlow node implementation
├── flow.py # Flow configuration
└── main.py # Example usage
```
## Setup
1. Create a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
2. Install dependencies:
```bash
pip install -r requirements.txt
```
## Usage
Run the example:
```bash
python main.py
```
This will:
1. Initialize a SQLite database with a tasks table
2. Create an example task
3. List all tasks in the database
4. Display the results
## Key Concepts Demonstrated
1. **Database Operations**
- Safe connection handling
- Query parameterization
- Schema management
2. **Code Organization**
- Clear separation between database operations and PocketFlow components
- Modular project structure
- Type hints and documentation
3. **PocketFlow Integration**
- Node implementation with prep->exec->post lifecycle
- Flow configuration
- Shared store usage for data passing
## Example Output
```
Database Status: Database initialized
Task Status: Task created successfully
All Tasks:
- ID: 1
Title: Example Task
Description: This is an example task created using PocketFlow
Status: pending
Created: 2024-03-02 12:34:56
```
================================================
FILE: cookbook/pocketflow-tool-database/flow.py
================================================
from pocketflow import Flow
from nodes import InitDatabaseNode, CreateTaskNode, ListTasksNode
def create_database_flow():
"""Create a flow for database operations"""
# Create nodes
init_db = InitDatabaseNode()
create_task = CreateTaskNode()
list_tasks = ListTasksNode()
# Connect nodes
init_db >> create_task >> list_tasks
# Create and return flow
return Flow(start=init_db)
================================================
FILE: cookbook/pocketflow-tool-database/main.py
================================================
from flow import create_database_flow
def main():
# Create the flow
flow = create_database_flow()
# Prepare example task data
shared = {
"task_title": "Example Task",
"task_description": "This is an example task created using PocketFlow"
}
# Run the flow
flow.run(shared)
# Print results
print("Database Status:", shared.get("db_status"))
print("Task Status:", shared.get("task_status"))
print("\nAll Tasks:")
for task in shared.get("tasks", []):
print(f"- ID: {task[0]}")
print(f" Title: {task[1]}")
print(f" Description: {task[2]}")
print(f" Status: {task[3]}")
print(f" Created: {task[4]}")
print()
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-tool-database/nodes.py
================================================
from pocketflow import Node
from tools.database import execute_sql, init_db
class InitDatabaseNode(Node):
"""Node for initializing the database"""
def exec(self, _):
init_db()
return "Database initialized"
def post(self, shared, prep_res, exec_res):
shared["db_status"] = exec_res
return "default"
class CreateTaskNode(Node):
"""Node for creating a new task"""
def prep(self, shared):
return (
shared.get("task_title", ""),
shared.get("task_description", "")
)
def exec(self, inputs):
title, description = inputs
query = "INSERT INTO tasks (title, description) VALUES (?, ?)"
execute_sql(query, (title, description))
return "Task created successfully"
def post(self, shared, prep_res, exec_res):
shared["task_status"] = exec_res
return "default"
class ListTasksNode(Node):
"""Node for listing all tasks"""
def exec(self, _):
query = "SELECT * FROM tasks"
return execute_sql(query)
def post(self, shared, prep_res, exec_res):
shared["tasks"] = exec_res
return "default"
================================================
FILE: cookbook/pocketflow-tool-database/requirements.txt
================================================
pocketflow>=0.1.0
python-dotenv>=0.19.0
================================================
FILE: cookbook/pocketflow-tool-database/tools/database.py
================================================
import sqlite3
from typing import List, Tuple, Any
def execute_sql(query: str, params: Tuple = None) -> List[Tuple[Any, ...]]:
"""Execute a SQL query and return results
Args:
query (str): SQL query to execute
params (tuple, optional): Query parameters to prevent SQL injection
Returns:
list: Query results as a list of tuples
"""
conn = sqlite3.connect("example.db")
try:
cursor = conn.cursor()
if params:
cursor.execute(query, params)
else:
cursor.execute(query)
result = cursor.fetchall()
conn.commit()
return result
finally:
conn.close()
def init_db():
"""Initialize database with example table"""
create_table_sql = """
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
execute_sql(create_table_sql)
================================================
FILE: cookbook/pocketflow-tool-database/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-tool-embeddings/README.md
================================================
# OpenAI Embeddings with PocketFlow
This example demonstrates how to properly integrate OpenAI's text embeddings API with PocketFlow, focusing on:
1. Clean code organization with separation of concerns:
- Tools layer for API interactions (`tools/embeddings.py`)
- Node implementation for PocketFlow integration (`nodes.py`)
- Flow configuration (`flow.py`)
- Centralized environment configuration (`utils/call_llm.py`)
2. Best practices for API key management:
- Using environment variables
- Supporting both `.env` files and system environment variables
- Secure configuration handling
3. Proper project structure:
- Modular code organization
- Clear separation between tools and PocketFlow components
- Reusable OpenAI client configuration
## Project Structure
```
pocketflow-tool-embeddings/
├── tools/
│ └── embeddings.py # OpenAI embeddings API wrapper
├── utils/
│ └── call_llm.py # Centralized OpenAI client configuration
├── nodes.py # PocketFlow node implementation
├── flow.py # Flow configuration
└── main.py # Example usage
```
## Setup
1. Create a virtual environment:
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Set up your OpenAI API key in one of two ways:
a. Using a `.env` file:
```bash
OPENAI_API_KEY=your_api_key_here
```
b. Or as a system environment variable:
```bash
export OPENAI_API_KEY=your_api_key_here
```
## Usage
Run the example:
```bash
python main.py
```
This will:
1. Load the OpenAI API key from environment
2. Create a PocketFlow node to handle embedding generation
3. Process a sample text and generate its embedding
4. Display the embedding dimension and first few values
## Key Concepts Demonstrated
1. **Environment Configuration**
- Secure API key handling
- Flexible configuration options
2. **Code Organization**
- Clear separation between tools and PocketFlow components
- Reusable OpenAI client configuration
- Modular project structure
3. **PocketFlow Integration**
- Node implementation with prep->exec->post lifecycle
- Flow configuration
- Shared store usage for data passing
================================================
FILE: cookbook/pocketflow-tool-embeddings/flow.py
================================================
from pocketflow import Flow
from nodes import EmbeddingNode
def create_embedding_flow():
"""Create a flow for text embedding"""
# Create embedding node
embedding = EmbeddingNode()
# Create and return flow
return Flow(start=embedding)
================================================
FILE: cookbook/pocketflow-tool-embeddings/main.py
================================================
from flow import create_embedding_flow
def main():
# Create the flow
flow = create_embedding_flow()
# Example text
text = "What's the meaning of life?"
# Prepare shared data
shared = {"text": text}
# Run the flow
flow.run(shared)
# Print results
print("Text:", text)
print("Embedding dimension:", len(shared["embedding"]))
print("First 5 values:", shared["embedding"][:5])
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-tool-embeddings/nodes.py
================================================
from pocketflow import Node
from tools.embeddings import get_embedding
class EmbeddingNode(Node):
"""Node for getting embeddings from OpenAI API"""
def prep(self, shared):
# Get text from shared store
return shared.get("text", "")
def exec(self, text):
# Get embedding using tool function
return get_embedding(text)
def post(self, shared, prep_res, exec_res):
# Store embedding in shared store
shared["embedding"] = exec_res
return "default"
================================================
FILE: cookbook/pocketflow-tool-embeddings/requirements.txt
================================================
openai>=1.0.0
numpy>=1.24.0
faiss-cpu>=1.7.0
python-dotenv>=1.0.0
pocketflow>=0.1.0
================================================
FILE: cookbook/pocketflow-tool-embeddings/tools/embeddings.py
================================================
from utils.call_llm import client
def get_embedding(text):
response = client.embeddings.create(
model="text-embedding-ada-002",
input=text
)
return response.data[0].embedding
================================================
FILE: cookbook/pocketflow-tool-embeddings/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-tool-embeddings/utils/call_llm.py
================================================
import os
from openai import OpenAI
# No need for dotenv if using system environment variables
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def call_llm(prompt):
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
if __name__ == "__main__":
prompt = "What is the meaning of life?"
print(call_llm(prompt))
================================================
FILE: cookbook/pocketflow-tool-pdf-vision/README.md
================================================
# PocketFlow Tool: PDF Vision
A PocketFlow example project demonstrating PDF processing with OpenAI's Vision API for OCR and text extraction.
## Features
- Convert PDF pages to images while maintaining quality and size limits
- Extract text from scanned documents using GPT-4 Vision API
- Support for custom extraction prompts
- Maintain page order and formatting in extracted text
- Batch processing of multiple PDFs from a directory
## Installation
1. Clone the repository
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Set your OpenAI API key as an environment variable:
```bash
export OPENAI_API_KEY=your_api_key_here
```
## Usage
1. Place your PDF files in the `pdfs` directory
2. Run the example:
```bash
python main.py
```
The script will process all PDF files in the `pdfs` directory and output the extracted text for each one.
## Project Structure
```
pocketflow-tool-pdf-vision/
├── pdfs/ # Directory for PDF files to process
├── tools/
│ ├── pdf.py # PDF to image conversion
│ └── vision.py # Vision API integration
├── utils/
│ └── call_llm.py # OpenAI client config
├── nodes.py # PocketFlow nodes
├── flow.py # Flow configuration
└── main.py # Example usage
```
## Flow Description
1. **LoadPDFNode**: Loads PDF and converts pages to images
2. **ExtractTextNode**: Processes images with Vision API
3. **CombineResultsNode**: Combines extracted text from all pages
## Customization
You can customize the extraction by modifying the prompt in `shared`:
```python
shared = {
"pdf_path": "your_file.pdf",
"extraction_prompt": "Your custom prompt here"
}
```
## Limitations
- Maximum PDF page size: 2000px (configurable in `tools/pdf.py`)
- Vision API token limit: 1000 tokens per response
- Image size limit: 20MB per image for Vision API
## License
MIT
================================================
FILE: cookbook/pocketflow-tool-pdf-vision/flow.py
================================================
from pocketflow import Flow
from nodes import ProcessPDFBatchNode
def create_vision_flow():
"""Create a flow for batch PDF processing with Vision API"""
return Flow(start=ProcessPDFBatchNode())
================================================
FILE: cookbook/pocketflow-tool-pdf-vision/main.py
================================================
from flow import create_vision_flow
def main():
# Create and run flow
flow = create_vision_flow()
shared = {}
flow.run(shared)
# Print results
if "results" in shared:
for result in shared["results"]:
print(f"\nFile: {result['filename']}")
print("-" * 50)
print(result["text"])
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-tool-pdf-vision/nodes.py
================================================
from pocketflow import Node, BatchNode
from tools.pdf import pdf_to_images
from tools.vision import extract_text_from_image
from typing import List, Dict, Any
from pathlib import Path
import os
class ProcessPDFBatchNode(BatchNode):
"""Node for processing multiple PDFs from a directory"""
def prep(self, shared):
# Get PDF directory path
root_dir = Path(__file__).parent
pdf_dir = root_dir / "pdfs"
# List all PDFs
pdf_files = []
for file in os.listdir(pdf_dir):
if file.lower().endswith('.pdf'):
pdf_files.append({
"pdf_path": str(pdf_dir / file),
"extraction_prompt": shared.get("extraction_prompt",
"Extract all text from this document, preserving formatting and layout.")
})
if not pdf_files:
print("No PDF files found in 'pdfs' directory!")
return []
print(f"Found {len(pdf_files)} PDF files")
return pdf_files
def exec(self, item):
# Create flow for single PDF
flow = create_single_pdf_flow()
# Process PDF
print(f"\nProcessing: {os.path.basename(item['pdf_path'])}")
print("-" * 50)
# Run flow
shared = item.copy()
flow.run(shared)
return {
"filename": os.path.basename(item["pdf_path"]),
"text": shared.get("final_text", "No text extracted")
}
def post(self, shared, prep_res, exec_res_list):
shared["results"] = exec_res_list
return "default"
class LoadPDFNode(Node):
"""Node for loading and converting a single PDF to images"""
def prep(self, shared):
return shared.get("pdf_path", "")
def exec(self, pdf_path):
return pdf_to_images(pdf_path)
def post(self, shared, prep_res, exec_res):
shared["page_images"] = exec_res
return "default"
class ExtractTextNode(Node):
"""Node for extracting text from images using Vision API"""
def prep(self, shared):
return (
shared.get("page_images", []),
shared.get("extraction_prompt", None)
)
def exec(self, inputs):
images, prompt = inputs
results = []
for img, page_num in images:
text = extract_text_from_image(img, prompt)
results.append({
"page": page_num,
"text": text
})
return results
def post(self, shared, prep_res, exec_res):
shared["extracted_text"] = exec_res
return "default"
class CombineResultsNode(Node):
"""Node for combining and formatting extracted text"""
def prep(self, shared):
return shared.get("extracted_text", [])
def exec(self, results):
# Sort by page number
sorted_results = sorted(results, key=lambda x: x["page"])
# Combine text with page numbers
combined = []
for result in sorted_results:
combined.append(f"=== Page {result['page']} ===\n{result['text']}\n")
return "\n".join(combined)
def post(self, shared, prep_res, exec_res):
shared["final_text"] = exec_res
return "default"
def create_single_pdf_flow():
"""Create a flow for processing a single PDF"""
from pocketflow import Flow
# Create nodes
load_pdf = LoadPDFNode()
extract_text = ExtractTextNode()
combine_results = CombineResultsNode()
# Connect nodes
load_pdf >> extract_text >> combine_results
# Create and return flow
return Flow(start=load_pdf)
================================================
FILE: cookbook/pocketflow-tool-pdf-vision/requirements.txt
================================================
pocketflow>=0.1.0
openai>=1.0.0
PyMuPDF>=1.22.0 # for PDF processing
Pillow>=10.0.0 # for image processing
================================================
FILE: cookbook/pocketflow-tool-pdf-vision/tools/pdf.py
================================================
import fitz # PyMuPDF
from PIL import Image
import io
import base64
from typing import List, Tuple
def pdf_to_images(pdf_path: str, max_size: int = 2000) -> List[Tuple[Image.Image, int]]:
"""Convert PDF pages to PIL Images with size limit
Args:
pdf_path (str): Path to PDF file
max_size (int): Maximum dimension (width/height) for images
Returns:
list: List of tuples (PIL Image, page number)
"""
doc = fitz.open(pdf_path)
images = []
try:
for page_num in range(len(doc)):
page = doc[page_num]
pix = page.get_pixmap()
# Convert to PIL Image
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
# Resize if needed while maintaining aspect ratio
if max(img.size) > max_size:
ratio = max_size / max(img.size)
new_size = tuple(int(dim * ratio) for dim in img.size)
img = img.resize(new_size, Image.Resampling.LANCZOS)
images.append((img, page_num + 1))
finally:
doc.close()
return images
def image_to_base64(image: Image.Image) -> str:
"""Convert PIL Image to base64 string
Args:
image (PIL.Image): Image to convert
Returns:
str: Base64 encoded image string
"""
buffer = io.BytesIO()
image.save(buffer, format="PNG")
return base64.b64encode(buffer.getvalue()).decode('utf-8')
================================================
FILE: cookbook/pocketflow-tool-pdf-vision/tools/vision.py
================================================
from PIL import Image
from utils.call_llm import client
from tools.pdf import image_to_base64
def extract_text_from_image(image: Image.Image, prompt: str = None) -> str:
"""Extract text from image using OpenAI Vision API
Args:
image (PIL.Image): Image to process
prompt (str, optional): Custom prompt for extraction. Defaults to general OCR.
Returns:
str: Extracted text from image
"""
# Convert image to base64
img_base64 = image_to_base64(image)
# Default prompt for general OCR
if prompt is None:
prompt = "Please extract all text from this image."
# Call Vision API
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_base64}"}}
]
}]
)
return response.choices[0].message.content
if __name__ == "__main__":
# Test vision processing
test_image = Image.open("example.png")
result = extract_text_from_image(test_image)
print("Extracted text:", result)
================================================
FILE: cookbook/pocketflow-tool-pdf-vision/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-tool-pdf-vision/utils/call_llm.py
================================================
import os
from openai import OpenAI
from pathlib import Path
# Get the project root directory (parent of utils directory)
ROOT_DIR = Path(__file__).parent.parent
# Initialize OpenAI client with API key from environment
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
================================================
FILE: cookbook/pocketflow-tool-search/README.md
================================================
# Web Search with Analysis
A web search tool built with PocketFlow that performs searches using SerpAPI and analyzes results using LLM.
## Features
- Web search using Google via SerpAPI
- Extracts titles, snippets, and links
- Analyzes search results using GPT-4 to provide:
- Result summaries
- Key points/facts
- Suggested follow-up queries
- Clean command-line interface
## Installation
1. Clone the repository
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Set required API keys:
```bash
export SERPAPI_API_KEY='your-serpapi-key'
export OPENAI_API_KEY='your-openai-key'
```
## Usage
Run the search tool:
```bash
python main.py
```
You will be prompted to:
1. Enter your search query
2. Specify number of results to fetch (default: 5)
The tool will then:
1. Perform the search using SerpAPI
2. Analyze results using GPT-4
3. Present a summary with key points and follow-up queries
## Project Structure
```
pocketflow-tool-search/
├── tools/
│ ├── search.py # SerpAPI search functionality
│ └── parser.py # Result analysis using LLM
├── utils/
│ └── call_llm.py # LLM API wrapper
├── nodes.py # PocketFlow nodes
├── flow.py # Flow configuration
├── main.py # Main script
└── requirements.txt # Dependencies
```
## Limitations
- Requires SerpAPI subscription
- Rate limited by both APIs
- Basic error handling
- Text results only
## Dependencies
- pocketflow: Flow-based processing
- google-search-results: SerpAPI client
- openai: GPT-4 API access
- pyyaml: YAML processing
================================================
FILE: cookbook/pocketflow-tool-search/flow.py
================================================
from pocketflow import Flow
from nodes import SearchNode, AnalyzeResultsNode
def create_flow() -> Flow:
"""Create and configure the search flow
Returns:
Flow: Configured flow ready to run
"""
# Create nodes
search = SearchNode()
analyze = AnalyzeResultsNode()
# Connect nodes
search >> analyze
# Create flow starting with search
return Flow(start=search)
================================================
FILE: cookbook/pocketflow-tool-search/main.py
================================================
import os
from flow import create_flow
def main():
"""Run the web search flow"""
# Get search query from user
query = input("Enter search query: ")
if not query:
print("Error: Query is required")
return
# Initialize shared data
shared = {
"query": query,
"num_results": 5
}
# Create and run flow
flow = create_flow()
flow.run(shared)
# Results are in shared["analysis"]
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-tool-search/nodes.py
================================================
from pocketflow import Node
from tools.search import SearchTool
from tools.parser import analyze_results
from typing import List, Dict
class SearchNode(Node):
"""Node to perform web search using SerpAPI"""
def prep(self, shared):
return shared.get("query"), shared.get("num_results", 5)
def exec(self, inputs):
query, num_results = inputs
if not query:
return []
searcher = SearchTool()
return searcher.search(query, num_results)
def post(self, shared, prep_res, exec_res):
shared["search_results"] = exec_res
return "default"
class AnalyzeResultsNode(Node):
"""Node to analyze search results using LLM"""
def prep(self, shared):
return shared.get("query"), shared.get("search_results", [])
def exec(self, inputs):
query, results = inputs
if not results:
return {
"summary": "No search results to analyze",
"key_points": [],
"follow_up_queries": []
}
return analyze_results(query, results)
def post(self, shared, prep_res, exec_res):
shared["analysis"] = exec_res
# Print analysis
print("\nSearch Analysis:")
print("\nSummary:", exec_res["summary"])
print("\nKey Points:")
for point in exec_res["key_points"]:
print(f"- {point}")
print("\nSuggested Follow-up Queries:")
for query in exec_res["follow_up_queries"]:
print(f"- {query}")
return "default"
================================================
FILE: cookbook/pocketflow-tool-search/requirements.txt
================================================
pocketflow>=0.1.0
google-search-results>=2.4.2 # SerpAPI client
openai>=1.0.0 # for search result analysis
pyyaml>=6.0.1 # for structured output
================================================
FILE: cookbook/pocketflow-tool-search/tools/parser.py
================================================
from typing import Dict, List
from utils.call_llm import call_llm
def analyze_results(query: str, results: List[Dict]) -> Dict:
"""Analyze search results using LLM
Args:
query (str): Original search query
results (List[Dict]): Search results to analyze
Returns:
Dict: Analysis including summary and key points
"""
# Format results for prompt
formatted_results = []
for i, result in enumerate(results, 1):
formatted_results.append(f"""
Result {i}:
Title: {result['title']}
Snippet: {result['snippet']}
URL: {result['link']}
""")
prompt = f"""
Analyze these search results for the query: "{query}"
{'\n'.join(formatted_results)}
Please provide:
1. A concise summary of the findings (2-3 sentences)
2. Key points or facts (up to 5 bullet points)
3. Suggested follow-up queries (2-3)
Output in YAML format:
```yaml
summary: >
brief summary here
key_points:
- point 1
- point 2
follow_up_queries:
- query 1
- query 2
```
"""
try:
response = call_llm(prompt)
# Extract YAML between code fences
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
import yaml
analysis = yaml.safe_load(yaml_str)
# Validate required fields
assert "summary" in analysis
assert "key_points" in analysis
assert "follow_up_queries" in analysis
assert isinstance(analysis["key_points"], list)
assert isinstance(analysis["follow_up_queries"], list)
return analysis
except Exception as e:
print(f"Error analyzing results: {str(e)}")
return {
"summary": "Error analyzing results",
"key_points": [],
"follow_up_queries": []
}
================================================
FILE: cookbook/pocketflow-tool-search/tools/search.py
================================================
import os
from serpapi import GoogleSearch
from typing import Dict, List, Optional
class SearchTool:
"""Tool for performing web searches using SerpAPI"""
def __init__(self, api_key: Optional[str] = None):
"""Initialize search tool with API key
Args:
api_key (str, optional): SerpAPI key. Defaults to env var SERPAPI_API_KEY.
"""
self.api_key = api_key or os.getenv("SERPAPI_API_KEY")
if not self.api_key:
raise ValueError("SerpAPI key not found. Set SERPAPI_API_KEY env var.")
def search(self, query: str, num_results: int = 5) -> List[Dict]:
"""Perform Google search via SerpAPI
Args:
query (str): Search query
num_results (int, optional): Number of results to return. Defaults to 5.
Returns:
List[Dict]: Search results with title, snippet, and link
"""
# Configure search parameters
params = {
"engine": "google",
"q": query,
"api_key": self.api_key,
"num": num_results
}
try:
# Execute search
search = GoogleSearch(params)
results = search.get_dict()
# Extract organic results
if "organic_results" not in results:
return []
processed_results = []
for result in results["organic_results"][:num_results]:
processed_results.append({
"title": result.get("title", ""),
"snippet": result.get("snippet", ""),
"link": result.get("link", "")
})
return processed_results
except Exception as e:
print(f"Search error: {str(e)}")
return []
================================================
FILE: cookbook/pocketflow-tool-search/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-tool-search/utils/call_llm.py
================================================
import os
from openai import OpenAI
from pathlib import Path
# Get the project root directory (parent of utils directory)
ROOT_DIR = Path(__file__).parent.parent
# Initialize OpenAI client with API key from environment
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def call_llm(prompt: str) -> str:
"""Call OpenAI API to analyze text
Args:
prompt (str): Input prompt for the model
Returns:
str: Model response
"""
try:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
except Exception as e:
print(f"Error calling LLM API: {str(e)}")
return ""
if __name__ == "__main__":
# Test LLM call
response = call_llm("What is web search?")
print("Response:", response)
================================================
FILE: cookbook/pocketflow-tracing/README.md
================================================
# PocketFlow Tracing with Langfuse
This cookbook provides comprehensive observability for PocketFlow workflows using [Langfuse](https://langfuse.com/) as the tracing backend. With minimal code changes (just adding a decorator), you can automatically trace all node executions, inputs, outputs, and errors in your PocketFlow workflows.
## 🎯 Features
- **Automatic Tracing**: Trace entire flows with a single decorator
- **Node-Level Observability**: Automatically trace `prep`, `exec`, and `post` phases of each node
- **Input/Output Tracking**: Capture all data flowing through your workflow
- **Error Tracking**: Automatically capture and trace exceptions
- **Async Support**: Full support for AsyncFlow and AsyncNode
- **Minimal Code Changes**: Just add `@trace_flow()` to your flow classes
- **Langfuse Integration**: Leverage Langfuse's powerful observability platform
## 🚀 Quick Start
### 1. Install Dependencies
```bash
pip install -r requirements.txt
```
### 2. Environment Setup
Copy the example environment file and configure your Langfuse credentials:
```bash
cp .env.example .env
```
Then edit the `.env` file with your actual Langfuse configuration:
```env
LANGFUSE_SECRET_KEY=your-langfuse-secret-key
LANGFUSE_PUBLIC_KEY=your-langfuse-public-key
LANGFUSE_HOST=your-langfuse-host-url
POCKETFLOW_TRACING_DEBUG=true
```
**Note**: Replace the placeholder values with your actual Langfuse credentials and host URL.
### 3. Basic Usage
```python
from pocketflow import Node, Flow
from tracing import trace_flow
class MyNode(Node):
def prep(self, shared):
return shared["input"]
def exec(self, data):
return f"Processed: {data}"
def post(self, shared, prep_res, exec_res):
shared["output"] = exec_res
return "default"
@trace_flow() # 🎉 That's it! Your flow is now traced
class MyFlow(Flow):
def __init__(self):
super().__init__(start=MyNode())
# Run your flow - tracing happens automatically
flow = MyFlow()
shared = {"input": "Hello World"}
flow.run(shared)
```
## 📊 What Gets Traced
When you apply the `@trace_flow()` decorator, the system automatically traces:
### Flow Level
- **Flow Start/End**: Overall execution time and status
- **Input Data**: Initial shared state when flow starts
- **Output Data**: Final shared state when flow completes
- **Errors**: Any exceptions that occur during flow execution
### Node Level
For each node in your flow, the system traces:
- **prep() Phase**:
- Input: `shared` data
- Output: `prep_res` returned by prep method
- Execution time and any errors
- **exec() Phase**:
- Input: `prep_res` from prep phase
- Output: `exec_res` returned by exec method
- Execution time and any errors
- Retry attempts (if configured)
- **post() Phase**:
- Input: `shared`, `prep_res`, `exec_res`
- Output: Action string returned
- Execution time and any errors
## 🔧 Configuration Options
### Basic Configuration
```python
from tracing import trace_flow, TracingConfig
# Use environment variables (default)
@trace_flow()
class MyFlow(Flow):
pass
# Custom flow name
@trace_flow(flow_name="CustomFlowName")
class MyFlow(Flow):
pass
# Custom session and user IDs
@trace_flow(session_id="session-123", user_id="user-456")
class MyFlow(Flow):
pass
```
### Advanced Configuration
```python
from tracing import TracingConfig
# Create custom configuration
config = TracingConfig(
langfuse_secret_key="your-secret-key",
langfuse_public_key="your-public-key",
langfuse_host="https://your-langfuse-instance.com",
debug=True,
trace_inputs=True,
trace_outputs=True,
trace_errors=True
)
@trace_flow(config=config)
class MyFlow(Flow):
pass
```
## 📁 Examples
### Basic Synchronous Flow
See `examples/basic_example.py` for a complete example of tracing a simple synchronous flow.
```bash
cd examples
python basic_example.py
```
### Asynchronous Flow
See `examples/async_example.py` for tracing AsyncFlow and AsyncNode.
```bash
cd examples
python async_example.py
```
## 🔍 Viewing Traces
After running your traced flows, visit your Langfuse dashboard to view the traces:
**Dashboard URL**: Use the URL you configured in `LANGFUSE_HOST` environment variable
In the dashboard you'll see:
- **Traces**: One trace per flow execution
- **Spans**: Individual node phases (prep, exec, post)
- **Input/Output Data**: All data flowing through your workflow
- **Performance Metrics**: Execution times for each phase
- **Error Details**: Stack traces and error messages
The tracings in examples.

Detailed tracing for a node.

## 🛠️ Advanced Usage
### Custom Tracer Configuration
```python
from tracing import TracingConfig, LangfuseTracer
# Create custom configuration
config = TracingConfig.from_env()
config.debug = True
# Use tracer directly (for advanced use cases)
tracer = LangfuseTracer(config)
```
### Environment Variables
You can customize tracing behavior with these environment variables:
```env
# Required Langfuse configuration
LANGFUSE_SECRET_KEY=your-secret-key
LANGFUSE_PUBLIC_KEY=your-public-key
LANGFUSE_HOST=your-langfuse-host
# Optional tracing configuration
POCKETFLOW_TRACING_DEBUG=true
POCKETFLOW_TRACE_INPUTS=true
POCKETFLOW_TRACE_OUTPUTS=true
POCKETFLOW_TRACE_PREP=true
POCKETFLOW_TRACE_EXEC=true
POCKETFLOW_TRACE_POST=true
POCKETFLOW_TRACE_ERRORS=true
# Optional session/user tracking
POCKETFLOW_SESSION_ID=your-session-id
POCKETFLOW_USER_ID=your-user-id
```
## 🐛 Troubleshooting
### Common Issues
1. **"langfuse package not installed"**
```bash
pip install langfuse
```
2. **"Langfuse client initialization failed"**
- Check your `.env` file configuration
- Verify Langfuse server is running at the specified host
- Check network connectivity
3. **"No traces appearing in dashboard"**
- Ensure `POCKETFLOW_TRACING_DEBUG=true` to see debug output
- Check that your flow is actually being executed
- Verify Langfuse credentials are correct
### Debug Mode
Enable debug mode to see detailed tracing information:
```env
POCKETFLOW_TRACING_DEBUG=true
```
This will print detailed information about:
- Langfuse client initialization
- Trace and span creation
- Data serialization
- Error messages
## 📚 API Reference
### `@trace_flow()`
Decorator to add Langfuse tracing to PocketFlow flows.
**Parameters:**
- `config` (TracingConfig, optional): Custom configuration. If None, loads from environment.
- `flow_name` (str, optional): Custom name for the flow. If None, uses class name.
- `session_id` (str, optional): Session ID for grouping related traces.
- `user_id` (str, optional): User ID for the trace.
### `TracingConfig`
Configuration class for tracing settings.
**Methods:**
- `TracingConfig.from_env()`: Create config from environment variables
- `validate()`: Check if configuration is valid
- `to_langfuse_kwargs()`: Convert to Langfuse client kwargs
### `LangfuseTracer`
Core tracer class for Langfuse integration.
**Methods:**
- `start_trace()`: Start a new trace
- `end_trace()`: End the current trace
- `start_node_span()`: Start a span for node execution
- `end_node_span()`: End a node execution span
- `flush()`: Flush pending traces to Langfuse
## 🤝 Contributing
This cookbook is designed to be a starting point for PocketFlow observability. Feel free to extend and customize it for your specific needs!
## 📄 License
This cookbook follows the same license as PocketFlow.
================================================
FILE: cookbook/pocketflow-tracing/examples/async_example.py
================================================
#!/usr/bin/env python3
"""
Async example demonstrating PocketFlow tracing with Langfuse.
This example shows how to use the @trace_flow decorator with AsyncFlow
and AsyncNode to trace asynchronous workflows.
"""
import asyncio
import sys
import os
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Add parent directory to path to import pocketflow and tracing
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from pocketflow import AsyncNode, AsyncFlow
from tracing import trace_flow, TracingConfig
class AsyncDataFetchNode(AsyncNode):
"""An async node that simulates fetching data."""
async def prep_async(self, shared):
"""Extract the query from shared data."""
query = shared.get("query", "default")
return query
async def exec_async(self, query):
"""Simulate async data fetching."""
print(f"🔍 Fetching data for query: {query}")
# Simulate async operation
await asyncio.sleep(1)
# Return mock data
data = {
"query": query,
"results": [f"Result {i} for {query}" for i in range(3)],
"timestamp": "2024-01-01T00:00:00Z",
}
return data
async def post_async(self, shared, prep_res, exec_res):
"""Store the fetched data."""
shared["fetched_data"] = exec_res
return "process"
class AsyncDataProcessNode(AsyncNode):
"""An async node that processes the fetched data."""
async def prep_async(self, shared):
"""Get the fetched data."""
return shared.get("fetched_data", {})
async def exec_async(self, data):
"""Process the data asynchronously."""
print("⚙️ Processing fetched data...")
# Simulate async processing
await asyncio.sleep(0.5)
# Process the results
processed_results = []
for result in data.get("results", []):
processed_results.append(f"PROCESSED: {result}")
return {
"original_query": data.get("query"),
"processed_results": processed_results,
"result_count": len(processed_results),
}
async def post_async(self, shared, prep_res, exec_res):
"""Store the processed data."""
shared["processed_data"] = exec_res
return "default"
@trace_flow(flow_name="AsyncDataProcessingFlow")
class AsyncDataProcessingFlow(AsyncFlow):
"""An async flow that fetches and processes data."""
def __init__(self):
# Create async nodes
fetch_node = AsyncDataFetchNode()
process_node = AsyncDataProcessNode()
# Connect nodes
fetch_node - "process" >> process_node
# Initialize async flow
super().__init__(start=fetch_node)
async def main():
"""Run the async tracing example."""
print("🚀 Starting PocketFlow Async Tracing Example")
print("=" * 50)
# Create the async flow
flow = AsyncDataProcessingFlow()
# Prepare shared data
shared = {"query": "machine learning tutorials"}
print(f"📥 Input: {shared}")
# Run the async flow (this will be automatically traced)
try:
result = await flow.run_async(shared)
print(f"📤 Output: {shared}")
print(f"🎯 Result: {result}")
print("✅ Async flow completed successfully!")
# Print the processed data
if "processed_data" in shared:
processed = shared["processed_data"]
print(
f"🎉 Processed {processed['result_count']} results for query: {processed['original_query']}"
)
for result in processed["processed_results"]:
print(f" - {result}")
except Exception as e:
print(f"❌ Async flow failed with error: {e}")
raise
print("\n📊 Check your Langfuse dashboard to see the async trace!")
langfuse_host = os.getenv("LANGFUSE_HOST", "your-langfuse-host")
print(f" Dashboard URL: {langfuse_host}")
if __name__ == "__main__":
asyncio.run(main())
================================================
FILE: cookbook/pocketflow-tracing/examples/basic_example.py
================================================
#!/usr/bin/env python3
"""
Basic example demonstrating PocketFlow tracing with Langfuse.
This example shows how to use the @trace_flow decorator to automatically
trace a simple PocketFlow workflow.
"""
import sys
import os
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Add parent directory to path to import pocketflow and tracing
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", ".."))
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from pocketflow import Node, Flow
from tracing import trace_flow, TracingConfig
class GreetingNode(Node):
"""A simple node that creates a greeting message."""
def prep(self, shared):
"""Extract the name from shared data."""
name = shared.get("name", "World")
return name
def exec(self, name):
"""Create a greeting message."""
greeting = f"Hello, {name}!"
return greeting
def post(self, shared, prep_res, exec_res):
"""Store the greeting in shared data."""
shared["greeting"] = exec_res
return "default"
class UppercaseNode(Node):
"""A node that converts the greeting to uppercase."""
def prep(self, shared):
"""Get the greeting from shared data."""
return shared.get("greeting", "")
def exec(self, greeting):
"""Convert to uppercase."""
return greeting.upper()
def post(self, shared, prep_res, exec_res):
"""Store the uppercase greeting."""
shared["uppercase_greeting"] = exec_res
return "default"
@trace_flow(flow_name="BasicGreetingFlow")
class BasicGreetingFlow(Flow):
"""A simple flow that creates and processes a greeting."""
def __init__(self):
# Create nodes
greeting_node = GreetingNode()
uppercase_node = UppercaseNode()
# Connect nodes
greeting_node >> uppercase_node
# Initialize flow
super().__init__(start=greeting_node)
def main():
"""Run the basic tracing example."""
print("🚀 Starting PocketFlow Tracing Basic Example")
print("=" * 50)
# Create the flow
flow = BasicGreetingFlow()
# Prepare shared data
shared = {"name": "PocketFlow User"}
print(f"📥 Input: {shared}")
# Run the flow (this will be automatically traced)
try:
result = flow.run(shared)
print(f"📤 Output: {shared}")
print(f"🎯 Result: {result}")
print("✅ Flow completed successfully!")
# Print the final greeting
if "uppercase_greeting" in shared:
print(f"🎉 Final greeting: {shared['uppercase_greeting']}")
except Exception as e:
print(f"❌ Flow failed with error: {e}")
raise
print("\n📊 Check your Langfuse dashboard to see the trace!")
langfuse_host = os.getenv("LANGFUSE_HOST", "your-langfuse-host")
print(f" Dashboard URL: {langfuse_host}")
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-tracing/requirements.txt
================================================
# Core dependencies for PocketFlow tracing
langfuse>=2.0.0,<3.0.0 # v2 low level SDK compatible with Langfuse servers
python-dotenv>=1.0.0
# Optional dependencies for enhanced functionality
pydantic>=2.0.0 # For data validation and serialization
================================================
FILE: cookbook/pocketflow-tracing/setup.py
================================================
#!/usr/bin/env python3
"""
Setup script for PocketFlow Tracing cookbook.
This script helps install dependencies and verify the setup.
"""
import subprocess
import sys
import os
def install_dependencies():
"""Install required dependencies."""
print("📦 Installing dependencies...")
try:
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]
)
print("✅ Dependencies installed successfully!")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Failed to install dependencies: {e}")
return False
def verify_setup():
"""Verify that the setup is working."""
print("🔍 Verifying setup...")
try:
# Try to import the tracing module
from tracing import trace_flow, TracingConfig
print("✅ Tracing module imported successfully!")
# Try to load configuration
config = TracingConfig.from_env()
if config.validate():
print("✅ Configuration is valid!")
else:
print("⚠️ Configuration validation failed - check your .env file")
return True
except ImportError as e:
print(f"❌ Failed to import tracing module: {e}")
return False
except Exception as e:
print(f"❌ Setup verification failed: {e}")
return False
def main():
"""Main setup function."""
print("🚀 PocketFlow Tracing Setup")
print("=" * 40)
# Check if we're in the right directory
if not os.path.exists("requirements.txt"):
print(
"❌ requirements.txt not found. Please run this script from the pocketflow-tracing directory."
)
sys.exit(1)
# Install dependencies
if not install_dependencies():
sys.exit(1)
# Verify setup
if not verify_setup():
sys.exit(1)
print("\n🎉 Setup completed successfully!")
print("\n📚 Next steps:")
print("1. Check the README.md for usage instructions")
print("2. Run the examples: python examples/basic_example.py")
print("3. Run the test suite: python test_tracing.py")
print("4. Check your Langfuse dashboard (URL configured in LANGFUSE_HOST)")
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-tracing/test_tracing.py
================================================
#!/usr/bin/env python3
"""
Test script for PocketFlow tracing functionality.
This script tests the tracing implementation to ensure it works correctly
with Langfuse integration.
"""
import sys
import os
import asyncio
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
# Add paths for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
sys.path.insert(0, os.path.dirname(__file__))
from pocketflow import Node, Flow, AsyncNode, AsyncFlow
from tracing import trace_flow, TracingConfig
from utils import setup_tracing
class TestNode(Node):
"""Simple test node for tracing verification."""
def prep(self, shared):
"""Test prep phase."""
return shared.get("input", "test_input")
def exec(self, prep_res):
"""Test exec phase."""
return f"processed_{prep_res}"
def post(self, shared, prep_res, exec_res):
"""Test post phase."""
shared["output"] = exec_res
return "default"
class TestAsyncNode(AsyncNode):
"""Simple async test node for tracing verification."""
async def prep_async(self, shared):
"""Test async prep phase."""
await asyncio.sleep(0.1) # Simulate async work
return shared.get("input", "async_test_input")
async def exec_async(self, prep_res):
"""Test async exec phase."""
await asyncio.sleep(0.1) # Simulate async work
return f"async_processed_{prep_res}"
async def post_async(self, shared, prep_res, exec_res):
"""Test async post phase."""
shared["output"] = exec_res
return "default"
@trace_flow(flow_name="TestSyncFlow")
class TestSyncFlow(Flow):
"""Test synchronous flow with tracing."""
def __init__(self):
super().__init__(start=TestNode())
@trace_flow(flow_name="TestAsyncFlow")
class TestAsyncFlow(AsyncFlow):
"""Test asynchronous flow with tracing."""
def __init__(self):
super().__init__(start=TestAsyncNode())
def test_sync_flow():
"""Test synchronous flow tracing."""
print("🧪 Testing synchronous flow tracing...")
flow = TestSyncFlow()
shared = {"input": "sync_test_data"}
print(f" Input: {shared}")
result = flow.run(shared)
print(f" Output: {shared}")
print(f" Result: {result}")
# Verify the flow worked
assert "output" in shared
assert shared["output"] == "processed_sync_test_data"
print(" ✅ Sync flow test passed")
async def test_async_flow():
"""Test asynchronous flow tracing."""
print("🧪 Testing asynchronous flow tracing...")
flow = TestAsyncFlow()
shared = {"input": "async_test_data"}
print(f" Input: {shared}")
result = await flow.run_async(shared)
print(f" Output: {shared}")
print(f" Result: {result}")
# Verify the flow worked
assert "output" in shared
assert shared["output"] == "async_processed_async_test_data"
print(" ✅ Async flow test passed")
def test_configuration():
"""Test configuration loading and validation."""
print("🧪 Testing configuration...")
# Test loading from environment
config = TracingConfig.from_env()
print(f" Loaded config: debug={config.debug}")
# Test validation
is_valid = config.validate()
print(f" Config valid: {is_valid}")
if is_valid:
print(" ✅ Configuration test passed")
else:
print(
" ⚠️ Configuration test failed (this may be expected if env vars not set)"
)
def test_error_handling():
"""Test error handling in traced flows."""
print("🧪 Testing error handling...")
class ErrorNode(Node):
def exec(self, prep_res):
raise ValueError("Test error for tracing")
@trace_flow(flow_name="TestErrorFlow")
class ErrorFlow(Flow):
def __init__(self):
super().__init__(start=ErrorNode())
flow = ErrorFlow()
shared = {"input": "error_test"}
try:
flow.run(shared)
print(" ❌ Expected error but flow succeeded")
except ValueError as e:
print(f" ✅ Error correctly caught and traced: {e}")
except Exception as e:
print(f" ⚠️ Unexpected error type: {e}")
async def main():
"""Run all tests."""
print("🚀 Starting PocketFlow Tracing Tests")
print("=" * 50)
# Test configuration first
test_configuration()
print()
# Test setup (optional - only if environment is configured)
try:
print("🔧 Testing setup...")
config = setup_tracing()
print(" ✅ Setup test passed")
except Exception as e:
print(f" ⚠️ Setup test failed: {e}")
print(" (This is expected if Langfuse is not configured)")
print()
# Test sync flow
test_sync_flow()
print()
# Test async flow
await test_async_flow()
print()
# Test error handling
test_error_handling()
print()
print("🎉 All tests completed!")
print("\n📊 If Langfuse is configured, check your dashboard for traces:")
langfuse_host = os.getenv("LANGFUSE_HOST", "your-langfuse-host")
print(f" Dashboard URL: {langfuse_host}")
if __name__ == "__main__":
asyncio.run(main())
================================================
FILE: cookbook/pocketflow-tracing/tracing/__init__.py
================================================
"""
PocketFlow Tracing Module
This module provides observability and tracing capabilities for PocketFlow workflows
using Langfuse as the backend. It includes decorators and utilities to automatically
trace node execution, inputs, and outputs.
"""
from .config import TracingConfig
from .core import LangfuseTracer
from .decorator import trace_flow
__all__ = ["trace_flow", "TracingConfig", "LangfuseTracer"]
================================================
FILE: cookbook/pocketflow-tracing/tracing/config.py
================================================
"""
Configuration module for PocketFlow tracing with Langfuse.
"""
import os
from dataclasses import dataclass
from typing import Optional
from dotenv import load_dotenv
@dataclass
class TracingConfig:
"""Configuration class for PocketFlow tracing with Langfuse."""
# Langfuse configuration
langfuse_secret_key: Optional[str] = None
langfuse_public_key: Optional[str] = None
langfuse_host: Optional[str] = None
# PocketFlow tracing configuration
debug: bool = False
trace_inputs: bool = True
trace_outputs: bool = True
trace_prep: bool = True
trace_exec: bool = True
trace_post: bool = True
trace_errors: bool = True
# Session configuration
session_id: Optional[str] = None
user_id: Optional[str] = None
@classmethod
def from_env(cls, env_file: Optional[str] = None) -> "TracingConfig":
"""
Create TracingConfig from environment variables.
Args:
env_file: Optional path to .env file. If None, looks for .env in current directory.
Returns:
TracingConfig instance with values from environment variables.
"""
# Load environment variables from .env file if it exists
if env_file:
load_dotenv(env_file)
else:
# Try to find .env file in current directory or parent directories
load_dotenv()
return cls(
langfuse_secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
langfuse_public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
langfuse_host=os.getenv("LANGFUSE_HOST"),
debug=os.getenv("POCKETFLOW_TRACING_DEBUG", "false").lower() == "true",
trace_inputs=os.getenv("POCKETFLOW_TRACE_INPUTS", "true").lower() == "true",
trace_outputs=os.getenv("POCKETFLOW_TRACE_OUTPUTS", "true").lower() == "true",
trace_prep=os.getenv("POCKETFLOW_TRACE_PREP", "true").lower() == "true",
trace_exec=os.getenv("POCKETFLOW_TRACE_EXEC", "true").lower() == "true",
trace_post=os.getenv("POCKETFLOW_TRACE_POST", "true").lower() == "true",
trace_errors=os.getenv("POCKETFLOW_TRACE_ERRORS", "true").lower() == "true",
session_id=os.getenv("POCKETFLOW_SESSION_ID"),
user_id=os.getenv("POCKETFLOW_USER_ID"),
)
def validate(self) -> bool:
"""
Validate that required configuration is present.
Returns:
True if configuration is valid, False otherwise.
"""
if not self.langfuse_secret_key:
if self.debug:
print("Warning: LANGFUSE_SECRET_KEY not set")
return False
if not self.langfuse_public_key:
if self.debug:
print("Warning: LANGFUSE_PUBLIC_KEY not set")
return False
if not self.langfuse_host:
if self.debug:
print("Warning: LANGFUSE_HOST not set")
return False
return True
def to_langfuse_kwargs(self) -> dict:
"""
Convert configuration to kwargs for Langfuse client initialization.
Returns:
Dictionary of kwargs for Langfuse client.
"""
kwargs = {}
if self.langfuse_secret_key:
kwargs["secret_key"] = self.langfuse_secret_key
if self.langfuse_public_key:
kwargs["public_key"] = self.langfuse_public_key
if self.langfuse_host:
kwargs["host"] = self.langfuse_host
if self.debug:
kwargs["debug"] = True
return kwargs
================================================
FILE: cookbook/pocketflow-tracing/tracing/core.py
================================================
"""
Core tracing functionality for PocketFlow with Langfuse integration.
"""
import json
import time
import uuid
from typing import Any, Dict, Optional, Union
from datetime import datetime
try:
from langfuse import Langfuse
LANGFUSE_AVAILABLE = True
except ImportError:
LANGFUSE_AVAILABLE = False
print("Warning: langfuse package not installed. Install with: pip install langfuse")
from .config import TracingConfig
class LangfuseTracer:
"""
Core tracer class that handles Langfuse integration for PocketFlow.
"""
def __init__(self, config: TracingConfig):
"""
Initialize the LangfuseTracer.
Args:
config: TracingConfig instance with Langfuse settings.
"""
self.config = config
self.client = None
self.current_trace = None
self.spans = {} # Store spans by node ID
if LANGFUSE_AVAILABLE and config.validate():
try:
# Initialize Langfuse client with proper parameters
kwargs = {}
if config.langfuse_secret_key:
kwargs["secret_key"] = config.langfuse_secret_key
if config.langfuse_public_key:
kwargs["public_key"] = config.langfuse_public_key
if config.langfuse_host:
kwargs["host"] = config.langfuse_host
if config.debug:
kwargs["debug"] = True
self.client = Langfuse(**kwargs)
if config.debug:
print(
f"✓ Langfuse client initialized with host: {config.langfuse_host}"
)
except Exception as e:
if config.debug:
print(f"✗ Failed to initialize Langfuse client: {e}")
self.client = None
else:
if config.debug:
print("✗ Langfuse not available or configuration invalid")
def start_trace(self, flow_name: str, input_data: Dict[str, Any]) -> Optional[str]:
"""
Start a new trace for a flow execution.
Args:
flow_name: Name of the flow being traced.
input_data: Input data for the flow.
Returns:
Trace ID if successful, None otherwise.
"""
if not self.client:
return None
try:
# Serialize input data safely
serialized_input = self._serialize_data(input_data)
# Use Langfuse v2 API to create a trace
self.current_trace = self.client.trace(
name=flow_name,
input=serialized_input,
metadata={
"framework": "PocketFlow",
"trace_type": "flow_execution",
"timestamp": datetime.now().isoformat(),
},
session_id=self.config.session_id,
user_id=self.config.user_id,
)
# Get the trace ID
trace_id = self.current_trace.id
if self.config.debug:
print(f"✓ Started trace: {trace_id} for flow: {flow_name}")
return trace_id
except Exception as e:
if self.config.debug:
print(f"✗ Failed to start trace: {e}")
return None
def end_trace(self, output_data: Dict[str, Any], status: str = "success") -> None:
"""
End the current trace.
Args:
output_data: Output data from the flow.
status: Status of the trace execution.
"""
if not self.current_trace:
return
try:
# Serialize output data safely
serialized_output = self._serialize_data(output_data)
# Update the trace with output data using v2 API
self.current_trace.update(
output=serialized_output,
metadata={
"status": status,
"end_timestamp": datetime.now().isoformat(),
},
)
if self.config.debug:
print(f"✓ Ended trace with status: {status}")
except Exception as e:
if self.config.debug:
print(f"✗ Failed to end trace: {e}")
finally:
self.current_trace = None
self.spans.clear()
def start_node_span(
self, node_name: str, node_id: str, phase: str
) -> Optional[str]:
"""
Start a span for a node execution phase.
Args:
node_name: Name/type of the node.
node_id: Unique identifier for the node instance.
phase: Execution phase (prep, exec, post).
Returns:
Span ID if successful, None otherwise.
"""
if not self.current_trace:
return None
try:
span_id = f"{node_id}_{phase}"
# Create a child span using v2 API
span = self.current_trace.span(
name=f"{node_name}.{phase}",
metadata={
"node_type": node_name,
"node_id": node_id,
"phase": phase,
"start_timestamp": datetime.now().isoformat(),
},
)
self.spans[span_id] = span
if self.config.debug:
print(f"✓ Started span: {span_id}")
return span_id
except Exception as e:
if self.config.debug:
print(f"✗ Failed to start span: {e}")
return None
def end_node_span(
self,
span_id: str,
input_data: Any = None,
output_data: Any = None,
error: Exception = None,
) -> None:
"""
End a node execution span.
Args:
span_id: ID of the span to end.
input_data: Input data for the phase.
output_data: Output data from the phase.
error: Exception if the phase failed.
"""
if span_id not in self.spans:
return
try:
span = self.spans[span_id]
# Prepare update data
update_data = {}
if input_data is not None and self.config.trace_inputs:
update_data["input"] = self._serialize_data(input_data)
if output_data is not None and self.config.trace_outputs:
update_data["output"] = self._serialize_data(output_data)
if error and self.config.trace_errors:
update_data.update(
{
"level": "ERROR",
"status_message": str(error),
"metadata": {
"error_type": type(error).__name__,
"error_message": str(error),
"end_timestamp": datetime.now().isoformat(),
},
}
)
else:
update_data.update(
{
"level": "DEFAULT",
"metadata": {"end_timestamp": datetime.now().isoformat()},
}
)
# Update the span with all data at once
span.update(**update_data)
# End the span
span.end()
if self.config.debug:
status = "ERROR" if error else "SUCCESS"
print(f"✓ Ended span: {span_id} with status: {status}")
except Exception as e:
if self.config.debug:
print(f"✗ Failed to end span: {e}")
finally:
if span_id in self.spans:
del self.spans[span_id]
def _serialize_data(self, data: Any) -> Any:
"""
Safely serialize data for Langfuse.
Args:
data: Data to serialize.
Returns:
Serialized data that can be sent to Langfuse.
"""
try:
# Handle common PocketFlow data types
if hasattr(data, "__dict__"):
# Convert objects to dict representation
return {"_type": type(data).__name__, "_data": str(data)}
elif isinstance(data, (dict, list, str, int, float, bool, type(None))):
# JSON-serializable types
return data
else:
# Fallback to string representation
return {"_type": type(data).__name__, "_data": str(data)}
except Exception:
# Ultimate fallback
return {"_type": "unknown", "_data": ""}
def flush(self) -> None:
"""Flush any pending traces to Langfuse."""
if self.client:
try:
self.client.flush()
if self.config.debug:
print("✓ Flushed traces to Langfuse")
except Exception as e:
if self.config.debug:
print(f"✗ Failed to flush traces: {e}")
================================================
FILE: cookbook/pocketflow-tracing/tracing/decorator.py
================================================
"""
Decorator for tracing PocketFlow workflows with Langfuse.
"""
import functools
import inspect
import uuid
from typing import Any, Callable, Dict, Optional, Union
from .config import TracingConfig
from .core import LangfuseTracer
def trace_flow(
config: Optional[TracingConfig] = None,
flow_name: Optional[str] = None,
session_id: Optional[str] = None,
user_id: Optional[str] = None
):
"""
Decorator to add Langfuse tracing to PocketFlow flows.
This decorator automatically traces:
- Flow execution start/end
- Each node's prep, exec, and post phases
- Input and output data for each phase
- Errors and exceptions
Args:
config: TracingConfig instance. If None, loads from environment.
flow_name: Custom name for the flow. If None, uses the flow class name.
session_id: Session ID for grouping related traces.
user_id: User ID for the trace.
Returns:
Decorated flow class or function.
Example:
```python
from tracing import trace_flow
@trace_flow()
class MyFlow(Flow):
def __init__(self):
super().__init__(start=MyNode())
# Or with custom configuration
config = TracingConfig.from_env()
@trace_flow(config=config, flow_name="CustomFlow")
class MyFlow(Flow):
pass
```
"""
def decorator(flow_class_or_func):
# Handle both class and function decoration
if inspect.isclass(flow_class_or_func):
return _trace_flow_class(flow_class_or_func, config, flow_name, session_id, user_id)
else:
return _trace_flow_function(flow_class_or_func, config, flow_name, session_id, user_id)
return decorator
def _trace_flow_class(flow_class, config, flow_name, session_id, user_id):
"""Trace a Flow class by wrapping its methods."""
# Get or create config
if config is None:
config = TracingConfig.from_env()
# Override session/user if provided
if session_id:
config.session_id = session_id
if user_id:
config.user_id = user_id
# Get flow name
if flow_name is None:
flow_name = flow_class.__name__
# Store original methods
original_init = flow_class.__init__
original_run = getattr(flow_class, 'run', None)
original_run_async = getattr(flow_class, 'run_async', None)
def traced_init(self, *args, **kwargs):
"""Initialize the flow with tracing capabilities."""
# Call original init
original_init(self, *args, **kwargs)
# Add tracing attributes
self._tracer = LangfuseTracer(config)
self._flow_name = flow_name
self._trace_id = None
# Patch all nodes in the flow
self._patch_nodes()
def traced_run(self, shared):
"""Traced version of the run method."""
if not hasattr(self, '_tracer'):
# Fallback if not properly initialized
return original_run(self, shared) if original_run else None
# Start trace
self._trace_id = self._tracer.start_trace(self._flow_name, shared)
try:
# Run the original flow
result = original_run(self, shared) if original_run else None
# End trace successfully
self._tracer.end_trace(shared, "success")
return result
except Exception as e:
# End trace with error
self._tracer.end_trace(shared, "error")
raise
finally:
# Ensure cleanup
self._tracer.flush()
async def traced_run_async(self, shared):
"""Traced version of the async run method."""
if not hasattr(self, '_tracer'):
# Fallback if not properly initialized
return await original_run_async(self, shared) if original_run_async else None
# Start trace
self._trace_id = self._tracer.start_trace(self._flow_name, shared)
try:
# Run the original flow
result = await original_run_async(self, shared) if original_run_async else None
# End trace successfully
self._tracer.end_trace(shared, "success")
return result
except Exception as e:
# End trace with error
self._tracer.end_trace(shared, "error")
raise
finally:
# Ensure cleanup
self._tracer.flush()
def patch_nodes(self):
"""Patch all nodes in the flow to add tracing."""
if not hasattr(self, 'start_node') or not self.start_node:
return
visited = set()
nodes_to_patch = [self.start_node]
while nodes_to_patch:
node = nodes_to_patch.pop(0)
if id(node) in visited:
continue
visited.add(id(node))
# Patch this node
self._patch_node(node)
# Add successors to patch list
if hasattr(node, 'successors'):
for successor in node.successors.values():
if successor and id(successor) not in visited:
nodes_to_patch.append(successor)
def patch_node(self, node):
"""Patch a single node to add tracing."""
if hasattr(node, '_pocketflow_traced'):
return # Already patched
node_id = str(uuid.uuid4())
node_name = type(node).__name__
# Store original methods
original_prep = getattr(node, 'prep', None)
original_exec = getattr(node, 'exec', None)
original_post = getattr(node, 'post', None)
original_prep_async = getattr(node, 'prep_async', None)
original_exec_async = getattr(node, 'exec_async', None)
original_post_async = getattr(node, 'post_async', None)
# Create traced versions
if original_prep:
node.prep = self._create_traced_method(original_prep, node_id, node_name, 'prep')
if original_exec:
node.exec = self._create_traced_method(original_exec, node_id, node_name, 'exec')
if original_post:
node.post = self._create_traced_method(original_post, node_id, node_name, 'post')
if original_prep_async:
node.prep_async = self._create_traced_async_method(original_prep_async, node_id, node_name, 'prep')
if original_exec_async:
node.exec_async = self._create_traced_async_method(original_exec_async, node_id, node_name, 'exec')
if original_post_async:
node.post_async = self._create_traced_async_method(original_post_async, node_id, node_name, 'post')
# Mark as traced
node._pocketflow_traced = True
def create_traced_method(self, original_method, node_id, node_name, phase):
"""Create a traced version of a synchronous method."""
@functools.wraps(original_method)
def traced_method(*args, **kwargs):
span_id = self._tracer.start_node_span(node_name, node_id, phase)
try:
result = original_method(*args, **kwargs)
self._tracer.end_node_span(span_id, input_data=args, output_data=result)
return result
except Exception as e:
self._tracer.end_node_span(span_id, input_data=args, error=e)
raise
return traced_method
def create_traced_async_method(self, original_method, node_id, node_name, phase):
"""Create a traced version of an asynchronous method."""
@functools.wraps(original_method)
async def traced_async_method(*args, **kwargs):
span_id = self._tracer.start_node_span(node_name, node_id, phase)
try:
result = await original_method(*args, **kwargs)
self._tracer.end_node_span(span_id, input_data=args, output_data=result)
return result
except Exception as e:
self._tracer.end_node_span(span_id, input_data=args, error=e)
raise
return traced_async_method
# Replace methods on the class
flow_class.__init__ = traced_init
flow_class._patch_nodes = patch_nodes
flow_class._patch_node = patch_node
flow_class._create_traced_method = create_traced_method
flow_class._create_traced_async_method = create_traced_async_method
if original_run:
flow_class.run = traced_run
if original_run_async:
flow_class.run_async = traced_run_async
return flow_class
def _trace_flow_function(flow_func, config, flow_name, session_id, user_id):
"""Trace a flow function (for functional-style flows)."""
# Get or create config
if config is None:
config = TracingConfig.from_env()
# Override session/user if provided
if session_id:
config.session_id = session_id
if user_id:
config.user_id = user_id
# Get flow name
if flow_name is None:
flow_name = flow_func.__name__
tracer = LangfuseTracer(config)
@functools.wraps(flow_func)
def traced_flow_func(*args, **kwargs):
# Assume first argument is shared data
shared = args[0] if args else {}
# Start trace
trace_id = tracer.start_trace(flow_name, shared)
try:
result = flow_func(*args, **kwargs)
tracer.end_trace(shared, "success")
return result
except Exception as e:
tracer.end_trace(shared, "error")
raise
finally:
tracer.flush()
return traced_flow_func
================================================
FILE: cookbook/pocketflow-tracing/utils/__init__.py
================================================
"""
Utility functions for PocketFlow tracing.
"""
from .setup import setup_tracing, test_langfuse_connection
__all__ = ['setup_tracing', 'test_langfuse_connection']
================================================
FILE: cookbook/pocketflow-tracing/utils/setup.py
================================================
"""
Setup and testing utilities for PocketFlow tracing.
"""
import os
import sys
from typing import Optional
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
try:
from langfuse import Langfuse
LANGFUSE_AVAILABLE = True
except ImportError:
LANGFUSE_AVAILABLE = False
from tracing import TracingConfig, LangfuseTracer
def setup_tracing(env_file: Optional[str] = None) -> TracingConfig:
"""
Set up tracing configuration and validate the setup.
Args:
env_file: Optional path to .env file. If None, uses default location.
Returns:
TracingConfig instance.
Raises:
RuntimeError: If setup fails.
"""
print("🔧 Setting up PocketFlow tracing...")
# Check if langfuse is installed
if not LANGFUSE_AVAILABLE:
raise RuntimeError(
"Langfuse package not installed. Install with: pip install langfuse"
)
# Load configuration
if env_file:
config = TracingConfig.from_env(env_file)
print(f"✓ Loaded configuration from: {env_file}")
else:
config = TracingConfig.from_env()
print("✓ Loaded configuration from environment")
# Validate configuration
if not config.validate():
raise RuntimeError(
"Invalid tracing configuration. Please check your environment variables:\n"
"- LANGFUSE_SECRET_KEY\n"
"- LANGFUSE_PUBLIC_KEY\n"
"- LANGFUSE_HOST"
)
print("✓ Configuration validated")
# Test connection
if test_langfuse_connection(config):
print("✓ Langfuse connection successful")
else:
raise RuntimeError("Failed to connect to Langfuse. Check your configuration and network.")
print("🎉 PocketFlow tracing setup complete!")
return config
def test_langfuse_connection(config: TracingConfig) -> bool:
"""
Test connection to Langfuse.
Args:
config: TracingConfig instance.
Returns:
True if connection successful, False otherwise.
"""
try:
# Create a test tracer
tracer = LangfuseTracer(config)
if not tracer.client:
return False
# Try to start and end a test trace
trace_id = tracer.start_trace("test_connection", {"test": True})
if trace_id:
tracer.end_trace({"test": "completed"}, "success")
tracer.flush()
return True
return False
except Exception as e:
if config.debug:
print(f"Connection test failed: {e}")
return False
def print_configuration_help():
"""Print help information for configuring tracing."""
print("""
🔧 PocketFlow Tracing Configuration Help
To use PocketFlow tracing, you need to configure Langfuse credentials.
1. Create or update your .env file with:
LANGFUSE_SECRET_KEY=your-secret-key
LANGFUSE_PUBLIC_KEY=your-public-key
LANGFUSE_HOST=your-langfuse-host
POCKETFLOW_TRACING_DEBUG=true
2. Optional configuration:
POCKETFLOW_TRACE_INPUTS=true
POCKETFLOW_TRACE_OUTPUTS=true
POCKETFLOW_TRACE_PREP=true
POCKETFLOW_TRACE_EXEC=true
POCKETFLOW_TRACE_POST=true
POCKETFLOW_TRACE_ERRORS=true
POCKETFLOW_SESSION_ID=your-session-id
POCKETFLOW_USER_ID=your-user-id
3. Install required packages:
pip install -r requirements.txt
4. Test your setup:
python -c "from utils import setup_tracing; setup_tracing()"
""")
if __name__ == "__main__":
"""Command-line interface for setup and testing."""
import argparse
parser = argparse.ArgumentParser(description="PocketFlow Tracing Setup")
parser.add_argument("--test", action="store_true", help="Test Langfuse connection")
parser.add_argument("--help-config", action="store_true", help="Show configuration help")
parser.add_argument("--env-file", type=str, help="Path to .env file")
args = parser.parse_args()
if args.help_config:
print_configuration_help()
sys.exit(0)
if args.test:
try:
config = setup_tracing(args.env_file)
print("\n✅ All tests passed! Your tracing setup is ready.")
except Exception as e:
print(f"\n❌ Setup failed: {e}")
print("\nFor help with configuration, run:")
print("python utils/setup.py --help-config")
sys.exit(1)
else:
print_configuration_help()
================================================
FILE: cookbook/pocketflow-visualization/README.md
================================================
# PocketFlow Visualization
This directory contains tools for visualizing PocketFlow workflow graphs using interactive D3.js visualizations.
## Overview
The visualization tools allow you to:
1. View PocketFlow nodes and flows as an interactive graph
2. See how different flows connect to each other
3. Understand the relationships between nodes within flows
## Features
- **Interactive Graph**: Nodes can be dragged to reorganize the layout
- **Group Visualization**: Flows are displayed as groups with dashed borders
- **Inter-Group Links**: Connections between flows are shown as dashed lines connecting group boundaries
- **Action Labels**: Edge labels show the actions that trigger transitions between nodes
## Requirements
- Python 3.6 or higher
- Modern web browser (Chrome, Firefox, Edge) for viewing the visualizations
## Usage
### 1. Basic Visualization
To visualize a PocketFlow graph, you can use the `visualize_flow` function in `visualize.py`:
```python
from visualize import visualize_flow
from your_flow_module import your_flow
# Generate visualization
visualize_flow(your_flow, "Your Flow Name")
```
This will:
1. Print a Mermaid diagram to the console
2. Generate a D3.js visualization in the `./viz` directory
### 2. Running the Example
The included example shows an order processing pipeline with payment, inventory, and shipping flows:
```bash
# Navigate to the directory
cd cookbook/pocketflow-minimal-flow2flow
# Run the visualization script
python visualize.py
```
This will generate visualization files in the `./viz` directory.
### 3. Viewing the Visualization
After running the script:
1. Host with
```
cd ./viz/
```
2. Interact with the visualization:
- **Drag nodes** to reorganize
- **Hover over nodes** to see node names
- **Observe connections** between nodes and flows
## Customizing the Visualization
### Adjusting Layout Parameters
You can adjust the force simulation parameters in `visualize.py` to change how nodes and groups are positioned:
```javascript
// Create a force simulation
const simulation = d3.forceSimulation(data.nodes)
// Controls the distance between connected nodes
.force("link", d3.forceLink(data.links).id(d => d.id).distance(100))
// Controls how nodes repel each other - lower values bring nodes closer
.force("charge", d3.forceManyBody().strength(-30))
// Centers the entire graph in the SVG
.force("center", d3.forceCenter(width / 2, height / 2))
// Prevents nodes from overlapping - acts like a minimum distance
.force("collide", d3.forceCollide().radius(50));
```
### Styling
Adjust the CSS styles in the HTML template inside `create_d3_visualization` function to change colors, shapes, and other visual properties.
## How It Works
The visualization process consists of three main steps:
1. **Flow to JSON Conversion**: The `flow_to_json` function traverses the PocketFlow graph and converts it to a structure with nodes, links, and group information.
2. **D3.js Visualization**: The JSON data is used to create an interactive D3.js visualization with:
- Nodes represented as circles
- Flows represented as dashed rectangles containing nodes
- Links showing connections within and between flows
3. **Group Boundary Connections**: The visualization calculates intersection points with group boundaries to ensure inter-group links connect at the borders rather than centers.
## Extending the Visualization
You can extend the visualization tools by:
1. Adding new node shapes
2. Implementing additional layout algorithms
3. Adding tooltips with more detailed information
4. Creating animation for flow execution
## Troubleshooting
If you encounter any issues:
- Make sure your flow objects are properly constructed with nodes connected correctly
- Check the browser console for any JavaScript errors
- Verify that the generated JSON data structure matches what you expect
## Example Output
The visualization displays:
- Payment processing flow nodes
- Inventory management flow nodes
- Shipping flow nodes
- Group boundaries around each flow
- Connections between flows (Payment → Inventory → Shipping)
================================================
FILE: cookbook/pocketflow-visualization/async_flow.py
================================================
from pocketflow import AsyncNode, AsyncFlow
import asyncio
# Define Payment Nodes
class ValidatePayment(AsyncNode):
async def exec_async(self, prep_res):
print("1.1.Validating payment...")
return "Payment validated successfully"
async def post_async(self, shared, prep_res, exec_res):
shared["payment_status"] = exec_res
return "default"
class ProcessPayment(AsyncNode):
async def exec_async(self, prep_res):
print("1.2.Processing payment...")
return "Payment processed successfully"
async def post_async(self, shared, prep_res, exec_res):
shared["payment_result"] = exec_res
return "default"
class PaymentConfirmation(AsyncNode):
async def exec_async(self, prep_res):
print("1.3.Confirming payment...")
return "Payment confirmed"
async def post_async(self, shared, prep_res, exec_res):
shared["payment_confirmation"] = exec_res
return "default"
# Define Inventory Nodes
class CheckStock(AsyncNode):
async def exec_async(self, prep_res):
print("2.1.Checking inventory stock...")
return "Stock available"
async def post_async(self, shared, prep_res, exec_res):
shared["stock_status"] = exec_res
return "default"
class ReserveItems(AsyncNode):
async def exec_async(self, prep_res):
print("2.2.Reserving items...")
return "Items reserved"
async def post_async(self, shared, prep_res, exec_res):
shared["reservation_status"] = exec_res
return "default"
class UpdateInventory(AsyncNode):
async def exec_async(self, prep_res):
print("2.3. Updating inventory...")
return "Inventory updated"
async def post_async(self, shared, prep_res, exec_res):
shared["inventory_update"] = exec_res
return "default"
# Define Shipping Nodes
class CreateLabel(AsyncNode):
async def exec_async(self, prep_res):
print("3.1 Creating shipping label...")
return "Shipping label created"
async def post_async(self, shared, prep_res, exec_res):
shared["shipping_label"] = exec_res
return "default"
class AssignCarrier(AsyncNode):
async def exec_async(self, prep_res):
print("3.2 Assigning carrier...")
return "Carrier assigned"
async def post_async(self, shared, prep_res, exec_res):
shared["carrier"] = exec_res
return "default"
class SchedulePickup(AsyncNode):
async def exec_async(self, prep_res):
print("3.3 Scheduling pickup...")
return "Pickup scheduled"
async def post_async(self, shared, prep_res, exec_res):
shared["pickup_status"] = exec_res
return "default"
# Create node instances
validate_payment = ValidatePayment()
process_payment = ProcessPayment()
payment_confirmation = PaymentConfirmation()
check_stock = CheckStock()
reserve_items = ReserveItems()
update_inventory = UpdateInventory()
create_label = CreateLabel()
assign_carrier = AssignCarrier()
schedule_pickup = SchedulePickup()
# Payment processing sub-flow
validate_payment >> process_payment >> payment_confirmation
payment_flow = AsyncFlow(start=validate_payment)
# Inventory sub-flow
check_stock >> reserve_items >> update_inventory
inventory_flow = AsyncFlow(start=check_stock)
# Shipping sub-flow
create_label >> assign_carrier >> schedule_pickup
shipping_flow = AsyncFlow(start=create_label)
# Connect the flows into a main order pipeline
payment_flow >> inventory_flow >> shipping_flow
# payment_flow >> inventory_flow >> create_label
# payment_flow >> inventory_flow >> assign_carrier
# Create the master flow
class OrderFlow(AsyncFlow):
pass
order_pipeline = OrderFlow(start=payment_flow)
# Create shared data structure
shared_data = {
"order_id": "ORD-12345",
"customer": "John Doe",
"items": [
{"id": "ITEM-001", "name": "Smartphone", "price": 999.99, "quantity": 1},
{"id": "ITEM-002", "name": "Phone case", "price": 29.99, "quantity": 1},
],
"shipping_address": {
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345",
},
}
# Run the entire pipeline asynchronously
async def main():
await order_pipeline.run_async(shared_data)
# Print final status
print("\nOrder processing completed!")
print(f"Payment: {shared_data.get('payment_confirmation')}")
print(f"Inventory: {shared_data.get('inventory_update')}")
print(f"Shipping: {shared_data.get('pickup_status')}")
if __name__ == "__main__":
asyncio.run(main())
================================================
FILE: cookbook/pocketflow-visualization/async_loop_flow.py
================================================
from async_flow import *
from pocketflow import Flow, AsyncParallelBatchNode, Node
# Create node instances
validate_payment = ValidatePayment()
process_payment = ProcessPayment()
payment_confirmation = PaymentConfirmation()
check_stock = CheckStock()
reserve_items = ReserveItems()
update_inventory = UpdateInventory()
create_label = CreateLabel()
assign_carrier = AssignCarrier()
schedule_pickup = SchedulePickup()
# Payment processing sub-flow
validate_payment >> process_payment
validate_payment - "out_of_stock" >> validate_payment # 循环重试
process_payment - 'something fail' >> validate_payment
process_payment - 'pass' >> payment_confirmation
payment_flow = AsyncFlow(start=validate_payment)
# Inventory sub-flow
check_stock >> reserve_items >> update_inventory
inventory_flow = AsyncFlow(start=check_stock)
# Shipping sub-flow
create_label >> assign_carrier >> schedule_pickup
shipping_flow = AsyncFlow(start=create_label)
# Connect the flows into a main order pipeline
payment_flow >> inventory_flow >> shipping_flow
# payment_flow >> inventory_flow >> create_label
# payment_flow >> inventory_flow >> assign_carrier
# Create the master flow
class OrderFlow(AsyncFlow):
pass
order_pipeline = OrderFlow(start=payment_flow)
# Create shared data structure
shared_data = {
"order_id": "ORD-12345",
"customer": "John Doe",
"items": [
{"id": "ITEM-001", "name": "Smartphone", "price": 999.99, "quantity": 1},
{"id": "ITEM-002", "name": "Phone case", "price": 29.99, "quantity": 1},
],
"shipping_address": {
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345",
},
}
# Run the entire pipeline asynchronously
async def main():
await order_pipeline.run_async(shared_data)
# Print final status
print("\nOrder processing completed!")
print(f"Payment: {shared_data.get('payment_confirmation')}")
print(f"Inventory: {shared_data.get('inventory_update')}")
print(f"Shipping: {shared_data.get('pickup_status')}")
if __name__ == "__main__":
asyncio.run(main())
================================================
FILE: cookbook/pocketflow-visualization/visualize.py
================================================
# %%
import json
import os
import http.server
import socketserver
import threading
import webbrowser
import time
import socket
import importlib
import sys
from pathlib import Path
from typing import Any, Optional, Tuple, Union
from pocketflow import Flow
from async_flow import order_pipeline
def build_mermaid(start):
ids, visited, lines = {}, set(), ["graph LR"]
ctr = 1
def get_id(n):
nonlocal ctr
return (
ids[n] if n in ids else (ids.setdefault(n, f"N{ctr}"), (ctr := ctr + 1))[0]
)
def link(a, b, action=None):
if action:
lines.append(f" {a} -->|{action}| {b}")
else:
lines.append(f" {a} --> {b}")
def walk(node, parent=None, action=None):
if node in visited:
return parent and link(parent, get_id(node), action)
visited.add(node)
if isinstance(node, Flow):
node.start_node and parent and link(parent, get_id(node.start_node), action)
lines.append(
f"\n subgraph sub_flow_{get_id(node)}[{type(node).__name__}]"
)
node.start_node and walk(node.start_node)
for act, nxt in node.successors.items():
node.start_node and walk(nxt, get_id(node.start_node), act) or (
parent and link(parent, get_id(nxt), action)
) or walk(nxt, None, act)
lines.append(" end\n")
else:
lines.append(f" {(nid := get_id(node))}['{type(node).__name__}']")
parent and link(parent, nid, action)
[walk(nxt, nid, act) for act, nxt in node.successors.items()]
walk(start)
return "\n".join(lines)
def flow_to_json(start):
"""Convert a flow to JSON format suitable for D3.js visualization.
This function walks through the flow graph and builds a structure with:
- nodes: All non-Flow nodes with their group memberships
- links: Connections between nodes within the same group
- group_links: Connections between different groups (for inter-flow connections)
- flows: Flow information for group labeling
Returns:
dict: A JSON-serializable dictionary with 'nodes' and 'links' arrays.
"""
nodes = []
links = []
group_links = [] # For connections between groups (Flow to Flow)
ids = {}
node_types = {}
flow_nodes = {} # Keep track of flow nodes
ctr = 1
visited = set()
def get_id(n):
nonlocal ctr
if n not in ids:
ids[n] = ctr
node_types[ctr] = type(n).__name__
if isinstance(n, Flow):
flow_nodes[ctr] = n # Store flow reference
ctr += 1
return ids[n]
def walk(node, parent=None, group=None, parent_group=None, action=None):
"""Recursively walk the flow graph to build the visualization data.
Args:
node: Current node being processed
parent: ID of the parent node that connects to this node
group: Group (Flow) ID this node belongs to
parent_group: Group ID of the parent node
action: Action label on the edge from parent to this node
"""
node_id = get_id(node)
if (node_id, action) in visited:
return
visited.add((node_id, action))
# Add node if not already in nodes list and not a Flow
if not any(n["id"] == node_id for n in nodes) and not isinstance(node, Flow):
node_data = {
"id": node_id,
"name": node_types[node_id],
"group": group or 0, # Default group
}
nodes.append(node_data)
# Add link from parent if exists
if parent and not isinstance(node, Flow):
links.append(
{"source": parent, "target": node_id, "action": action or "default"}
)
# Process different types of nodes
if isinstance(node, Flow):
# This is a Flow node - it becomes a group container
flow_group = node_id # Use flow's ID as group for contained nodes
# Add a group-to-group link if this flow has a parent group
# This creates connections between nested flows
if parent_group is not None and parent_group != flow_group:
# Check if this link already exists
if not any(
l["source"] == parent_group and l["target"] == flow_group
for l in group_links
):
group_links.append(
{
"source": parent_group,
"target": flow_group,
"action": action or "default",
}
)
if node.start_node:
# Process the start node of this flow
walk(node.start_node, parent, flow_group, parent_group, action)
# Process successors of the flow's start node
for next_action, nxt in node.successors.items():
walk(
nxt,
get_id(node.start_node),
flow_group,
parent_group,
next_action,
)
else:
# Process successors for regular nodes
for next_action, nxt in node.successors.items():
if isinstance(nxt, Flow):
# This node connects to a flow - track the group relationship
flow_group_id = get_id(nxt)
walk(nxt, node_id, None, group, next_action)
else:
# Regular node-to-node connection
walk(nxt, node_id, group, parent_group, next_action)
# Start the traversal
walk(start)
# Post-processing: Generate group links based on node connections between different groups
# This ensures that when nodes in different groups are connected, we show a group-to-group
# link rather than a direct node-to-node link
node_groups = {n["id"]: n["group"] for n in nodes}
filtered_links = []
for link in links:
source_id = link["source"]
target_id = link["target"]
source_group = node_groups.get(source_id, 0)
target_group = node_groups.get(target_id, 0)
# If source and target are in different groups and both groups are valid
if source_group != target_group and source_group > 0 and target_group > 0:
# Add to group links if not already there
# This creates the dashed lines connecting group boxes
if not any(
gl["source"] == source_group and gl["target"] == target_group
for gl in group_links
):
group_links.append(
{
"source": source_group,
"target": target_group,
"action": link["action"],
}
)
# Skip adding this link to filtered_links - we don't want direct node connections across groups
else:
# Keep links within the same group
filtered_links.append(link)
return {
"nodes": nodes,
"links": filtered_links, # Use filtered links instead of all links
"group_links": group_links,
"flows": {str(k): v.__class__.__name__ for k, v in flow_nodes.items()},
}
def create_d3_visualization(
json_data,
output_dir="./viz",
filename="flow_viz",
html_title="PocketFlow Visualization",
):
"""Create a D3.js visualization from JSON data.
Args:
json_data: The JSON data for the visualization
output_dir: Directory to save the files
filename: Base filename (without extension)
html_title: Title for the HTML page
Returns:
str: Path to the HTML file
"""
# Create output directory if it doesn't exist
os.makedirs(output_dir, exist_ok=True)
# Save JSON data to file
json_path = os.path.join(output_dir, f"{filename}.json")
with open(json_path, "w") as f:
json.dump(json_data, f, indent=2)
# Create HTML file with D3.js visualization
html_content = r"""
TITLE_PLACEHOLDER
"""
# Replace the placeholders with the actual values
html_content = html_content.replace("FILENAME_PLACEHOLDER", filename)
html_content = html_content.replace("TITLE_PLACEHOLDER", html_title)
# Write HTML to file
html_path = os.path.join(output_dir, f"{filename}.html")
with open(html_path, "w") as f:
f.write(html_content)
print(f"Visualization created at {html_path}")
return html_path
def find_free_port():
"""Find a free port on localhost."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return s.getsockname()[1]
def start_http_server(directory, port=None):
"""Start an HTTP server in the given directory.
Args:
directory: Directory to serve files from
port: Port to use (finds a free port if None)
Returns:
tuple: (server_thread, port)
"""
if port is None:
port = find_free_port()
# Get the absolute path of the directory
directory = str(Path(directory).absolute())
# Change to the directory to serve files
os.chdir(directory)
# Create HTTP server
handler = http.server.SimpleHTTPRequestHandler
httpd = socketserver.TCPServer(("", port), handler)
# Start server in a separate thread
server_thread = threading.Thread(target=httpd.serve_forever)
server_thread.daemon = (
True # This makes the thread exit when the main program exits
)
server_thread.start()
print(f"Server started at http://localhost:{port}")
return server_thread, port
def serve_and_open_visualization(html_path, auto_open=True):
"""Serve the HTML file and open it in a browser.
Args:
html_path: Path to the HTML file
auto_open: Whether to automatically open the browser
Returns:
tuple: (server_thread, url)
"""
# Get the directory and filename
directory = os.path.dirname(os.path.abspath(html_path))
filename = os.path.basename(html_path)
# Start the server
server_thread, port = start_http_server(directory)
# Build the URL
url = f"http://localhost:{port}/{filename}"
# Open the URL in a browser
if auto_open:
print(f"Opening {url} in your browser...")
webbrowser.open(url)
else:
print(f"Visualization available at {url}")
return server_thread, url
def visualize_flow(
flow: Flow,
flow_name: str,
serve: bool = True,
auto_open: bool = True,
output_dir: str = "./viz",
html_title: Optional[str] = None,
) -> Union[str, Tuple[str, Any, str]]:
"""Helper function to visualize a flow with both mermaid and D3.js
Args:
flow: Flow object to visualize
flow_name: Name of the flow (used for filename and display)
serve: Whether to start a server for the visualization
auto_open: Whether to automatically open in browser
output_dir: Directory to save visualization files
html_title: Custom title for the HTML page (defaults to flow_name if None)
Returns:
str or tuple: Path to HTML file, or (path, server_thread, url) if serve=True
"""
print(f"\n--- {flow_name} Mermaid Diagram ---")
print(build_mermaid(start=flow))
print(f"\n--- {flow_name} D3.js Visualization ---")
json_data = flow_to_json(flow)
# Create the visualization
output_filename = f"{flow_name.lower().replace(' ', '_')}"
# Use flow_name as the HTML title if not specified
if html_title is None:
html_title = f"PocketFlow: {flow_name}"
html_path = create_d3_visualization(
json_data,
output_dir=output_dir,
filename=output_filename,
html_title=html_title,
)
# Serve and open if requested
if serve:
server_thread, url = serve_and_open_visualization(html_path, auto_open)
return html_path, server_thread, url
return html_path
def load_flow_from_module(module_path: str, flow_variable: str) -> Flow:
"""Dynamically load a flow from a module.
Args:
module_path: Path to the module (e.g., 'my_package.my_module')
flow_variable: Name of the flow variable in the module
Returns:
Flow: The loaded flow object
"""
try:
module = importlib.import_module(module_path)
return getattr(module, flow_variable)
except (ImportError, AttributeError) as e:
print(f"Error loading flow: {e}")
sys.exit(1)
# Example usage
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Visualize a PocketFlow flow")
parser.add_argument(
"--module", default="async_loop_flow", help="Module containing the flow"
)
parser.add_argument(
"--flow", default="order_pipeline", help="Flow variable name in the module"
)
parser.add_argument(
"--name", default="Flow Visualization", help="Name for the visualization"
)
parser.add_argument(
"--output-dir", default="./viz", help="Directory to save visualization files"
)
parser.add_argument("--no-serve", action="store_true", help="Don't start a server")
parser.add_argument(
"--no-open", action="store_true", help="Don't open browser automatically"
)
parser.add_argument("--title", help="Custom HTML title")
args = parser.parse_args()
# Load flow from the specified module
flow_obj = load_flow_from_module(args.module, args.flow)
# Visualize the flow
visualize_flow(
flow=flow_obj,
flow_name=args.name,
serve=not args.no_serve,
auto_open=not args.no_open,
output_dir=args.output_dir,
html_title=args.title,
)
# Keep server running if serving
if not args.no_serve:
try:
print("\nServer is running. Press Ctrl+C to stop...")
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\nShutting down...")
================================================
FILE: cookbook/pocketflow-visualization/viz/flow_visualization.html
================================================
PocketFlow: Flow Visualization
================================================
FILE: cookbook/pocketflow-visualization/viz/flow_visualization.json
================================================
{
"nodes": [
{
"id": 3,
"name": "ValidatePayment",
"group": 2
},
{
"id": 4,
"name": "ProcessPayment",
"group": 2
},
{
"id": 5,
"name": "PaymentConfirmation",
"group": 2
},
{
"id": 7,
"name": "CheckStock",
"group": 6
},
{
"id": 8,
"name": "ReserveItems",
"group": 6
},
{
"id": 9,
"name": "UpdateInventory",
"group": 6
},
{
"id": 11,
"name": "CreateLabel",
"group": 10
},
{
"id": 12,
"name": "AssignCarrier",
"group": 10
},
{
"id": 13,
"name": "SchedulePickup",
"group": 10
}
],
"links": [
{
"source": 3,
"target": 4,
"action": "default"
},
{
"source": 4,
"target": 5,
"action": "default"
},
{
"source": 7,
"target": 8,
"action": "default"
},
{
"source": 8,
"target": 9,
"action": "default"
},
{
"source": 11,
"target": 12,
"action": "default"
},
{
"source": 12,
"target": 13,
"action": "default"
}
],
"group_links": [
{
"source": 2,
"target": 6,
"action": "default"
},
{
"source": 6,
"target": 10,
"action": "default"
}
],
"flows": {
"1": "OrderFlow",
"2": "AsyncFlow",
"6": "AsyncFlow",
"10": "AsyncFlow"
}
}
================================================
FILE: cookbook/pocketflow-voice-chat/README.md
================================================
# PocketFlow Voice Chat
This project demonstrates a voice-based interactive chat application built with PocketFlow. Users can speak their queries, and the system will respond with spoken answers from an LLM, maintaining conversation history.
- Check out the [Substack Post Tutorial](https://pocketflow.substack.com/p/build-your-own-voice-chatbot-from) for more!
## Features
- **Voice Activity Detection (VAD)**: Automatically detects when the user starts and stops speaking.
- **Speech-to-Text (STT)**: Converts spoken audio into text using OpenAI.
- **LLM Interaction**: Processes the transcribed text with an LLM (e.g., GPT-4o), maintaining conversation history.
- **Text-to-Speech (TTS)**: Converts the LLM's text response back into audible speech using OpenAI.
- **Continuous Conversation**: Loops back to listen for the next user query after responding, allowing for an ongoing dialogue.
## How to Run
1. **Set your OpenAI API key**:
```bash
export OPENAI_API_KEY="your-api-key-here"
```
Ensure this environment variable is set, as the utility scripts for STT, LLM, and TTS rely on it.
You can test individual utility functions (e.g., `python utils/call_llm.py`, `python utils/text_to_speech.py`) to help verify your API key and setup.
2. **Install dependencies**:
Make sure you have Python installed. Then, install the required libraries using pip:
```bash
pip install -r requirements.txt
```
This will install libraries such as `openai`, `pocketflow`, `sounddevice`, `numpy`, `scipy`, and `soundfile`.
**Note for Linux users**: `sounddevice` may require PortAudio. If you encounter issues, you might need to install it first:
```bash
sudo apt-get update && sudo apt-get install -y portaudio19-dev
```
3. **Run the application**:
```bash
python main.py
```
Follow the console prompts. The application will start listening when you see "Listening for your query...".
## How It Works
The application uses a PocketFlow workflow to manage the conversation steps:
```mermaid
flowchart TD
CaptureAudio[Capture Audio] --> SpeechToText[Speech to Text]
SpeechToText --> QueryLLM[Query LLM]
QueryLLM --> TextToSpeech[Text to Speech & Play]
TextToSpeech -- "Next Turn" --> CaptureAudio
```
Here's what each node in the flow does:
1. **`CaptureAudioNode`**: Records audio from the user's microphone. It uses Voice Activity Detection (VAD) to start recording when speech is detected and stop when silence is detected.
2. **`SpeechToTextNode`**: Takes the recorded audio data, converts it to a suitable format, and sends it to OpenAI's STT API (gpt-4o-transcribe) to get the transcribed text.
3. **`QueryLLMNode`**: Takes the transcribed text from the user, along with the existing conversation history, and sends it to an LLM (OpenAI's GPT-4o model) to generate an intelligent response.
4. **`TextToSpeechNode`**: Receives the text response from the LLM, converts it into audio using OpenAI's TTS API (gpt-4o-mini-tts), and plays the audio back to the user. If the conversation is set to continue, it transitions back to the `CaptureAudioNode`.
## Example Interaction
When you run `main.py`:
1. The console will display:
```
Starting PocketFlow Voice Chat...
Speak your query after 'Listening for your query...' appears.
...
```
2. When you see `Listening for your query...`, speak clearly into your microphone.
3. After you stop speaking, the console will show updates:
```
Audio captured (X.XXs), proceeding to STT.
Converting speech to text...
User: [Your transcribed query will appear here]
Sending query to LLM...
LLM: [The LLM's response text will appear here]
Converting LLM response to speech...
Playing LLM response...
```
4. You will hear the LLM's response spoken aloud.
5. The application will then loop back, and you'll see `Listening for your query...` again, ready for your next input.
The conversation continues in this manner. To stop the application, you typically need to interrupt it (e.g., Ctrl+C in the terminal), as it's designed to loop continuously.
================================================
FILE: cookbook/pocketflow-voice-chat/docs/design.md
================================================
# Design Doc: PocketFlow Voice Chat
> Please DON'T remove notes for AI
## Requirements
> Notes for AI: Keep it simple and clear.
> If the requirements are abstract, write concrete user stories
- **Goal**: Enable users to interact with an LLM via voice in a continuous conversation, receiving spoken responses.
- **User Story 1**: As a user, I want to speak my query into a microphone so that the application can understand what I'm asking.
- **User Story 2**: As a user, I want the application to send my spoken query to an LLM for processing.
- **User Story 3**: As a user, I want to hear the LLM's response spoken back to me.
- **User Story 4**: As a user, after hearing the response, I want the application to be ready for my next spoken query without restarting.
- **Core Functionalities**:
1. Capture audio input.
2. Convert speech to text (STT).
3. Process text with an LLM (maintaining conversation history).
4. Convert LLM text response to speech (TTS).
5. Play back synthesized audio.
6. Loop back to capture new audio input for a continuous conversation.
## Flow Design
> Notes for AI:
> 1. Consider the design patterns of agent, map-reduce, rag, and workflow. Apply them if they fit.
> 2. Present a concise, high-level description of the workflow.
### Applicable Design Pattern:
- **Workflow**: A sequential workflow with a loop is most appropriate. Each step (audio capture, STT, LLM query, TTS, audio playback) directly follows the previous, and after playback, the flow returns to the audio capture stage.
### Flow high-level Design:
The application will operate in a loop to allow for continuous conversation:
1. **`CaptureAudioNode`**: Records audio from the user\'s microphone when triggered.
2. **`SpeechToTextNode`**: Converts the recorded audio into text.
3. **`QueryLLMNode`**: Sends the transcribed text (with history) to an LLM and gets a text response.
4. **`TextToSpeechNode`**: Converts the LLM\'s text response into in-memory audio data and then plays it. After completion, the flow transitions back to the `CaptureAudioNode`.
```mermaid
flowchart TD
CaptureAudio[Capture Audio] --> SpeechToText[Speech to Text]
SpeechToText --> QueryLLM[Query LLM]
QueryLLM --> TextToSpeech[Text to Speech & Play]
TextToSpeech -- "Next Turn" --> CaptureAudio
```
## Utility Functions
> Notes for AI:
> 1. Understand the utility function definition thoroughly by reviewing the doc.
> 2. Include only the necessary utility functions, based on nodes in the flow.
1. **`record_audio()`** (`utils/audio_utils.py`)
- *Input*: (Optional) `sample_rate` (int, Hz, e.g., `DEFAULT_SAMPLE_RATE`), `channels` (int, e.g., `DEFAULT_CHANNELS`), `chunk_size_ms` (int, e.g., `DEFAULT_CHUNK_SIZE_MS`), `silence_threshold_rms` (float, e.g., `DEFAULT_SILENCE_THRESHOLD_RMS`), `min_silence_duration_ms` (int, e.g., `DEFAULT_MIN_SILENCE_DURATION_MS`), `max_recording_duration_s` (int, e.g., `DEFAULT_MAX_RECORDING_DURATION_S`), `pre_roll_chunks_count` (int, e.g., `DEFAULT_PRE_ROLL_CHUNKS`).
- *Output*: A tuple `(audio_data, sample_rate)` where `audio_data` is a NumPy array of float32 audio samples, and `sample_rate` is the recording sample rate (int). Returns `(None, sample_rate)` if no speech is detected or recording fails.
- *Description*: Records audio from the microphone using silence-based Voice Activity Detection (VAD). Buffers `pre_roll_chunks_count` of audio and starts full recording when sound is detected above `silence_threshold_rms`. Stops after `min_silence_duration_ms` of sound below the threshold or if `max_recording_duration_s` is reached.
- *Necessity*: Used by `CaptureAudioNode` to get user\'s voice input.
2. **`speech_to_text_api(audio_data, sample_rate)`** (`utils/speech_to_text.py`)
- *Input*: `audio_data` (bytes), `sample_rate` (int, though the API might infer this from the audio format).
- *Output*: `transcribed_text` (str).
- *Necessity*: Used by `SpeechToTextNode` to convert in-memory audio data to text.
- *Example Model*: OpenAI `gpt-4o-transcribe`.
3. **`call_llm(messages)`** (`utils/call_llm.py`)
- *Input*: `messages` (list of dicts, e.g., `[{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]`). This should be the complete conversation history including the latest user query.
- *Output*: `llm_response_text` (str)
- *Necessity*: Used by `QueryLLMNode` to get an intelligent response.
- *Example Model*: OpenAI `gpt-4o`.
4. **`text_to_speech_api(text_to_synthesize)`** (`utils/text_to_speech.py`)
- *Input*: `text_to_synthesize` (str).
- *Output*: A tuple `(audio_data, sample_rate)` where `audio_data` is in-memory audio as bytes (e.g., MP3 format from OpenAI) and `sample_rate` is the audio sample rate (int, e.g., 24000 Hz for OpenAI `gpt-4o-mini-tts`).
- *Necessity*: Used by `TextToSpeechNode` to convert LLM text to speakable in-memory audio data.
- *Example Model*: OpenAI `gpt-4o-mini-tts`.
5. **`play_audio_data(audio_data, sample_rate)`** (`utils/audio_utils.py`)
- *Input*: `audio_data` (NumPy array of float32 audio samples), `sample_rate` (int).
- *Output*: None
- *Necessity*: Used by `TextToSpeechNode` (in its `post` method) to play the in-memory synthesized speech.
## Node Design
### Shared Memory
> Notes for AI: Try to minimize data redundancy
The shared memory structure is organized as follows:
```python
shared = {
"user_audio_data": None, # In-memory audio data (NumPy array) from user
"user_audio_sample_rate": None, # int: Sample rate of the user audio
"chat_history": [], # list: Conversation history [{"role": "user/assistant", "content": "..."}]
"continue_conversation": True # boolean: Flag to control the main conversation loop
}
```
### Node Steps
> Notes for AI: Carefully decide whether to use Batch/Async Node/Flow.
1. **`CaptureAudioNode`**
- *Purpose*: Record audio input from the user using VAD.
- *Type*: Regular
- *Steps*:
- *prep*: Check `shared["continue_conversation"]`. (Potentially load VAD parameters from `shared["config"]` if dynamic).
- *exec*: Call `utils.audio_utils.record_audio()` (passing VAD parameters if configured). This returns a NumPy array and sample rate.
- *post*: `audio_numpy_array, sample_rate = exec_res`. Write `audio_numpy_array` to `shared["user_audio_data"]` and `sample_rate` to `shared["user_audio_sample_rate"]`. Returns `"default"`.
2. **`SpeechToTextNode`**
- *Purpose*: Convert the recorded in-memory audio to text.
- *Type*: Regular
- *Steps*:
- *prep*: Read `shared["user_audio_data"]` (NumPy array) and `shared["user_audio_sample_rate"]`. Return `(user_audio_data_numpy, user_audio_sample_rate)`.
- *exec*: `audio_numpy_array, sample_rate = prep_res`. **Convert `audio_numpy_array` to audio `bytes` (e.g., in WAV format using `scipy.io.wavfile.write` to an `io.BytesIO` object).** Call `utils.speech_to_text.speech_to_text_api(audio_bytes, sample_rate)`.
- *post*:
- Let `transcribed_text = exec_res`.
- Append `{"role": "user", "content": transcribed_text}` to `shared["chat_history"]`.
- Clear `shared["user_audio_data"]` and `shared["user_audio_sample_rate"]` as they are no longer needed.
- Returns `"default"` (assuming STT is successful as per simplification).
3. **`QueryLLMNode`**
- *Purpose*: Get a response from the LLM based on the user's query and conversation history.
- *Type*: Regular
- *Steps*:
- *prep*: Read `shared["chat_history"]`. Return `chat_history`.
- *exec*: `history = prep_res`. Call `utils.call_llm.call_llm(messages=history)`.
- *post*:
- Let `llm_response = exec_res`.
- Append `{"role": "assistant", "content": llm_response}` to `shared["chat_history"]`.
- Returns `"default"` (assuming LLM call is successful).
4. **`TextToSpeechNode`**
- *Purpose*: Convert the LLM's text response into speech and play it.
- *Type*: Regular
- *Steps*:
- *prep*: Read `shared["chat_history"]`. Identify the last message, which should be the LLM's response. Return its content.
- *exec*: `text_to_synthesize = prep_res`. Call `utils.text_to_speech.text_to_speech_api(text_to_synthesize)`. This returns `(llm_audio_bytes, llm_sample_rate)`.
- *post*: `llm_audio_bytes, llm_sample_rate = exec_res`.
- **Convert `llm_audio_bytes` (e.g., MP3 bytes from TTS API) to a NumPy array of audio samples (e.g., using a library like `pydub` or `soundfile` to decode).**
- Call `utils.audio_utils.play_audio_data(llm_audio_numpy_array, llm_sample_rate)`.
- (Optional) Log completion.
- If `shared["continue_conversation"]` is `True`, return `"next_turn"` to loop back.
- Otherwise, return `"end_conversation"`.
================================================
FILE: cookbook/pocketflow-voice-chat/flow.py
================================================
from pocketflow import Flow
from nodes import CaptureAudioNode, SpeechToTextNode, QueryLLMNode, TextToSpeechNode
def create_voice_chat_flow() -> Flow:
"""Creates and returns the voice chat flow."""
# Create nodes
capture_audio = CaptureAudioNode()
speech_to_text = SpeechToTextNode()
query_llm = QueryLLMNode()
text_to_speech = TextToSpeechNode()
# Define transitions
capture_audio >> speech_to_text
speech_to_text >> query_llm
query_llm >> text_to_speech
# Loop back for next turn or end
text_to_speech - "next_turn" >> capture_audio
# "end_conversation" action from any node will terminate the flow naturally
# if no transition is defined for it from the current node.
# Alternatively, one could explicitly transition to an EndNode if desired.
# Create flow starting with the capture audio node
voice_chat_flow = Flow(start=capture_audio)
return voice_chat_flow
================================================
FILE: cookbook/pocketflow-voice-chat/main.py
================================================
from flow import create_voice_chat_flow
def main():
"""Runs the PocketFlow Voice Chat application."""
print("Starting PocketFlow Voice Chat...")
print("Speak your query after 'Listening for your query...' appears.")
print("The conversation will continue until an error occurs or the loop is intentionally stopped.")
print("To attempt to stop, you might need to cause an error (e.g., silence during capture if not handled by VAD to end gracefully) or modify shared[\"continue_conversation\"] if a mechanism is added.")
shared = {
"user_audio_data": None,
"user_audio_sample_rate": None,
"chat_history": [],
"continue_conversation": True # Flag to control the main conversation loop
}
# Create the flow
voice_chat_flow = create_voice_chat_flow()
# Run the flow
# The flow will loop based on the "next_turn" action from TextToSpeechNode
# and the continue_conversation flag checked within nodes or if an error action is returned.
voice_chat_flow.run(shared)
if __name__ == "__main__":
main()
================================================
FILE: cookbook/pocketflow-voice-chat/nodes.py
================================================
import numpy as np
import scipy.io.wavfile
import io
import soundfile # For converting MP3 bytes to NumPy array
from pocketflow import Node
from utils.audio_utils import record_audio, play_audio_data
from utils.speech_to_text import speech_to_text_api
from utils.call_llm import call_llm
from utils.text_to_speech import text_to_speech_api
class CaptureAudioNode(Node):
"""Records audio input from the user using VAD."""
def exec(self, _): # prep_res is not used as per design
print("\nListening for your query...")
audio_data, sample_rate = record_audio()
if audio_data is None:
return None, None
return audio_data, sample_rate
def post(self, shared, prep_res, exec_res):
audio_numpy_array, sample_rate = exec_res
if audio_numpy_array is None:
shared["user_audio_data"] = None
shared["user_audio_sample_rate"] = None
print("CaptureAudioNode: Failed to capture audio.")
return "end_conversation"
shared["user_audio_data"] = audio_numpy_array
shared["user_audio_sample_rate"] = sample_rate
print(f"Audio captured ({len(audio_numpy_array)/sample_rate:.2f}s), proceeding to STT.")
class SpeechToTextNode(Node):
"""Converts the recorded in-memory audio to text."""
def prep(self, shared):
user_audio_data = shared.get("user_audio_data")
user_audio_sample_rate = shared.get("user_audio_sample_rate")
if user_audio_data is None or user_audio_sample_rate is None:
print("SpeechToTextNode: No audio data to process.")
return None # Signal to skip exec
return user_audio_data, user_audio_sample_rate
def exec(self, prep_res):
if prep_res is None:
return None # Skip if no audio data
audio_numpy_array, sample_rate = prep_res
# Convert NumPy array to WAV bytes for the API
byte_io = io.BytesIO()
scipy.io.wavfile.write(byte_io, sample_rate, audio_numpy_array)
wav_bytes = byte_io.getvalue()
print("Converting speech to text...")
transcribed_text = speech_to_text_api(audio_data=wav_bytes, sample_rate=sample_rate)
return transcribed_text
def post(self, shared, prep_res, exec_res):
if exec_res is None:
print("SpeechToTextNode: STT API returned no text.")
return "end_conversation"
transcribed_text = exec_res
print(f"User: {transcribed_text}")
if "chat_history" not in shared:
shared["chat_history"] = []
shared["chat_history"].append({"role": "user", "content": transcribed_text})
shared["user_audio_data"] = None
shared["user_audio_sample_rate"] = None
return "default"
class QueryLLMNode(Node):
"""Gets a response from the LLM."""
def prep(self, shared):
chat_history = shared.get("chat_history", [])
if not chat_history:
print("QueryLLMNode: Chat history is empty. Skipping LLM call.")
return None
return chat_history
def exec(self, prep_res):
if prep_res is None:
return None
chat_history = prep_res
print("Sending query to LLM...")
llm_response_text = call_llm(messages=chat_history)
return llm_response_text
def post(self, shared, prep_res, exec_res):
if exec_res is None:
print("QueryLLMNode: LLM API returned no response.")
return "end_conversation"
llm_response_text = exec_res
print(f"LLM: {llm_response_text}")
shared["chat_history"].append({"role": "assistant", "content": llm_response_text})
return "default"
class TextToSpeechNode(Node):
"""Converts the LLM's text response into speech and plays it."""
def prep(self, shared):
chat_history = shared.get("chat_history", [])
if not chat_history:
print("TextToSpeechNode: Chat history is empty. No LLM response to synthesize.")
return None
last_message = chat_history[-1]
if last_message.get("role") == "assistant" and last_message.get("content"):
return last_message.get("content")
else:
print("TextToSpeechNode: Last message not from assistant or no content. Skipping TTS.")
return None
def exec(self, prep_res):
if prep_res is None:
return None, None
llm_text_response = prep_res
print("Converting LLM response to speech...")
llm_audio_bytes, llm_sample_rate = text_to_speech_api(llm_text_response)
return llm_audio_bytes, llm_sample_rate
def post(self, shared, prep_res, exec_res):
if exec_res is None or exec_res[0] is None:
print("TextToSpeechNode: TTS failed or was skipped.")
return "next_turn"
llm_audio_bytes, llm_sample_rate = exec_res
print("Playing LLM response...")
try:
audio_segment, sr_from_file = soundfile.read(io.BytesIO(llm_audio_bytes))
play_audio_data(audio_segment, sr_from_file)
except Exception as e:
print(f"Error playing TTS audio: {e}")
return "next_turn"
if shared.get("continue_conversation", True):
return "next_turn"
else:
print("Conversation ended by user flag.")
return "end_conversation"
================================================
FILE: cookbook/pocketflow-voice-chat/requirements.txt
================================================
openai
pocketflow
numpy
sounddevice
scipy
soundfile
================================================
FILE: cookbook/pocketflow-voice-chat/utils/__init__.py
================================================
================================================
FILE: cookbook/pocketflow-voice-chat/utils/audio_utils.py
================================================
import sounddevice as sd
import numpy as np
DEFAULT_SAMPLE_RATE = 44100
DEFAULT_CHANNELS = 1
DEFAULT_CHUNK_SIZE_MS = 50 # Process audio in 50ms chunks for VAD
DEFAULT_SILENCE_THRESHOLD_RMS = 0.01 # RMS value, needs tuning
DEFAULT_MIN_SILENCE_DURATION_MS = 1000 # 1 second of silence to stop
DEFAULT_MAX_RECORDING_DURATION_S = 15 # Safety cap for recording
DEFAULT_PRE_ROLL_CHUNKS = 3 # Number of chunks to keep before speech starts
def record_audio(sample_rate = DEFAULT_SAMPLE_RATE,
channels = DEFAULT_CHANNELS,
chunk_size_ms = DEFAULT_CHUNK_SIZE_MS,
silence_threshold_rms = DEFAULT_SILENCE_THRESHOLD_RMS,
min_silence_duration_ms = DEFAULT_MIN_SILENCE_DURATION_MS,
max_recording_duration_s = DEFAULT_MAX_RECORDING_DURATION_S,
pre_roll_chunks_count = DEFAULT_PRE_ROLL_CHUNKS):
"""
Records audio from the microphone with silence-based VAD.
Returns in-memory audio data (NumPy array of float32) and sample rate.
Returns (None, sample_rate) if recording fails or max duration is met without speech.
"""
chunk_size_frames = int(sample_rate * chunk_size_ms / 1000)
min_silence_chunks = int(min_silence_duration_ms / chunk_size_ms)
max_chunks = int(max_recording_duration_s * 1000 / chunk_size_ms)
print(f"Listening... (max {max_recording_duration_s}s). Speak when ready.")
print(f"(Silence threshold RMS: {silence_threshold_rms}, Min silence duration: {min_silence_duration_ms}ms)")
recorded_frames = []
pre_roll_frames = []
is_recording = False
silence_counter = 0
chunks_recorded = 0
with sd.InputStream(samplerate=sample_rate, channels=channels, dtype='float32') as stream:
for i in range(max_chunks):
audio_chunk, overflowed = stream.read(chunk_size_frames)
if overflowed:
print("Warning: Audio buffer overflowed!")
rms = np.sqrt(np.mean(audio_chunk**2))
if is_recording:
recorded_frames.append(audio_chunk)
chunks_recorded += 1
if rms < silence_threshold_rms:
silence_counter += 1
if silence_counter >= min_silence_chunks:
print("Silence detected, stopping recording.")
break
else:
silence_counter = 0 # Reset silence counter on sound
else:
pre_roll_frames.append(audio_chunk)
if len(pre_roll_frames) > pre_roll_chunks_count:
pre_roll_frames.pop(0)
if rms > silence_threshold_rms:
print("Speech detected, starting recording.")
is_recording = True
for frame_to_add in pre_roll_frames:
recorded_frames.append(frame_to_add)
chunks_recorded = len(recorded_frames)
pre_roll_frames.clear()
if i == max_chunks - 1 and not is_recording:
print("No speech detected within the maximum recording duration.")
return None, sample_rate
if not recorded_frames and is_recording:
print("Recording started but captured no frames before stopping. This might be due to immediate silence.")
if not recorded_frames:
print("No audio was recorded.")
return None, sample_rate
audio_data = np.concatenate(recorded_frames)
print(f"Recording finished. Total duration: {len(audio_data)/sample_rate:.2f}s")
return audio_data, sample_rate
def play_audio_data(audio_data, sample_rate):
"""Plays in-memory audio data (NumPy array)."""
try:
print(f"Playing in-memory audio data (Sample rate: {sample_rate} Hz, Duration: {len(audio_data)/sample_rate:.2f}s)")
sd.play(audio_data, sample_rate)
sd.wait()
print("Playback from memory finished.")
except Exception as e:
print(f"Error playing in-memory audio: {e}")
if __name__ == "__main__":
print("--- Testing audio_utils.py ---")
# Test 1: record_audio() and play_audio_data() (in-memory)
print("\n--- Test: Record and Play In-Memory Audio ---")
print("Please speak into the microphone. Recording will start on sound and stop on silence.")
recorded_audio, rec_sr = record_audio(
sample_rate=DEFAULT_SAMPLE_RATE,
silence_threshold_rms=0.02,
min_silence_duration_ms=1500,
max_recording_duration_s=10
)
if recorded_audio is not None and rec_sr is not None:
print(f"Recorded audio data shape: {recorded_audio.shape}, Sample rate: {rec_sr} Hz")
play_audio_data(recorded_audio, rec_sr)
else:
print("No audio recorded or recording failed.")
print("\n--- audio_utils.py tests finished. ---")
================================================
FILE: cookbook/pocketflow-voice-chat/utils/call_llm.py
================================================
from openai import OpenAI
import os
def call_llm(messages):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0.7
)
return response.choices[0].message.content
if __name__ == "__main__":
# Test the LLM call
messages = [{"role": "user", "content": "In a few words, what's the meaning of life?"}]
response = call_llm(messages)
print(f"Prompt: {messages[0]['content']}")
print(f"Response: {response}")
================================================
FILE: cookbook/pocketflow-voice-chat/utils/speech_to_text.py
================================================
import os
from openai import OpenAI
import io
def speech_to_text_api(audio_data: bytes, sample_rate: int):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
# The API expects a file-like object. We can use io.BytesIO for in-memory bytes.
# We also need to give it a name, as if it were a file upload.
audio_file = io.BytesIO(audio_data)
audio_file.name = "audio.wav" # Corrected to WAV format
transcript = client.audio.transcriptions.create(
model="gpt-4o-transcribe",
file=audio_file
# language="en" # Optional: specify language ISO-639-1 code
# prompt="PocketFlow, LLM" # Optional: provide a prompt to guide the model
)
return transcript.text
if __name__ == "__main__":
print("Testing Speech-to-Text API...")
# The OpenAI client will raise an error if API key is not found or invalid.
# No explicit check here to keep it minimal.
test_audio_path = "tts_output.mp3"
if os.path.exists(test_audio_path):
print(f"Found {test_audio_path}, using it for STT test.")
with open(test_audio_path, "rb") as f:
audio_bytes_for_stt = f.read()
# Sample rate for tts_output.mp3 from our TTS script is 24000
# but Whisper should ideally infer or handle common formats well.
stt_sample_rate = 24000
transcribed_text = speech_to_text_api(audio_bytes_for_stt, stt_sample_rate)
if transcribed_text:
print(f"Transcribed text: {transcribed_text}")
else:
print("Failed to transcribe audio (API returned empty data).")
else:
print(f"Test audio file '{test_audio_path}' not found.")
print("Please run the text_to_speech.py test first to generate it, or place your own audio file")
print(" (e.g., named 'test_audio.mp3') in the same directory as this script and modify the path.")
print("Make sure it's a common audio format like MP3, WAV, M4A etc.")
================================================
FILE: cookbook/pocketflow-voice-chat/utils/text_to_speech.py
================================================
import os
from openai import OpenAI
def text_to_speech_api(text_to_synthesize: str):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
response = client.audio.speech.create(
model="gpt-4o-mini-tts",
voice="alloy", # Other voices: echo, fable, onyx, nova, shimmer
input=text_to_synthesize,
response_format="mp3" # Other formats: opus, aac, flac. MP3 is widely supported.
# OpenAI default sample rate for tts-1 is 24kHz.
)
# The response.content is already bytes (the audio data)
# Alternatively, for streaming and saving to file: response.stream_to_file("output.mp3")
audio_data_bytes = response.content
sample_rate = 24000 # OpenAI TTS model tts-1 outputs 24kHz
return audio_data_bytes, sample_rate
if __name__ == "__main__":
print("Testing Text-to-Speech API...")
# The OpenAI client will raise an error if API key is not found or invalid.
# No explicit check here to keep it minimal.
text = "Hello from PocketFlow! This is a test of the text-to-speech functionality."
audio_bytes, rate = text_to_speech_api(text)
if audio_bytes and rate:
print(f"Successfully converted text to speech. Audio data length: {len(audio_bytes)} bytes, Sample rate: {rate} Hz.")
with open('tts_output.mp3', 'wb') as f:
f.write(audio_bytes)
print("Saved TTS output to tts_output.mp3")
else:
print("Failed to convert text to speech (API returned empty data).")
================================================
FILE: cookbook/pocketflow-workflow/README.md
================================================
# Article Writing Workflow
A PocketFlow example that demonstrates an article writing workflow using a sequence of LLM calls.
## Features
- Generate a simple outline with up to 3 main sections using YAML structured output
- Write concise (100 words max) content for each section in simple terms
- Apply a conversational, engaging style to the final article
## Getting Started
1. Install the required dependencies:
```bash
pip install -r requirements.txt
```
2. Set your OpenAI API key as an environment variable:
```bash
export OPENAI_API_KEY=your_api_key_here
```
3. Run the application with a default topic ("AI Safety"):
```bash
python main.py
```
4. Or specify your own topic:
```bash
python main.py Climate Change
```
## How It Works
The workflow consists of three sequential nodes:
```mermaid
graph LR
Outline[Generate Outline] --> Write[Write Content]
Write --> Style[Apply Style]
```
Here's what each node does:
1. **Generate Outline**: Creates a simple outline with up to 3 main sections using YAML structured output
2. **Write Simple Content**: Writes a concise 100-word explanation for each section
3. **Apply Style**: Rewrites the combined content in a conversational, engaging style
## Files
- [`main.py`](./main.py): Main entry point for running the article workflow
- [`flow.py`](./flow.py): Defines the flow that connects the nodes
- [`nodes.py`](./nodes.py): Contains the node classes for each step in the workflow
- [`utils/call_llm.py`](./utils/call_llm.py): LLM utility function
- [`requirements.txt`](./requirements.txt): Lists the required dependencies
## Example Output
```
=== Starting Article Workflow on Topic: AI Safety ===
===== OUTLINE (YAML) =====
sections:
- Introduction to AI Safety
- Key Challenges in AI Safety
- Strategies for Ensuring AI Safety
===== PARSED OUTLINE =====
1. Introduction to AI Safety
2. Key Challenges in AI Safety
3. Strategies for Ensuring AI Safety
=========================
===== SECTION CONTENTS =====
--- Introduction to AI Safety ---
AI Safety is about making sure that artificial intelligence (AI) systems are helpful and not harmful. Imagine teaching a robot to help with chores. AI Safety is like setting ground rules for the robot so it doesn't accidentally cause trouble, like mistaking a pet for a toy. By ensuring AI systems understand their tasks and limitations, we can trust them to act safely. It's about creating guidelines and checks to ensure AI assists us without unintended consequences.
--- Key Challenges in AI Safety ---
AI safety is about ensuring that artificial intelligence systems operate in ways that are beneficial and not harmful. One key challenge is making sure AI makes decisions that align with human values. Imagine teaching a robot to fetch coffee, but it ends up knocking things over because it doesn't understand the mess it creates. Similarly, if AI systems don't fully grasp human intentions, they might act in unexpected ways. The task is to make AI smart enough to achieve goals without causing problems, much like training a puppy to follow rules without chewing on your shoes.
--- Strategies for Ensuring AI Safety ---
Ensuring AI safety is about making sure artificial intelligence behaves as expected and doesn’t cause harm. Imagine AI as a new driver on the road; we need rules and safeguards to prevent accidents. By testing AI systems under different conditions, setting clear rules for their behavior, and keeping human oversight, we can manage risks. For instance, just as cars have brakes to ensure safety, AI systems need to have fail-safes. This helps in building trust and avoiding unexpected issues, keeping both humans and AI on the right track.
===========================
===== FINAL ARTICLE =====
# Welcome to the World of AI Safety
Have you ever wondered what it would be like to have your very own robot helping you around the house? Sounds like a dream, right? But let’s hit pause for a moment. What if this robot mistook your fluffy cat for a toy? That’s exactly where AI Safety comes in. Think of AI Safety as setting some friendly ground rules for your household helper, ensuring that it knows the difference between doing chores and causing a bit of chaos. It’s all about making sure our AI allies play by the rules, making life easier without those pesky accidental hiccups.
# Navigating the Maze of AI Challenges
Picture this: you've asked your trusty robot to grab you a cup of coffee. But instead, it sends mugs flying and spills coffee because it doesn’t quite get the concept of a mess. Frustrating, isn’t it? One of the biggest hurdles in AI Safety is aligning AI decisions with our human values and intentions. It’s like training a puppy not to gnaw on your favorite pair of shoes. Our job is to teach AI how to reach its goals without stepping on our toes, all while being as reliable and lovable as a well-trained pup.
# Steering AI Toward Safe Horizons
Now, how do we keep our AI friends on the straight and narrow? Imagine AI as a new driver learning to navigate the roads of life. Just like we teach new drivers the rules of the road and equip cars with brakes for safety, we provide AI with guidelines and fail-safes to prevent any unintended mishaps. Testing AI systems in various scenarios and keeping a watchful human eye on them ensures they don’t veer off track. It’s all about building trust and creating a partnership where both humans and AI are cruising smoothly together.
# Wrapping It Up
At the end of the day, AI Safety is about creating a harmonious relationship between humans and machines, where we trust our metal companions to support us without the fear of unexpected surprises. By setting boundaries and ensuring understanding, we’re not just building smarter machines—we’re crafting a future where AI and humanity can thrive together. So, next time you’re imagining that helpful robot assistant, rest easy knowing that AI Safety is making sure it's ready to lend a hand without dropping the ball—or your coffee mug!
========================
=== Workflow Completed ===
Topic: AI Safety
Outline Length: 96 characters
Draft Length: 1690 characters
Final Article Length: 2266 characters
```
================================================
FILE: cookbook/pocketflow-workflow/flow.py
================================================
from pocketflow import Flow
from nodes import GenerateOutline, WriteSimpleContent, ApplyStyle
def create_article_flow():
"""
Create and configure the article writing workflow
"""
# Create node instances
outline_node = GenerateOutline()
write_node = WriteSimpleContent()
style_node = ApplyStyle()
# Connect nodes in sequence
outline_node >> write_node >> style_node
# Create flow starting with outline node
article_flow = Flow(start=outline_node)
return article_flow
================================================
FILE: cookbook/pocketflow-workflow/main.py
================================================
from flow import create_article_flow
def run_flow(topic="AI Safety"):
"""
Run the article writing workflow with a specific topic
Args:
topic (str): The topic for the article
"""
# Initialize shared data with the topic
shared = {"topic": topic}
# Print starting message
print(f"\n=== Starting Article Workflow on Topic: {topic} ===\n")
# Run the flow
flow = create_article_flow()
flow.run(shared)
# Output summary
print("\n=== Workflow Completed ===\n")
print(f"Topic: {shared['topic']}")
print(f"Outline Length: {len(shared['outline'])} characters")
print(f"Draft Length: {len(shared['draft'])} characters")
print(f"Final Article Length: {len(shared['final_article'])} characters")
return shared
if __name__ == "__main__":
import sys
# Get topic from command line if provided
topic = "AI Safety" # Default topic
if len(sys.argv) > 1:
topic = " ".join(sys.argv[1:])
run_flow(topic)
================================================
FILE: cookbook/pocketflow-workflow/nodes.py
================================================
import re
from pocketflow import Node, BatchNode
from utils.call_llm import call_llm
import yaml
class GenerateOutline(Node):
def prep(self, shared):
return shared["topic"]
def exec(self, topic):
prompt = f"""
Create a simple outline for an article about {topic}.
Include at most 3 main sections (no subsections).
Output the sections in YAML format as shown below:
```yaml
sections:
- |
First section
- |
Second section
- |
Third section
```"""
response = call_llm(prompt)
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
structured_result = yaml.safe_load(yaml_str)
return structured_result
def post(self, shared, prep_res, exec_res):
# Store the structured data
shared["outline_yaml"] = exec_res
# Extract sections
sections = exec_res["sections"]
shared["sections"] = sections
# Format for display
formatted_outline = "\n".join([f"{i+1}. {section}" for i, section in enumerate(sections)])
shared["outline"] = formatted_outline
# Display the results
print("\n===== OUTLINE (YAML) =====\n")
print(yaml.dump(exec_res, default_flow_style=False))
print("\n===== PARSED OUTLINE =====\n")
print(formatted_outline)
print("\n=========================\n")
return "default"
class WriteSimpleContent(BatchNode):
def prep(self, shared):
# Get the list of sections to process and store for progress tracking
self.sections = shared.get("sections", [])
return self.sections
def exec(self, section):
prompt = f"""
Write a short paragraph (MAXIMUM 100 WORDS) about this section:
{section}
Requirements:
- Explain the idea in simple, easy-to-understand terms
- Use everyday language, avoiding jargon
- Keep it very concise (no more than 100 words)
- Include one brief example or analogy
"""
content = call_llm(prompt)
# Show progress for this section
current_section_index = self.sections.index(section) if section in self.sections else 0
total_sections = len(self.sections)
print(f"✓ Completed section {current_section_index + 1}/{total_sections}: {section}")
return section, content
def post(self, shared, prep_res, exec_res_list):
# exec_res_list contains [(section, content), (section, content), ...]
section_contents = {}
all_sections_content = []
for section, content in exec_res_list:
section_contents[section] = content
all_sections_content.append(f"## {section}\n\n{content}\n")
draft = "\n".join(all_sections_content)
# Store the section contents and draft
shared["section_contents"] = section_contents
shared["draft"] = draft
print("\n===== SECTION CONTENTS =====\n")
for section, content in section_contents.items():
print(f"--- {section} ---")
print(content)
print()
print("===========================\n")
return "default"
class ApplyStyle(Node):
def prep(self, shared):
"""
Get the draft from shared data
"""
return shared["draft"]
def exec(self, draft):
"""
Apply a specific style to the article
"""
prompt = f"""
Rewrite the following draft in a conversational, engaging style:
{draft}
Make it:
- Conversational and warm in tone
- Include rhetorical questions that engage the reader
- Add analogies and metaphors where appropriate
- Include a strong opening and conclusion
"""
return call_llm(prompt)
def post(self, shared, prep_res, exec_res):
"""
Store the final article in shared data
"""
shared["final_article"] = exec_res
print("\n===== FINAL ARTICLE =====\n")
print(exec_res)
print("\n========================\n")
return "default"
================================================
FILE: cookbook/pocketflow-workflow/requirements.txt
================================================
pocketflow>=0.0.1
openai>=1.0.0
pyyaml>=6.0
================================================
FILE: cookbook/pocketflow-workflow/utils/call_llm.py
================================================
import os
from openai import OpenAI
def call_llm(prompt):
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", "your-api-key"))
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
# Example usage
if __name__ == "__main__":
print(call_llm("Tell me a short joke"))
================================================
FILE: cookbook/pocketflow_demo.ipynb
================================================
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"! pip install pocketflow\n",
"! pip install faiss-cpu\n",
"! pip install openai"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"
\n",
" Utility Functions are the helper functions like calling an LLM, generating embeddings, or using external APIs. Pocket Flow is deliberately kept minimal and does NOT provide any of these. \n",
"
\n",
"\n",
"
\n",
"But don’t worry: you can simply ask Cursor AI to create them for you. \n",
"
\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> Help me implement (1) `call_llm` function that takes a prompt and returns the response from the OpenAI gpt-4o model. (2) `get_embedding` function that takes a text and returns the embedding from the OpenAI text-embedding-ada-002 model. "
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"from openai import OpenAI\n",
"import os\n",
"\n",
"def call_llm(prompt):\n",
" client = OpenAI(api_key=API_KEY)\n",
" response = client.chat.completions.create(\n",
" model=\"gpt-4o\",\n",
" messages=[{\"role\": \"user\", \"content\": prompt}]\n",
" )\n",
" return response.choices[0].message.content\n",
"\n",
"def get_embedding(text):\n",
" client = OpenAI(api_key=API_KEY)\n",
" response = client.embeddings.create(\n",
" model=\"text-embedding-ada-002\",\n",
" input=text\n",
" )\n",
" return response.data[0].embedding\n",
"\n",
"# Example usage:\n",
"response = call_llm(\"What's the meaning of life?\")\n",
"print(response)\n",
"embedding = get_embedding(\"What's the meaning of life?\")\n",
"print(embedding)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"
\n",
"2. Node\n",
"
\n",
"\n",
" \n",
"
\n",
" A Node is your smallest unit of work with 3 steps \n",
" prep->exec->post:\n",
"
\n",
"\n",
"\n",
"\n",
"\n",
"
\n",
"
\n",
" prep(shared)\n",
"
\n",
"
\n",
" - Reads and preprocess data from the shared store.\n",
"
\n",
"
\n",
" - E.g., load a file, query a database, or turn data into a string.\n",
"
\n",
"
\n",
"\n",
"
\n",
"
\n",
" exec(prep_res)\n",
"
\n",
"
\n",
" - Executes the core logic\n",
"
\n",
"
\n",
" - E.g., call an LLM, invoke remote APIs, or embed texts.\n",
"
\n",
"
\n",
"\n",
"
\n",
"
\n",
" post(shared, prep_res, exec_res)\n",
"
\n",
"
\n",
" - Writes data back to the shared store.\n",
"
\n",
"
\n",
"\n",
"\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> Help me implement a single summarization node that reads data from the shared store, calls an LLM to summarize the text into 50 words, and writes the summary back to the shared store. Then, test it with a shared store that have pre-loaded data from `./data/PaulGrahamEssaysLarge/before.txt`."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Summary: This essay highlights the counterintuitive nature of startups, emphasizing that instincts often lead to mistakes. Key advice includes trusting instincts about people, not needing deep startup knowledge, and focusing on creating products users want. Startups are all-consuming, best pursued after college, and require openness to learning and serendipity.\n"
]
}
],
"source": [
"from pocketflow import Node\n",
"\n",
"class SummarizeNode(Node):\n",
" def prep(self, shared):\n",
" # Read data from shared store\n",
" return shared[\"data\"][\"before.txt\"]\n",
" \n",
" def exec(self, text):\n",
" # Call LLM to summarize\n",
" prompt = f\"Summarize this text in 50 words:\\n\\n{text}\"\n",
" return call_llm(prompt)\n",
" \n",
" def post(self, shared, prep_res, exec_res):\n",
" # Store the summary back\n",
" shared[\"summary\"] = exec_res\n",
" # No specific next action needed\n",
" return \"default\"\n",
"\n",
"# Create test data\n",
"shared = {\n",
" \"data\": {},\n",
" \"summary\": None\n",
"}\n",
"\n",
"# Load the file\n",
"with open(\"./data/PaulGrahamEssaysLarge/before.txt\", \"r\") as f:\n",
" shared[\"data\"][\"before.txt\"] = f.read()\n",
"\n",
"# Create and run the node\n",
"summarize_node = SummarizeNode()\n",
"summarize_node.run(shared)\n",
"\n",
"# Print the result\n",
"print(\"Summary:\", shared[\"summary\"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" \n",
"
\n",
" 3. Batch\n",
"
\n",
"\n",
" \n",
"
\n",
" Batch helps repeat the same work multiple items. \n",
" Instead of calling exec() once, a Batch Node calls \n",
" exec() \n",
" for each item in a list from prep(). \n",
"
\n",
"
\n",
" Think of it as \"item-by-item\" processing:\n",
"
\n",
"\n",
" \n",
"
\n",
"
\n",
" prep(shared): Return a list of items.\n",
"
\n",
"
\n",
" exec(item): Called once per item.\n",
"
\n",
"
\n",
" post(shared, item_list, results_list): Combines all results.\n",
"
\n",
"
\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> Help me implement a batch summarization node that reads the list of data from the shared store, calls an LLM to summarize the text into 50 words, and writes the summary back to the shared store. Then, test it with a shared store that have pre-loaded all text files from `./data/PaulGrahamEssaysLarge/`."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Summaries:\n",
"\n",
"aord.txt:\n",
"The text discusses the critical concern of whether startups are \"default alive\" or \"default dead,\" meaning whether they can reach profitability with existing resources. Many founders are unaware of this status. Addressing this concern early is vital since assumptions about easy fundraising can be misleading. Over-hiring is a common pitfall, emphasizing growth over prudent scaling.\n",
"\n",
"apple.txt:\n",
"Apple's App Store approval process is harming its reputation with developers, damaging their goodwill and causing app delays. The approval system, akin to outdated software publishing, obstructs modern iterative app development. This misalignment with programmers' needs risks alienating talented potential employees and developers essential for Apple's platform success.\n",
"\n",
"avg.txt:\n",
"In 1995, Paul Graham and Robert Morris founded Viaweb, a startup enabling users to create online stores. Using Lisp for its innovative capabilities, they gained a competitive edge due to Lisp's rapid development potential. Viaweb's success highlighted Lisp’s power, challenging conventional language choices and showcasing unconventional advantages in business.\n",
"\n",
"before.txt:\n",
"The text advises potential startup founders to understand the counterintuitive nature of startups, emphasizing trust in instincts about people, focusing on solving user problems, and avoiding the illusion of gaming the system. It suggests gaining broad knowledge, exploring diverse interests, and delaying startup efforts until post-college to maximize potential and personal growth.\n",
"\n",
"addiction.txt:\n",
"The text discusses the accelerating process of technological progress, leading to more addictive forms of various substances and experiences. It warns that this trend will continue, making it harder to distinguish between beneficial and harmful advancements. Society must adapt by developing new customs to manage increasing addiction, while individuals need to find personal strategies to avoid negative impacts.\n"
]
}
],
"source": [
"from pocketflow import BatchNode\n",
"import os\n",
"\n",
"class BatchSummarizeNode(BatchNode):\n",
" def prep(self, shared):\n",
" # Return list of (filename, content) tuples from shared store\n",
" return [(fn, content) for fn, content in shared[\"data\"].items()]\n",
" \n",
" def exec(self, item):\n",
" # Unpack the filename and content\n",
" filename, text = item\n",
" # Call LLM to summarize\n",
" prompt = f\"Summarize this text in 50 words:\\n\\n{text}\"\n",
" summary = call_llm(prompt)\n",
" return filename, summary\n",
" \n",
" def post(self, shared, prep_res, exec_res_list):\n",
" # Store all summaries in a dict by filename\n",
" shared[\"summaries\"] = {\n",
" filename: summary \n",
" for filename, summary in exec_res_list\n",
" }\n",
" return \"default\"\n",
"\n",
"# Create test data structure\n",
"shared = {\n",
" \"data\": {},\n",
" \"summaries\": {}\n",
"}\n",
"\n",
"# Load all files from the directory\n",
"path = \"./data/PaulGrahamEssaysLarge\"\n",
"for filename in os.listdir(path):\n",
" with open(os.path.join(path, filename), \"r\") as f:\n",
" shared[\"data\"][filename] = f.read()\n",
"\n",
"# Create and run the batch node\n",
"batch_summarize = BatchSummarizeNode()\n",
"batch_summarize.run(shared)\n",
"\n",
"# Print results\n",
"print(\"Summaries:\")\n",
"for filename, summary in shared[\"summaries\"].items():\n",
" print(f\"\\n{filename}:\")\n",
" print(summary)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
" \n",
"
\n",
" 4. Flow\n",
"
\n",
"\n",
" \n",
"
\n",
" Flow connects your Nodes to a graph.\n",
"
\n",
"\n",
" \n",
"
\n",
"
\n",
" Chaining \n",
" (node_1 >> node_2): Break down complex problems into simple chained steps.\n",
"
\n",
" Set a Start Point: Create flow by specifying \n",
" Flow(start=node_a). \n",
" Then call \n",
" flow.run(shared).\n",
"
\n",
"
\n",
"\n",
" \n",
"
\n",
" That’s it! You can nest Flows, branch your actions, or keep it simple with a straight chain of Nodes.\n",
"
"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> Help me implement a RAG chatbot that, given a user’s input question, finds the most relevant file based on embeddings and then answers the user's question. Test it with a shared store that has preloaded all text files from `./data/PaulGrahamEssaysLarge/`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
""
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Q: how to find startup idea\n",
"A: To find a startup idea, the context advises not to make a conscious effort to think of startup ideas, as this often results in bad and plausible-sounding ideas that can waste time. Instead, it suggests turning your mind into the type that generates startup ideas unconsciously. This can be achieved by:\n",
"\n",
"1. Learning extensively about things that matter.\n",
"2. Working on problems that genuinely interest you.\n",
"3. Collaborating with people you like and respect.\n",
"\n",
"By engaging in these activities, you'll naturally start to encounter ideas that have the potential to become startups, often without initially realizing it. The essay emphasizes that many successful startups, like Apple, Yahoo, Google, and Facebook, began as side projects rather than direct pursuits to start a company.\n",
"\n",
"Source: before.txt\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"/home/zh2408/.venv/lib/python3.9/site-packages/pocketflow/__init__.py:43: UserWarning: Flow ends: 'end' not found in ['answer', 'end']\n",
" if not nxt and curr.successors: warnings.warn(f\"Flow ends: '{action}' not found in {list(curr.successors)}\")\n"
]
}
],
"source": [
"from pocketflow import Node, Flow\n",
"import faiss\n",
"import numpy as np\n",
"import os\n",
"\n",
"class PrepareEmbeddings(Node):\n",
" def prep(self, shared):\n",
" # Get list of (filename, content) pairs\n",
" return list(shared[\"data\"].items())\n",
" \n",
" def exec(self, items):\n",
" # Create embeddings for each document\n",
" embeddings = []\n",
" filenames = []\n",
" for filename, content in items:\n",
" embedding = get_embedding(content)\n",
" embeddings.append(embedding)\n",
" filenames.append(filename)\n",
" \n",
" # Create FAISS index\n",
" dim = len(embeddings[0])\n",
" index = faiss.IndexFlatL2(dim)\n",
" index.add(np.array(embeddings).astype('float32'))\n",
" \n",
" return index, filenames\n",
" \n",
" def post(self, shared, prep_res, exec_res):\n",
" # Store index and filenames in shared store\n",
" index, filenames = exec_res\n",
" shared[\"search_index\"] = index\n",
" shared[\"filenames\"] = filenames\n",
" return \"default\"\n",
"\n",
"class FindRelevantDocument(Node):\n",
" def prep(self, shared):\n",
" # Get user question\n",
" question = input(\"Enter your question (or press Enter to quit): \")\n",
" if not question:\n",
" return None\n",
" return question\n",
" \n",
" def exec(self, question):\n",
" if question is None:\n",
" return None\n",
" \n",
" # Get question embedding and search\n",
" query_embedding = get_embedding(question)\n",
" \n",
" # Search for most similar document\n",
" D, I = shared[\"search_index\"].search(\n",
" np.array([query_embedding]).astype('float32'),\n",
" k=1\n",
" )\n",
" most_relevant_idx = I[0][0]\n",
" most_relevant_file = shared[\"filenames\"][most_relevant_idx]\n",
" \n",
" return question, most_relevant_file\n",
" \n",
" def post(self, shared, prep_res, exec_res):\n",
" if exec_res is None:\n",
" return \"end\"\n",
" \n",
" question, filename = exec_res\n",
" shared[\"current_question\"] = question\n",
" shared[\"relevant_file\"] = filename\n",
" shared[\"context\"] = shared[\"data\"][filename]\n",
" return \"answer\"\n",
" \n",
"class AnswerQuestion(Node):\n",
" def prep(self, shared):\n",
" return (\n",
" shared[\"current_question\"],\n",
" shared[\"context\"]\n",
" )\n",
" \n",
" def exec(self, inputs):\n",
" question, context = inputs\n",
" prompt = f\"\"\"\n",
"Context: {context}\n",
"\n",
"Question: {question}\n",
"\n",
"Answer the question based on the context above. If the context doesn't contain relevant information, say so.\n",
"Answer:\"\"\"\n",
" return call_llm(prompt)\n",
" \n",
" def post(self, shared, prep_res, exec_res):\n",
" print(f\"\\nQ: {shared['current_question']}\")\n",
" print(f\"A: {exec_res}\")\n",
" print(f\"\\nSource: {shared['relevant_file']}\")\n",
" return \"continue\" # Loop back for more questions\n",
"\n",
"# Create test data\n",
"shared = {\"data\": {}}\n",
"\n",
"# Load all files\n",
"path = \"./data/PaulGrahamEssaysLarge\"\n",
"for filename in os.listdir(path):\n",
" with open(os.path.join(path, filename), \"r\") as f:\n",
" shared[\"data\"][filename] = f.read()\n",
"\n",
"# Create nodes and flow\n",
"prep_embeddings = PrepareEmbeddings()\n",
"find_relevant = FindRelevantDocument()\n",
"answer = AnswerQuestion()\n",
"\n",
"# Connect nodes\n",
"prep_embeddings >> find_relevant\n",
"find_relevant - \"answer\" >> answer\n",
"find_relevant - \"end\" >> None\n",
"answer - \"continue\" >> find_relevant\n",
"\n",
"# Create and run flow\n",
"rag_flow = Flow(start=prep_embeddings)\n",
"rag_flow.run(shared)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "myvenv",
"language": "python",
"name": "myvenv"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.2"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
================================================
FILE: docs/_config.yml
================================================
# Basic site settings
title: Pocket Flow
tagline: A 100-line LLM framework
description: Pocket Flow – Minimalist LLM Framework in 100 Lines, Enabling LLMs to Program Themselves
# Theme settings
remote_theme: just-the-docs/just-the-docs
search_enabled: true
# SEO & sitemap
plugins:
- jekyll-seo-tag
- jekyll-sitemap
jekyll-seo-tag:
social:
name: "Pocket Flow"
twitter: "ZacharyHuang12"
github: "the-pocket/PocketFlow"
# Navigation
nav_sort: case_sensitive
# Aux links (shown in upper right)
aux_links:
"View on GitHub":
- "//github.com/the-pocket/PocketFlow"
# Color scheme
color_scheme: light
# Author settings
author:
name: Zachary Huang
url: https://www.columbia.edu/~zh2408/
twitter: ZacharyHuang12
# Mermaid settings
mermaid:
version: "9.1.3"
config: |
directionLR
# Callouts settings
callouts:
warning:
title: Warning
color: red
note:
title: Note
color: blue
best-practice:
title: Best Practice
color: green
# Custom navigation
nav:
- Home: index.md
- GitHub: "https://github.com/the-pocket/PocketFlow"
- Discord: "https://discord.gg/hUHHE9Sa6T"
================================================
FILE: docs/_includes/footer_custom.html
================================================
================================================
FILE: docs/core_abstraction/async.md
================================================
---
layout: default
title: "(Advanced) Async"
parent: "Core Abstraction"
nav_order: 5
---
# (Advanced) Async
**Async** Nodes implement `prep_async()`, `exec_async()`, `exec_fallback_async()`, and/or `post_async()`. This is useful for:
1. **prep_async()**: For *fetching/reading data (files, APIs, DB)* in an I/O-friendly way.
2. **exec_async()**: Typically used for async LLM calls.
3. **post_async()**: For *awaiting user feedback*, *coordinating across multi-agents* or any additional async steps after `exec_async()`.
**Note**: `AsyncNode` must be wrapped in `AsyncFlow`. `AsyncFlow` can also include regular (sync) nodes.
### Example
```python
class SummarizeThenVerify(AsyncNode):
async def prep_async(self, shared):
# Example: read a file asynchronously
doc_text = await read_file_async(shared["doc_path"])
return doc_text
async def exec_async(self, prep_res):
# Example: async LLM call
summary = await call_llm_async(f"Summarize: {prep_res}")
return summary
async def post_async(self, shared, prep_res, exec_res):
# Example: wait for user feedback
decision = await gather_user_feedback(exec_res)
if decision == "approve":
shared["summary"] = exec_res
return "approve"
return "deny"
summarize_node = SummarizeThenVerify()
final_node = Finalize()
# Define transitions
summarize_node - "approve" >> final_node
summarize_node - "deny" >> summarize_node # retry
flow = AsyncFlow(start=summarize_node)
async def main():
shared = {"doc_path": "document.txt"}
await flow.run_async(shared)
print("Final Summary:", shared.get("summary"))
asyncio.run(main())
```
================================================
FILE: docs/core_abstraction/batch.md
================================================
---
layout: default
title: "Batch"
parent: "Core Abstraction"
nav_order: 4
---
# Batch
**Batch** makes it easier to handle large inputs in one Node or **rerun** a Flow multiple times. Example use cases:
- **Chunk-based** processing (e.g., splitting large texts).
- **Iterative** processing over lists of input items (e.g., user queries, files, URLs).
## 1. BatchNode
A **BatchNode** extends `Node` but changes `prep()` and `exec()`:
- **`prep(shared)`**: returns an **iterable** (e.g., list, generator).
- **`exec(item)`**: called **once** per item in that iterable.
- **`post(shared, prep_res, exec_res_list)`**: after all items are processed, receives a **list** of results (`exec_res_list`) and returns an **Action**.
### Example: Summarize a Large File
```python
class MapSummaries(BatchNode):
def prep(self, shared):
# Suppose we have a big file; chunk it
content = shared["data"]
chunk_size = 10000
chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
return chunks
def exec(self, chunk):
prompt = f"Summarize this chunk in 10 words: {chunk}"
summary = call_llm(prompt)
return summary
def post(self, shared, prep_res, exec_res_list):
combined = "\n".join(exec_res_list)
shared["summary"] = combined
return "default"
map_summaries = MapSummaries()
flow = Flow(start=map_summaries)
flow.run(shared)
```
---
## 2. BatchFlow
A **BatchFlow** runs a **Flow** multiple times, each time with different `params`. Think of it as a loop that replays the Flow for each parameter set.
### Key Differences from BatchNode
**Important**: Unlike BatchNode, which processes items and modifies the shared store:
1. BatchFlow returns **parameters to pass to the child Flow**, not data to process
2. These parameters are accessed in child nodes via `self.params`, not from the shared store
3. Each child Flow runs independently with a different set of parameters
4. Child nodes can be regular Nodes, not BatchNodes (the batching happens at the Flow level)
### Example: Summarize Many Files
```python
class SummarizeAllFiles(BatchFlow):
def prep(self, shared):
# IMPORTANT: Return a list of param dictionaries (not data for processing)
filenames = list(shared["data"].keys()) # e.g., ["file1.txt", "file2.txt", ...]
return [{"filename": fn} for fn in filenames]
# Child node that accesses filename from params, not shared store
class LoadFile(Node):
def prep(self, shared):
# Access filename from params (not from shared)
filename = self.params["filename"] # Important! Use self.params, not shared
return filename
def exec(self, filename):
with open(filename, 'r') as f:
return f.read()
def post(self, shared, prep_res, exec_res):
# Store file content in shared
shared["current_file_content"] = exec_res
return "default"
# Summarize node that works on the currently loaded file
class Summarize(Node):
def prep(self, shared):
return shared["current_file_content"]
def exec(self, content):
prompt = f"Summarize this file in 50 words: {content}"
return call_llm(prompt)
def post(self, shared, prep_res, exec_res):
# Store summary in shared, indexed by current filename
filename = self.params["filename"] # Again, using params
if "summaries" not in shared:
shared["summaries"] = {}
shared["summaries"][filename] = exec_res
return "default"
# Create a per-file flow
load_file = LoadFile()
summarize = Summarize()
load_file >> summarize
summarize_file = Flow(start=load_file)
# Wrap in a BatchFlow to process all files
summarize_all_files = SummarizeAllFiles(start=summarize_file)
summarize_all_files.run(shared)
```
### Under the Hood
1. `prep(shared)` in the BatchFlow returns a list of param dicts—e.g., `[{"filename": "file1.txt"}, {"filename": "file2.txt"}, ...]`.
2. The **BatchFlow** loops through each dict. For each one:
- It merges the dict with the BatchFlow's own `params` (if any): `{**batch_flow.params, **dict_from_prep}`
- It calls `flow.run(shared)` using the merged parameters
- **IMPORTANT**: These parameters are passed to the child Flow's nodes via `self.params`, NOT via the shared store
3. This means the sub-Flow is run **repeatedly**, once for every param dict, with each node in the flow accessing the parameters via `self.params`.
---
## 3. Nested or Multi-Level Batches
You can nest a **BatchFlow** in another **BatchFlow**. For instance:
- **Outer** batch: returns a list of directory param dicts (e.g., `{"directory": "/pathA"}`, `{"directory": "/pathB"}`, ...).
- **Inner** batch: returning a list of per-file param dicts.
At each level, **BatchFlow** merges its own param dict with the parent’s. By the time you reach the **innermost** node, the final `params` is the merged result of **all** parents in the chain. This way, a nested structure can keep track of the entire context (e.g., directory + file name) at once.
```python
class FileBatchFlow(BatchFlow):
def prep(self, shared):
# Access directory from params (set by parent)
directory = self.params["directory"]
# e.g., files = ["file1.txt", "file2.txt", ...]
files = [f for f in os.listdir(directory) if f.endswith(".txt")]
return [{"filename": f} for f in files]
class DirectoryBatchFlow(BatchFlow):
def prep(self, shared):
directories = [ "/path/to/dirA", "/path/to/dirB"]
return [{"directory": d} for d in directories]
# The actual processing node
class ProcessFile(Node):
def prep(self, shared):
# Access both directory and filename from params
directory = self.params["directory"] # From outer batch
filename = self.params["filename"] # From inner batch
full_path = os.path.join(directory, filename)
return full_path
def exec(self, full_path):
# Process the file...
return f"Processed {full_path}"
def post(self, shared, prep_res, exec_res):
# Store results, perhaps indexed by path
if "results" not in shared:
shared["results"] = {}
shared["results"][prep_res] = exec_res
return "default"
# Set up the nested batch structure
process_node = ProcessFile()
inner_flow = FileBatchFlow(start=process_node)
outer_flow = DirectoryBatchFlow(start=inner_flow)
# Run it
outer_flow.run(shared)
```
================================================
FILE: docs/core_abstraction/communication.md
================================================
---
layout: default
title: "Communication"
parent: "Core Abstraction"
nav_order: 3
---
# Communication
Nodes and Flows **communicate** in 2 ways:
1. **Shared Store (for almost all the cases)**
- A global data structure (often an in-mem dict) that all nodes can read ( `prep()`) and write (`post()`).
- Great for data results, large content, or anything multiple nodes need.
- You shall design the data structure and populate it ahead.
- > **Separation of Concerns:** Use `Shared Store` for almost all cases to separate *Data Schema* from *Compute Logic*! This approach is both flexible and easy to manage, resulting in more maintainable code. `Params` is more a syntax sugar for [Batch](./batch.md).
{: .best-practice }
2. **Params (only for [Batch](./batch.md))**
- Each node has a local, ephemeral `params` dict passed in by the **parent Flow**, used as an identifier for tasks. Parameter keys and values shall be **immutable**.
- Good for identifiers like filenames or numeric IDs, in Batch mode.
If you know memory management, think of the **Shared Store** like a **heap** (shared by all function calls), and **Params** like a **stack** (assigned by the caller).
---
## 1. Shared Store
### Overview
A shared store is typically an in-mem dictionary, like:
```python
shared = {"data": {}, "summary": {}, "config": {...}, ...}
```
It can also contain local file handlers, DB connections, or a combination for persistence. We recommend deciding the data structure or DB schema first based on your app requirements.
### Example
```python
class LoadData(Node):
def post(self, shared, prep_res, exec_res):
# We write data to shared store
shared["data"] = "Some text content"
return None
class Summarize(Node):
def prep(self, shared):
# We read data from shared store
return shared["data"]
def exec(self, prep_res):
# Call LLM to summarize
prompt = f"Summarize: {prep_res}"
summary = call_llm(prompt)
return summary
def post(self, shared, prep_res, exec_res):
# We write summary to shared store
shared["summary"] = exec_res
return "default"
load_data = LoadData()
summarize = Summarize()
load_data >> summarize
flow = Flow(start=load_data)
shared = {}
flow.run(shared)
```
Here:
- `LoadData` writes to `shared["data"]`.
- `Summarize` reads from `shared["data"]`, summarizes, and writes to `shared["summary"]`.
---
## 2. Params
**Params** let you store *per-Node* or *per-Flow* config that doesn't need to live in the shared store. They are:
- **Immutable** during a Node's run cycle (i.e., they don't change mid-`prep->exec->post`).
- **Set** via `set_params()`.
- **Cleared** and updated each time a parent Flow calls it.
> Only set the uppermost Flow params because others will be overwritten by the parent Flow.
>
> If you need to set child node params, see [Batch](./batch.md).
{: .warning }
Typically, **Params** are identifiers (e.g., file name, page number). Use them to fetch the task you assigned or write to a specific part of the shared store.
### Example
```python
# 1) Create a Node that uses params
class SummarizeFile(Node):
def prep(self, shared):
# Access the node's param
filename = self.params["filename"]
return shared["data"].get(filename, "")
def exec(self, prep_res):
prompt = f"Summarize: {prep_res}"
return call_llm(prompt)
def post(self, shared, prep_res, exec_res):
filename = self.params["filename"]
shared["summary"][filename] = exec_res
return "default"
# 2) Set params
node = SummarizeFile()
# 3) Set Node params directly (for testing)
node.set_params({"filename": "doc1.txt"})
node.run(shared)
# 4) Create Flow
flow = Flow(start=node)
# 5) Set Flow params (overwrites node params)
flow.set_params({"filename": "doc2.txt"})
flow.run(shared) # The node summarizes doc2, not doc1
```
================================================
FILE: docs/core_abstraction/flow.md
================================================
---
layout: default
title: "Flow"
parent: "Core Abstraction"
nav_order: 2
---
# Flow
A **Flow** orchestrates a graph of Nodes. You can chain Nodes in a sequence or create branching depending on the **Actions** returned from each Node's `post()`.
## 1. Action-based Transitions
Each Node's `post()` returns an **Action** string. By default, if `post()` doesn't return anything, we treat that as `"default"`.
You define transitions with the syntax:
1. **Basic default transition**: `node_a >> node_b`
This means if `node_a.post()` returns `"default"`, go to `node_b`.
(Equivalent to `node_a - "default" >> node_b`)
2. **Named action transition**: `node_a - "action_name" >> node_b`
This means if `node_a.post()` returns `"action_name"`, go to `node_b`.
It's possible to create loops, branching, or multi-step flows.
## 2. Creating a Flow
A **Flow** begins with a **start** node. You call `Flow(start=some_node)` to specify the entry point. When you call `flow.run(shared)`, it executes the start node, looks at its returned Action from `post()`, follows the transition, and continues until there's no next node.
### Example: Simple Sequence
Here's a minimal flow of two nodes in a chain:
```python
node_a >> node_b
flow = Flow(start=node_a)
flow.run(shared)
```
- When you run the flow, it executes `node_a`.
- Suppose `node_a.post()` returns `"default"`.
- The flow then sees `"default"` Action is linked to `node_b` and runs `node_b`.
- `node_b.post()` returns `"default"` but we didn't define `node_b >> something_else`. So the flow ends there.
### Example: Branching & Looping
Here's a simple expense approval flow that demonstrates branching and looping. The `ReviewExpense` node can return three possible Actions:
- `"approved"`: expense is approved, move to payment processing
- `"needs_revision"`: expense needs changes, send back for revision
- `"rejected"`: expense is denied, finish the process
We can wire them like this:
```python
# Define the flow connections
review - "approved" >> payment # If approved, process payment
review - "needs_revision" >> revise # If needs changes, go to revision
review - "rejected" >> finish # If rejected, finish the process
revise >> review # After revision, go back for another review
payment >> finish # After payment, finish the process
flow = Flow(start=review)
```
Let's see how it flows:
1. If `review.post()` returns `"approved"`, the expense moves to the `payment` node
2. If `review.post()` returns `"needs_revision"`, it goes to the `revise` node, which then loops back to `review`
3. If `review.post()` returns `"rejected"`, it moves to the `finish` node and stops
```mermaid
flowchart TD
review[Review Expense] -->|approved| payment[Process Payment]
review -->|needs_revision| revise[Revise Report]
review -->|rejected| finish[Finish Process]
revise --> review
payment --> finish
```
### Running Individual Nodes vs. Running a Flow
- `node.run(shared)`: Just runs that node alone (calls `prep->exec->post()`), returns an Action.
- `flow.run(shared)`: Executes from the start node, follows Actions to the next node, and so on until the flow can't continue.
> `node.run(shared)` **does not** proceed to the successor.
> This is mainly for debugging or testing a single node.
>
> Always use `flow.run(...)` in production to ensure the full pipeline runs correctly.
{: .warning }
## 3. Nested Flows
A **Flow** can act like a Node, which enables powerful composition patterns. This means you can:
1. Use a Flow as a Node within another Flow's transitions.
2. Combine multiple smaller Flows into a larger Flow for reuse.
3. Node `params` will be a merging of **all** parents' `params`.
### Flow's Node Methods
A **Flow** is also a **Node**, so it will run `prep()` and `post()`. However:
- It **won't** run `exec()`, as its main logic is to orchestrate its nodes.
- `post()` always receives `None` for `exec_res` and should instead get the flow execution results from the shared store.
### Basic Flow Nesting
Here's how to connect a flow to another node:
```python
# Create a sub-flow
node_a >> node_b
subflow = Flow(start=node_a)
# Connect it to another node
subflow >> node_c
# Create the parent flow
parent_flow = Flow(start=subflow)
```
When `parent_flow.run()` executes:
1. It starts `subflow`
2. `subflow` runs through its nodes (`node_a->node_b`)
3. After `subflow` completes, execution continues to `node_c`
### Example: Order Processing Pipeline
Here's a practical example that breaks down order processing into nested flows:
```python
# Payment processing sub-flow
validate_payment >> process_payment >> payment_confirmation
payment_flow = Flow(start=validate_payment)
# Inventory sub-flow
check_stock >> reserve_items >> update_inventory
inventory_flow = Flow(start=check_stock)
# Shipping sub-flow
create_label >> assign_carrier >> schedule_pickup
shipping_flow = Flow(start=create_label)
# Connect the flows into a main order pipeline
payment_flow >> inventory_flow >> shipping_flow
# Create the master flow
order_pipeline = Flow(start=payment_flow)
# Run the entire pipeline
order_pipeline.run(shared_data)
```
This creates a clean separation of concerns while maintaining a clear execution path:
```mermaid
flowchart LR
subgraph order_pipeline[Order Pipeline]
subgraph paymentFlow["Payment Flow"]
A[Validate Payment] --> B[Process Payment] --> C[Payment Confirmation]
end
subgraph inventoryFlow["Inventory Flow"]
D[Check Stock] --> E[Reserve Items] --> F[Update Inventory]
end
subgraph shippingFlow["Shipping Flow"]
G[Create Label] --> H[Assign Carrier] --> I[Schedule Pickup]
end
paymentFlow --> inventoryFlow
inventoryFlow --> shippingFlow
end
```
================================================
FILE: docs/core_abstraction/index.md
================================================
---
layout: default
title: "Core Abstraction"
nav_order: 2
has_children: true
---
================================================
FILE: docs/core_abstraction/node.md
================================================
---
layout: default
title: "Node"
parent: "Core Abstraction"
nav_order: 1
---
# Node
A **Node** is the smallest building block. Each Node has 3 steps `prep->exec->post`:
1. `prep(shared)`
- **Read and preprocess data** from `shared` store.
- Examples: *query DB, read files, or serialize data into a string*.
- Return `prep_res`, which is used by `exec()` and `post()`.
2. `exec(prep_res)`
- **Execute compute logic**, with optional retries and error handling (below).
- Examples: *(mostly) LLM calls, remote APIs, tool use*.
- ⚠️ This shall be only for compute and **NOT** access `shared`.
- ⚠️ If retries enabled, ensure idempotent implementation.
- ⚠️ Defer exception handling to the Node's built-in retry mechanism.
- Return `exec_res`, which is passed to `post()`.
3. `post(shared, prep_res, exec_res)`
- **Postprocess and write data** back to `shared`.
- Examples: *update DB, change states, log results*.
- **Decide the next action** by returning a *string* (`action = "default"` if *None*).
> **Why 3 steps?** To enforce the principle of *separation of concerns*. The data storage and data processing are operated separately.
>
> All steps are *optional*. E.g., you can only implement `prep` and `post` if you just need to process data.
{: .note }
### Fault Tolerance & Retries
You can **retry** `exec()` if it raises an exception via two parameters when define the Node:
- `max_retries` (int): Max times to run `exec()`. The default is `1` (**no** retry).
- `wait` (int): The time to wait (in **seconds**) before next retry. By default, `wait=0` (no waiting).
`wait` is helpful when you encounter rate-limits or quota errors from your LLM provider and need to back off.
```python
my_node = SummarizeFile(max_retries=3, wait=10)
```
When an exception occurs in `exec()`, the Node automatically retries until:
- It either succeeds, or
- The Node has retried `max_retries - 1` times already and fails on the last attempt.
You can get the current retry times (0-based) from `self.cur_retry`.
```python
class RetryNode(Node):
def exec(self, prep_res):
print(f"Retry {self.cur_retry} times")
raise Exception("Failed")
```
### Graceful Fallback
To **gracefully handle** the exception (after all retries) rather than raising it, override:
```python
def exec_fallback(self, prep_res, exc):
raise exc
```
By default, it just re-raises exception. But you can return a fallback result instead, which becomes the `exec_res` passed to `post()`.
### Example: Summarize file
```python
class SummarizeFile(Node):
def prep(self, shared):
return shared["data"]
def exec(self, prep_res):
if not prep_res:
return "Empty file content"
prompt = f"Summarize this text in 10 words: {prep_res}"
summary = call_llm(prompt) # might fail
return summary
def exec_fallback(self, prep_res, exc):
# Provide a simple fallback instead of crashing
return "There was an error processing your request."
def post(self, shared, prep_res, exec_res):
shared["summary"] = exec_res
# Return "default" by not returning
summarize_node = SummarizeFile(max_retries=3)
# node.run() calls prep->exec->post
# If exec() fails, it retries up to 3 times before calling exec_fallback()
action_result = summarize_node.run(shared)
print("Action returned:", action_result) # "default"
print("Summary stored:", shared["summary"])
```
================================================
FILE: docs/core_abstraction/parallel.md
================================================
---
layout: default
title: "(Advanced) Parallel"
parent: "Core Abstraction"
nav_order: 6
---
# (Advanced) Parallel
**Parallel** Nodes and Flows let you run multiple **Async** Nodes and Flows **concurrently**—for example, summarizing multiple texts at once. This can improve performance by overlapping I/O and compute.
> Because of Python’s GIL, parallel nodes and flows can’t truly parallelize CPU-bound tasks (e.g., heavy numerical computations). However, they excel at overlapping I/O-bound work—like LLM calls, database queries, API requests, or file I/O.
{: .warning }
> - **Ensure Tasks Are Independent**: If each item depends on the output of a previous item, **do not** parallelize.
>
> - **Beware of Rate Limits**: Parallel calls can **quickly** trigger rate limits on LLM services. You may need a **throttling** mechanism (e.g., semaphores or sleep intervals).
>
> - **Consider Single-Node Batch APIs**: Some LLMs offer a **batch inference** API where you can send multiple prompts in a single call. This is more complex to implement but can be more efficient than launching many parallel requests and mitigates rate limits.
{: .best-practice }
## AsyncParallelBatchNode
Like **AsyncBatchNode**, but run `exec_async()` in **parallel**:
```python
class ParallelSummaries(AsyncParallelBatchNode):
async def prep_async(self, shared):
# e.g., multiple texts
return shared["texts"]
async def exec_async(self, text):
prompt = f"Summarize: {text}"
return await call_llm_async(prompt)
async def post_async(self, shared, prep_res, exec_res_list):
shared["summary"] = "\n\n".join(exec_res_list)
return "default"
node = ParallelSummaries()
flow = AsyncFlow(start=node)
```
## AsyncParallelBatchFlow
Parallel version of **BatchFlow**. Each iteration of the sub-flow runs **concurrently** using different parameters:
```python
class SummarizeMultipleFiles(AsyncParallelBatchFlow):
async def prep_async(self, shared):
return [{"filename": f} for f in shared["files"]]
sub_flow = AsyncFlow(start=LoadAndSummarizeFile())
parallel_flow = SummarizeMultipleFiles(start=sub_flow)
await parallel_flow.run_async(shared)
```
================================================
FILE: docs/design_pattern/agent.md
================================================
---
layout: default
title: "Agent"
parent: "Design Pattern"
nav_order: 1
---
# Agent
Agent is a powerful design pattern in which nodes can take dynamic actions based on the context.
## Implement Agent with Graph
1. **Context and Action:** Implement nodes that supply context and perform actions.
2. **Branching:** Use branching to connect each action node to an agent node. Use action to allow the agent to direct the [flow](../core_abstraction/flow.md) between nodes—and potentially loop back for multi-step.
3. **Agent Node:** Provide a prompt to decide action—for example:
```python
f"""
### CONTEXT
Task: {task_description}
Previous Actions: {previous_actions}
Current State: {current_state}
### ACTION SPACE
[1] search
Description: Use web search to get results
Parameters:
- query (str): What to search for
[2] answer
Description: Conclude based on the results
Parameters:
- result (str): Final answer to provide
### NEXT ACTION
Decide the next action based on the current context and available action space.
Return your response in the following format:
```yaml
thinking: |
action:
parameters:
:
```"""
```
The core of building **high-performance** and **reliable** agents boils down to:
1. **Context Management:** Provide *relevant, minimal context.* For example, rather than including an entire chat history, retrieve the most relevant via [RAG](./rag.md). Even with larger context windows, LLMs still fall victim to ["lost in the middle"](https://arxiv.org/abs/2307.03172), overlooking mid-prompt content.
2. **Action Space:** Provide *a well-structured and unambiguous* set of actions—avoiding overlap like separate `read_databases` or `read_csvs`. Instead, import CSVs into the database.
## Example Good Action Design
- **Incremental:** Feed content in manageable chunks (500 lines or 1 page) instead of all at once.
- **Overview-zoom-in:** First provide high-level structure (table of contents, summary), then allow drilling into details (raw texts).
- **Parameterized/Programmable:** Instead of fixed actions, enable parameterized (columns to select) or programmable (SQL queries) actions, for example, to read CSV files.
- **Backtracking:** Let the agent undo the last step instead of restarting entirely, preserving progress when encountering errors or dead ends.
## Example: Search Agent
This agent:
1. Decides whether to search or answer
2. If searches, loops back to decide if more search needed
3. Answers when enough context gathered
```python
class DecideAction(Node):
def prep(self, shared):
context = shared.get("context", "No previous search")
query = shared["query"]
return query, context
def exec(self, inputs):
query, context = inputs
prompt = f"""
Given input: {query}
Previous search results: {context}
Should I: 1) Search web for more info 2) Answer with current knowledge
Output in yaml:
```yaml
action: search/answer
reason: why this action
search_term: search phrase if action is search
```"""
resp = call_llm(prompt)
yaml_str = resp.split("```yaml")[1].split("```")[0].strip()
result = yaml.safe_load(yaml_str)
assert isinstance(result, dict)
assert "action" in result
assert "reason" in result
assert result["action"] in ["search", "answer"]
if result["action"] == "search":
assert "search_term" in result
return result
def post(self, shared, prep_res, exec_res):
if exec_res["action"] == "search":
shared["search_term"] = exec_res["search_term"]
return exec_res["action"]
class SearchWeb(Node):
def prep(self, shared):
return shared["search_term"]
def exec(self, search_term):
return search_web(search_term)
def post(self, shared, prep_res, exec_res):
prev_searches = shared.get("context", [])
shared["context"] = prev_searches + [
{"term": shared["search_term"], "result": exec_res}
]
return "decide"
class DirectAnswer(Node):
def prep(self, shared):
return shared["query"], shared.get("context", "")
def exec(self, inputs):
query, context = inputs
return call_llm(f"Context: {context}\nAnswer: {query}")
def post(self, shared, prep_res, exec_res):
print(f"Answer: {exec_res}")
shared["answer"] = exec_res
# Connect nodes
decide = DecideAction()
search = SearchWeb()
answer = DirectAnswer()
decide - "search" >> search
decide - "answer" >> answer
search - "decide" >> decide # Loop back
flow = Flow(start=decide)
flow.run({"query": "Who won the Nobel Prize in Physics 2024?"})
```
================================================
FILE: docs/design_pattern/index.md
================================================
---
layout: default
title: "Design Pattern"
nav_order: 3
has_children: true
---
================================================
FILE: docs/design_pattern/mapreduce.md
================================================
---
layout: default
title: "Map Reduce"
parent: "Design Pattern"
nav_order: 4
---
# Map Reduce
MapReduce is a design pattern suitable when you have either:
- Large input data (e.g., multiple files to process), or
- Large output data (e.g., multiple forms to fill)
and there is a logical way to break the task into smaller, ideally independent parts.
You first break down the task using [BatchNode](../core_abstraction/batch.md) in the map phase, followed by aggregation in the reduce phase.
### Example: Document Summarization
```python
class SummarizeAllFiles(BatchNode):
def prep(self, shared):
files_dict = shared["files"] # e.g. 10 files
return list(files_dict.items()) # [("file1.txt", "aaa..."), ("file2.txt", "bbb..."), ...]
def exec(self, one_file):
filename, file_content = one_file
summary_text = call_llm(f"Summarize the following file:\n{file_content}")
return (filename, summary_text)
def post(self, shared, prep_res, exec_res_list):
shared["file_summaries"] = dict(exec_res_list)
class CombineSummaries(Node):
def prep(self, shared):
return shared["file_summaries"]
def exec(self, file_summaries):
# format as: "File1: summary\nFile2: summary...\n"
text_list = []
for fname, summ in file_summaries.items():
text_list.append(f"{fname} summary:\n{summ}\n")
big_text = "\n---\n".join(text_list)
return call_llm(f"Combine these file summaries into one final summary:\n{big_text}")
def post(self, shared, prep_res, final_summary):
shared["all_files_summary"] = final_summary
batch_node = SummarizeAllFiles()
combine_node = CombineSummaries()
batch_node >> combine_node
flow = Flow(start=batch_node)
shared = {
"files": {
"file1.txt": "Alice was beginning to get very tired of sitting by her sister...",
"file2.txt": "Some other interesting text ...",
# ...
}
}
flow.run(shared)
print("Individual Summaries:", shared["file_summaries"])
print("\nFinal Summary:\n", shared["all_files_summary"])
```
> **Performance Tip**: The example above works sequentially. You can speed up the map phase by running it in parallel. See [(Advanced) Parallel](../core_abstraction/parallel.md) for more details.
{: .note }
================================================
FILE: docs/design_pattern/multi_agent.md
================================================
---
layout: default
title: "(Advanced) Multi-Agents"
parent: "Design Pattern"
nav_order: 6
---
# (Advanced) Multi-Agents
Multiple [Agents](./flow.md) can work together by handling subtasks and communicating the progress.
Communication between agents is typically implemented using message queues in shared storage.
> Most of time, you don't need Multi-Agents. Start with a simple solution first.
{: .best-practice }
### Example Agent Communication: Message Queue
Here's a simple example showing how to implement agent communication using `asyncio.Queue`.
The agent listens for messages, processes them, and continues listening:
```python
class AgentNode(AsyncNode):
async def prep_async(self, _):
message_queue = self.params["messages"]
message = await message_queue.get()
print(f"Agent received: {message}")
return message
# Create node and flow
agent = AgentNode()
agent >> agent # connect to self
flow = AsyncFlow(start=agent)
# Create heartbeat sender
async def send_system_messages(message_queue):
counter = 0
messages = [
"System status: all systems operational",
"Memory usage: normal",
"Network connectivity: stable",
"Processing load: optimal"
]
while True:
message = f"{messages[counter % len(messages)]} | timestamp_{counter}"
await message_queue.put(message)
counter += 1
await asyncio.sleep(1)
async def main():
message_queue = asyncio.Queue()
shared = {}
flow.set_params({"messages": message_queue})
# Run both coroutines
await asyncio.gather(
flow.run_async(shared),
send_system_messages(message_queue)
)
asyncio.run(main())
```
The output:
```
Agent received: System status: all systems operational | timestamp_0
Agent received: Memory usage: normal | timestamp_1
Agent received: Network connectivity: stable | timestamp_2
Agent received: Processing load: optimal | timestamp_3
```
### Interactive Multi-Agent Example: Taboo Game
Here's a more complex example where two agents play the word-guessing game Taboo.
One agent provides hints while avoiding forbidden words, and another agent tries to guess the target word:
```python
class AsyncHinter(AsyncNode):
async def prep_async(self, shared):
guess = await shared["hinter_queue"].get()
if guess == "GAME_OVER":
return None
return shared["target_word"], shared["forbidden_words"], shared.get("past_guesses", [])
async def exec_async(self, inputs):
if inputs is None:
return None
target, forbidden, past_guesses = inputs
prompt = f"Generate hint for '{target}'\nForbidden words: {forbidden}"
if past_guesses:
prompt += f"\nPrevious wrong guesses: {past_guesses}\nMake hint more specific."
prompt += "\nUse at most 5 words."
hint = call_llm(prompt)
print(f"\nHinter: Here's your hint - {hint}")
return hint
async def post_async(self, shared, prep_res, exec_res):
if exec_res is None:
return "end"
await shared["guesser_queue"].put(exec_res)
return "continue"
class AsyncGuesser(AsyncNode):
async def prep_async(self, shared):
hint = await shared["guesser_queue"].get()
return hint, shared.get("past_guesses", [])
async def exec_async(self, inputs):
hint, past_guesses = inputs
prompt = f"Given hint: {hint}, past wrong guesses: {past_guesses}, make a new guess. Directly reply a single word:"
guess = call_llm(prompt)
print(f"Guesser: I guess it's - {guess}")
return guess
async def post_async(self, shared, prep_res, exec_res):
if exec_res.lower() == shared["target_word"].lower():
print("Game Over - Correct guess!")
await shared["hinter_queue"].put("GAME_OVER")
return "end"
if "past_guesses" not in shared:
shared["past_guesses"] = []
shared["past_guesses"].append(exec_res)
await shared["hinter_queue"].put(exec_res)
return "continue"
async def main():
# Set up game
shared = {
"target_word": "nostalgia",
"forbidden_words": ["memory", "past", "remember", "feeling", "longing"],
"hinter_queue": asyncio.Queue(),
"guesser_queue": asyncio.Queue()
}
print("Game starting!")
print(f"Target word: {shared['target_word']}")
print(f"Forbidden words: {shared['forbidden_words']}")
# Initialize by sending empty guess to hinter
await shared["hinter_queue"].put("")
# Create nodes and flows
hinter = AsyncHinter()
guesser = AsyncGuesser()
# Set up flows
hinter_flow = AsyncFlow(start=hinter)
guesser_flow = AsyncFlow(start=guesser)
# Connect nodes to themselves
hinter - "continue" >> hinter
guesser - "continue" >> guesser
# Run both agents concurrently
await asyncio.gather(
hinter_flow.run_async(shared),
guesser_flow.run_async(shared)
)
asyncio.run(main())
```
The Output:
```
Game starting!
Target word: nostalgia
Forbidden words: ['memory', 'past', 'remember', 'feeling', 'longing']
Hinter: Here's your hint - Thinking of childhood summer days
Guesser: I guess it's - popsicle
Hinter: Here's your hint - When childhood cartoons make you emotional
Guesser: I guess it's - nostalgic
Hinter: Here's your hint - When old songs move you
Guesser: I guess it's - memories
Hinter: Here's your hint - That warm emotion about childhood
Guesser: I guess it's - nostalgia
Game Over - Correct guess!
```
================================================
FILE: docs/design_pattern/rag.md
================================================
---
layout: default
title: "RAG"
parent: "Design Pattern"
nav_order: 3
---
# RAG (Retrieval Augmented Generation)
For certain LLM tasks like answering questions, providing relevant context is essential. One common architecture is a **two-stage** RAG pipeline:
1. **Offline stage**: Preprocess and index documents ("building the index").
2. **Online stage**: Given a question, generate answers by retrieving the most relevant context.
---
## Stage 1: Offline Indexing
We create three Nodes:
1. `ChunkDocs` – [chunks](../utility_function/chunking.md) raw text.
2. `EmbedDocs` – [embeds](../utility_function/embedding.md) each chunk.
3. `StoreIndex` – stores embeddings into a [vector database](../utility_function/vector.md).
```python
class ChunkDocs(BatchNode):
def prep(self, shared):
# A list of file paths in shared["files"]. We process each file.
return shared["files"]
def exec(self, filepath):
# read file content. In real usage, do error handling.
with open(filepath, "r", encoding="utf-8") as f:
text = f.read()
# chunk by 100 chars each
chunks = []
size = 100
for i in range(0, len(text), size):
chunks.append(text[i : i + size])
return chunks
def post(self, shared, prep_res, exec_res_list):
# exec_res_list is a list of chunk-lists, one per file.
# flatten them all into a single list of chunks.
all_chunks = []
for chunk_list in exec_res_list:
all_chunks.extend(chunk_list)
shared["all_chunks"] = all_chunks
class EmbedDocs(BatchNode):
def prep(self, shared):
return shared["all_chunks"]
def exec(self, chunk):
return get_embedding(chunk)
def post(self, shared, prep_res, exec_res_list):
# Store the list of embeddings.
shared["all_embeds"] = exec_res_list
print(f"Total embeddings: {len(exec_res_list)}")
class StoreIndex(Node):
def prep(self, shared):
# We'll read all embeds from shared.
return shared["all_embeds"]
def exec(self, all_embeds):
# Create a vector index (faiss or other DB in real usage).
index = create_index(all_embeds)
return index
def post(self, shared, prep_res, index):
shared["index"] = index
# Wire them in sequence
chunk_node = ChunkDocs()
embed_node = EmbedDocs()
store_node = StoreIndex()
chunk_node >> embed_node >> store_node
OfflineFlow = Flow(start=chunk_node)
```
Usage example:
```python
shared = {
"files": ["doc1.txt", "doc2.txt"], # any text files
}
OfflineFlow.run(shared)
```
---
## Stage 2: Online Query & Answer
We have 3 nodes:
1. `EmbedQuery` – embeds the user’s question.
2. `RetrieveDocs` – retrieves top chunk from the index.
3. `GenerateAnswer` – calls the LLM with the question + chunk to produce the final answer.
```python
class EmbedQuery(Node):
def prep(self, shared):
return shared["question"]
def exec(self, question):
return get_embedding(question)
def post(self, shared, prep_res, q_emb):
shared["q_emb"] = q_emb
class RetrieveDocs(Node):
def prep(self, shared):
# We'll need the query embedding, plus the offline index/chunks
return shared["q_emb"], shared["index"], shared["all_chunks"]
def exec(self, inputs):
q_emb, index, chunks = inputs
I, D = search_index(index, q_emb, top_k=1)
best_id = I[0][0]
relevant_chunk = chunks[best_id]
return relevant_chunk
def post(self, shared, prep_res, relevant_chunk):
shared["retrieved_chunk"] = relevant_chunk
print("Retrieved chunk:", relevant_chunk[:60], "...")
class GenerateAnswer(Node):
def prep(self, shared):
return shared["question"], shared["retrieved_chunk"]
def exec(self, inputs):
question, chunk = inputs
prompt = f"Question: {question}\nContext: {chunk}\nAnswer:"
return call_llm(prompt)
def post(self, shared, prep_res, answer):
shared["answer"] = answer
print("Answer:", answer)
embed_qnode = EmbedQuery()
retrieve_node = RetrieveDocs()
generate_node = GenerateAnswer()
embed_qnode >> retrieve_node >> generate_node
OnlineFlow = Flow(start=embed_qnode)
```
Usage example:
```python
# Suppose we already ran OfflineFlow and have:
# shared["all_chunks"], shared["index"], etc.
shared["question"] = "Why do people like cats?"
OnlineFlow.run(shared)
# final answer in shared["answer"]
```
================================================
FILE: docs/design_pattern/structure.md
================================================
---
layout: default
title: "Structured Output"
parent: "Design Pattern"
nav_order: 5
---
# Structured Output
In many use cases, you may want the LLM to output a specific structure, such as a list or a dictionary with predefined keys.
There are several approaches to achieve a structured output:
- **Prompting** the LLM to strictly return a defined structure.
- Using LLMs that natively support **schema enforcement**.
- **Post-processing** the LLM's response to extract structured content.
In practice, **Prompting** is simple and reliable for modern LLMs.
### Example Use Cases
- Extracting Key Information
```yaml
product:
name: Widget Pro
price: 199.99
description: |
A high-quality widget designed for professionals.
Recommended for advanced users.
```
- Summarizing Documents into Bullet Points
```yaml
summary:
- This product is easy to use.
- It is cost-effective.
- Suitable for all skill levels.
```
- Generating Configuration Files
```yaml
server:
host: 127.0.0.1
port: 8080
ssl: true
```
## Prompt Engineering
When prompting the LLM to produce **structured** output:
1. **Wrap** the structure in code fences (e.g., `yaml`).
2. **Validate** that all required fields exist (and let `Node` handles retry).
### Example Text Summarization
```python
class SummarizeNode(Node):
def exec(self, prep_res):
# Suppose `prep_res` is the text to summarize.
prompt = f"""
Please summarize the following text as YAML, with exactly 3 bullet points
{prep_res}
Now, output:
```yaml
summary:
- bullet 1
- bullet 2
- bullet 3
```"""
response = call_llm(prompt)
yaml_str = response.split("```yaml")[1].split("```")[0].strip()
import yaml
structured_result = yaml.safe_load(yaml_str)
assert "summary" in structured_result
assert isinstance(structured_result["summary"], list)
return structured_result
```
> Besides using `assert` statements, another popular way to validate schemas is [Pydantic](https://github.com/pydantic/pydantic)
{: .note }
### Why YAML instead of JSON?
Current LLMs struggle with escaping. YAML is easier with strings since they don't always need quotes.
**In JSON**
```json
{
"dialogue": "Alice said: \"Hello Bob.\\nHow are you?\\nI am good.\""
}
```
- Every double quote inside the string must be escaped with `\"`.
- Each newline in the dialogue must be represented as `\n`.
**In YAML**
```yaml
dialogue: |
Alice said: "Hello Bob.
How are you?
I am good."
```
- No need to escape interior quotes—just place the entire text under a block literal (`|`).
- Newlines are naturally preserved without needing `\n`.
================================================
FILE: docs/design_pattern/workflow.md
================================================
---
layout: default
title: "Workflow"
parent: "Design Pattern"
nav_order: 2
---
# Workflow
Many real-world tasks are too complex for one LLM call. The solution is to **Task Decomposition**: decompose them into a [chain](../core_abstraction/flow.md) of multiple Nodes.
> - You don't want to make each task **too coarse**, because it may be *too complex for one LLM call*.
> - You don't want to make each task **too granular**, because then *the LLM call doesn't have enough context* and results are *not consistent across nodes*.
>
> You usually need multiple *iterations* to find the *sweet spot*. If the task has too many *edge cases*, consider using [Agents](./agent.md).
{: .best-practice }
### Example: Article Writing
```python
class GenerateOutline(Node):
def prep(self, shared): return shared["topic"]
def exec(self, topic): return call_llm(f"Create a detailed outline for an article about {topic}")
def post(self, shared, prep_res, exec_res): shared["outline"] = exec_res
class WriteSection(Node):
def prep(self, shared): return shared["outline"]
def exec(self, outline): return call_llm(f"Write content based on this outline: {outline}")
def post(self, shared, prep_res, exec_res): shared["draft"] = exec_res
class ReviewAndRefine(Node):
def prep(self, shared): return shared["draft"]
def exec(self, draft): return call_llm(f"Review and improve this draft: {draft}")
def post(self, shared, prep_res, exec_res): shared["final_article"] = exec_res
# Connect nodes
outline = GenerateOutline()
write = WriteSection()
review = ReviewAndRefine()
outline >> write >> review
# Create and run flow
writing_flow = Flow(start=outline)
shared = {"topic": "AI Safety"}
writing_flow.run(shared)
```
For *dynamic cases*, consider using [Agents](./agent.md).
================================================
FILE: docs/guide.md
================================================
---
layout: default
title: "Agentic Coding"
---
# Agentic Coding: Humans Design, Agents code!
> If you are an AI agent involved in building LLM Systems, read this guide **VERY, VERY** carefully! This is the most important chapter in the entire document. Throughout development, you should always (1) start with a small and simple solution, (2) design at a high level (`docs/design.md`) before implementation, and (3) frequently ask humans for feedback and clarification.
{: .warning }
## Agentic Coding Steps
Agentic Coding should be a collaboration between Human System Design and Agent Implementation:
| Steps | Human | AI | Comment |
|:-----------------------|:----------:|:---------:|:------------------------------------------------------------------------|
| 1. Requirements | ★★★ High | ★☆☆ Low | Humans understand the requirements and context. |
| 2. Flow | ★★☆ Medium | ★★☆ Medium | Humans specify the high-level design, and the AI fills in the details. |
| 3. Utilities | ★★☆ Medium | ★★☆ Medium | Humans provide available external APIs and integrations, and the AI helps with implementation. |
| 4. Data | ★☆☆ Low | ★★★ High | AI designs the data schema, and humans verify. |
| 5. Node | ★☆☆ Low | ★★★ High | The AI helps design the node based on the flow. |
| 6. Implementation | ★☆☆ Low | ★★★ High | The AI implements the flow based on the design. |
| 7. Optimization | ★★☆ Medium | ★★☆ Medium | Humans evaluate the results, and the AI helps optimize. |
| 8. Reliability | ★☆☆ Low | ★★★ High | The AI writes test cases and addresses corner cases. |
1. **Requirements**: Clarify the requirements for your project, and evaluate whether an AI system is a good fit.
- Understand AI systems' strengths and limitations:
- **Good for**: Routine tasks requiring common sense (filling forms, replying to emails)
- **Good for**: Creative tasks with well-defined inputs (building slides, writing SQL)
- **Not good for**: Ambiguous problems requiring complex decision-making (business strategy, startup planning)
- **Keep It User-Centric:** Explain the "problem" from the user's perspective rather than just listing features.
- **Balance complexity vs. impact**: Aim to deliver the highest value features with minimal complexity early.
2. **Flow Design**: Outline at a high level, describe how your AI system orchestrates nodes.
- Identify applicable design patterns (e.g., [Map Reduce](./design_pattern/mapreduce.md), [Agent](./design_pattern/agent.md), [RAG](./design_pattern/rag.md)).
- For each node in the flow, start with a high-level one-line description of what it does.
- If using **Map Reduce**, specify how to map (what to split) and how to reduce (how to combine).
- If using **Agent**, specify what are the inputs (context) and what are the possible actions.
- If using **RAG**, specify what to embed, noting that there's usually both offline (indexing) and online (retrieval) workflows.
- Outline the flow and draw it in a mermaid diagram. For example:
```mermaid
flowchart LR
start[Start] --> batch[Batch]
batch --> check[Check]
check -->|OK| process
check -->|Error| fix[Fix]
fix --> check
subgraph process[Process]
step1[Step 1] --> step2[Step 2]
end
process --> endNode[End]
```
- > **If Humans can't specify the flow, AI Agents can't automate it!** Before building an LLM system, thoroughly understand the problem and potential solution by manually solving example inputs to develop intuition.
{: .best-practice }
3. **Utilities**: Based on the Flow Design, identify and implement necessary utility functions.
- Think of your AI system as the brain. It needs a body—these *external utility functions*—to interact with the real world:
- Reading inputs (e.g., retrieving Slack messages, reading emails)
- Writing outputs (e.g., generating reports, sending emails)
- Using external tools (e.g., calling LLMs, searching the web)
- **NOTE**: *LLM-based tasks* (e.g., summarizing text, analyzing sentiment) are **NOT** utility functions; rather, they are *core functions* internal in the AI system.
- For each utility function, implement it and write a simple test.
- Document their input/output, as well as why they are necessary. For example:
- `name`: `get_embedding` (`utils/get_embedding.py`)
- `input`: `str`
- `output`: a vector of 3072 floats
- `necessity`: Used by the second node to embed text
- Example utility implementation:
```python
# utils/call_llm.py
from openai import OpenAI
def call_llm(prompt):
client = OpenAI(api_key="YOUR_API_KEY_HERE")
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
if __name__ == "__main__":
prompt = "What is the meaning of life?"
print(call_llm(prompt))
```
- > **Sometimes, design Utilities before Flow:** For example, for an LLM project to automate a legacy system, the bottleneck will likely be the available interface to that system. Start by designing the hardest utilities for interfacing, and then build the flow around them.
{: .best-practice }
- > **Avoid Exception Handling in Utilities**: If a utility function is called from a Node's `exec()` method, avoid using `try...except` blocks within the utility. Let the Node's built-in retry mechanism handle failures.
{: .warning }
4. **Data Design**: Design the shared store that nodes will use to communicate.
- One core design principle for PocketFlow is to use a well-designed [shared store](./core_abstraction/communication.md)—a data contract that all nodes agree upon to retrieve and store data.
- For simple systems, use an in-memory dictionary.
- For more complex systems or when persistence is required, use a database.
- **Don't Repeat Yourself**: Use in-memory references or foreign keys.
- Example shared store design:
```python
shared = {
"user": {
"id": "user123",
"context": { # Another nested dict
"weather": {"temp": 72, "condition": "sunny"},
"location": "San Francisco"
}
},
"results": {} # Empty dict to store outputs
}
```
5. **Node Design**: Plan how each node will read and write data, and use utility functions.
- For each [Node](./core_abstraction/node.md), describe its type, how it reads and writes data, and which utility function it uses. Keep it specific but high-level without codes. For example:
- `type`: Regular (or Batch, or Async)
- `prep`: Read "text" from the shared store
- `exec`: Call the embedding utility function. **Avoid exception handling here**; let the Node's retry mechanism manage failures.
- `post`: Write "embedding" to the shared store
6. **Implementation**: Implement the initial nodes and flows based on the design.
- 🎉 If you've reached this step, humans have finished the design. Now *Agentic Coding* begins!
- **"Keep it simple, stupid!"** Avoid complex features and full-scale type checking.
- **FAIL FAST**! Leverage the built-in [Node](./core_abstraction/node.md) retry and fallback mechanisms to handle failures gracefully. This helps you quickly identify weak points in the system.
- Add logging throughout the code to facilitate debugging.
7. **Optimization**:
- **Use Intuition**: For a quick initial evaluation, human intuition is often a good start.
- **Redesign Flow (Back to Step 3)**: Consider breaking down tasks further, introducing agentic decisions, or better managing input contexts.
- If your flow design is already solid, move on to micro-optimizations:
- **Prompt Engineering**: Use clear, specific instructions with examples to reduce ambiguity.
- **In-Context Learning**: Provide robust examples for tasks that are difficult to specify with instructions alone.
- > **You'll likely iterate a lot!** Expect to repeat Steps 3–6 hundreds of times.
>
>
{: .best-practice }
8. **Reliability**
- **Node Retries**: Add checks in the node `exec` to ensure outputs meet requirements, and consider increasing `max_retries` and `wait` times.
- **Logging and Visualization**: Maintain logs of all attempts and visualize node results for easier debugging.
- **Self-Evaluation**: Add a separate node (powered by an LLM) to review outputs when results are uncertain.
## Example LLM Project File Structure
```
my_project/
├── main.py
├── nodes.py
├── flow.py
├── utils/
│ ├── __init__.py
│ ├── call_llm.py
│ └── search_web.py
├── requirements.txt
└── docs/
└── design.md
```
- **`requirements.txt`**: Lists the Python dependencies for the project.
```
PyYAML
pocketflow
```
- **`docs/design.md`**: Contains project documentation for each step above. This should be *high-level* and *no-code*.
~~~
# Design Doc: Your Project Name
> Please DON'T remove notes for AI
## Requirements
> Notes for AI: Keep it simple and clear.
> If the requirements are abstract, write concrete user stories
## Flow Design
> Notes for AI:
> 1. Consider the design patterns of agent, map-reduce, rag, and workflow. Apply them if they fit.
> 2. Present a concise, high-level description of the workflow.
### Applicable Design Pattern:
1. Map the file summary into chunks, then reduce these chunks into a final summary.
2. Agentic file finder
- *Context*: The entire summary of the file
- *Action*: Find the file
### Flow high-level Design:
1. **First Node**: This node is for ...
2. **Second Node**: This node is for ...
3. **Third Node**: This node is for ...
```mermaid
flowchart TD
firstNode[First Node] --> secondNode[Second Node]
secondNode --> thirdNode[Third Node]
```
## Utility Functions
> Notes for AI:
> 1. Understand the utility function definition thoroughly by reviewing the doc.
> 2. Include only the necessary utility functions, based on nodes in the flow.
1. **Call LLM** (`utils/call_llm.py`)
- *Input*: prompt (str)
- *Output*: response (str)
- Generally used by most nodes for LLM tasks
2. **Embedding** (`utils/get_embedding.py`)
- *Input*: str
- *Output*: a vector of 3072 floats
- Used by the second node to embed text
## Node Design
### Shared Store
> Notes for AI: Try to minimize data redundancy
The shared store structure is organized as follows:
```python
shared = {
"key": "value"
}
```
### Node Steps
> Notes for AI: Carefully decide whether to use Batch/Async Node/Flow.
1. First Node
- *Purpose*: Provide a short explanation of the node’s function
- *Type*: Decide between Regular, Batch, or Async
- *Steps*:
- *prep*: Read "key" from the shared store
- *exec*: Call the utility function
- *post*: Write "key" to the shared store
2. Second Node
...
~~~
- **`utils/`**: Contains all utility functions.
- It's recommended to dedicate one Python file to each API call, for example `call_llm.py` or `search_web.py`.
- Each file should also include a `main()` function to try that API call
```python
from google import genai
import os
def call_llm(prompt: str) -> str:
client = genai.Client(
api_key=os.getenv("GEMINI_API_KEY", ""),
)
model = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
response = client.models.generate_content(model=model, contents=[prompt])
return response.text
if __name__ == "__main__":
test_prompt = "Hello, how are you?"
# First call - should hit the API
print("Making call...")
response1 = call_llm(test_prompt, use_cache=False)
print(f"Response: {response1}")
```
- **`nodes.py`**: Contains all the node definitions.
```python
# nodes.py
from pocketflow import Node
from utils.call_llm import call_llm
class GetQuestionNode(Node):
def exec(self, _):
# Get question directly from user input
user_question = input("Enter your question: ")
return user_question
def post(self, shared, prep_res, exec_res):
# Store the user's question
shared["question"] = exec_res
return "default" # Go to the next node
class AnswerNode(Node):
def prep(self, shared):
# Read question from shared
return shared["question"]
def exec(self, question):
# Call LLM to get the answer
return call_llm(question)
def post(self, shared, prep_res, exec_res):
# Store the answer in shared
shared["answer"] = exec_res
```
- **`flow.py`**: Implements functions that create flows by importing node definitions and connecting them.
```python
# flow.py
from pocketflow import Flow
from nodes import GetQuestionNode, AnswerNode
def create_qa_flow():
"""Create and return a question-answering flow."""
# Create nodes
get_question_node = GetQuestionNode()
answer_node = AnswerNode()
# Connect nodes in sequence
get_question_node >> answer_node
# Create flow starting with input node
return Flow(start=get_question_node)
```
- **`main.py`**: Serves as the project's entry point.
```python
# main.py
from flow import create_qa_flow
# Example main function
# Please replace this with your own main function
def main():
shared = {
"question": None, # Will be populated by GetQuestionNode from user input
"answer": None # Will be populated by AnswerNode
}
# Create the flow and run it
qa_flow = create_qa_flow()
qa_flow.run(shared)
print(f"Question: {shared['question']}")
print(f"Answer: {shared['answer']}")
if __name__ == "__main__":
main()
```
================================================
FILE: docs/index.md
================================================
---
layout: default
title: "Home"
nav_order: 1
---
# Pocket Flow
A [100-line](https://github.com/the-pocket/PocketFlow/blob/main/pocketflow/__init__.py) minimalist LLM framework for *Agents, Task Decomposition, RAG, etc*.
- **Lightweight**: Just the core graph abstraction in 100 lines. ZERO dependencies, and vendor lock-in.
- **Expressive**: Everything you love from larger frameworks—([Multi-](./design_pattern/multi_agent.html))[Agents](./design_pattern/agent.html), [Workflow](./design_pattern/workflow.html), [RAG](./design_pattern/rag.html), and more.
- **Agentic-Coding**: Intuitive enough for AI agents to help humans build complex LLM applications.
## Core Abstraction
We model the LLM workflow as a **Graph + Shared Store**:
- [Node](./core_abstraction/node.md) handles simple (LLM) tasks.
- [Flow](./core_abstraction/flow.md) connects nodes through **Actions** (labeled edges).
- [Shared Store](./core_abstraction/communication.md) enables communication between nodes within flows.
- [Batch](./core_abstraction/batch.md) nodes/flows allow for data-intensive tasks.
- [Async](./core_abstraction/async.md) nodes/flows allow waiting for asynchronous tasks.
- [(Advanced) Parallel](./core_abstraction/parallel.md) nodes/flows handle I/O-bound tasks.
## Design Pattern
From there, it’s easy to implement popular design patterns:
- [Agent](./design_pattern/agent.md) autonomously makes decisions.
- [Workflow](./design_pattern/workflow.md) chains multiple tasks into pipelines.
- [RAG](./design_pattern/rag.md) integrates data retrieval with generation.
- [Map Reduce](./design_pattern/mapreduce.md) splits data tasks into Map and Reduce steps.
- [Structured Output](./design_pattern/structure.md) formats outputs consistently.
- [(Advanced) Multi-Agents](./design_pattern/multi_agent.md) coordinate multiple agents.
## Utility Function
We **do not** provide built-in utilities. Instead, we offer *examples*—please *implement your own*:
- [LLM Wrapper](./utility_function/llm.md)
- [Viz and Debug](./utility_function/viz.md)
- [Web Search](./utility_function/websearch.md)
- [Chunking](./utility_function/chunking.md)
- [Embedding](./utility_function/embedding.md)
- [Vector Databases](./utility_function/vector.md)
- [Text-to-Speech](./utility_function/text_to_speech.md)
**Why not built-in?**: I believe it's a *bad practice* for vendor-specific APIs in a general framework:
- *API Volatility*: Frequent changes lead to heavy maintenance for hardcoded APIs.
- *Flexibility*: You may want to switch vendors, use fine-tuned models, or run them locally.
- *Optimizations*: Prompt caching, batching, and streaming are easier without vendor lock-in.
## Ready to build your Apps?
Check out [Agentic Coding Guidance](./guide.md), the fastest way to develop LLM projects with Pocket Flow!
================================================
FILE: docs/utility_function/chunking.md
================================================
---
layout: default
title: "Text Chunking"
parent: "Utility Function"
nav_order: 4
---
# Text Chunking
We recommend some implementations of commonly used text chunking approaches.
> Text Chunking is more a micro optimization, compared to the Flow Design.
>
> It's recommended to start with the Naive Chunking and optimize later.
{: .best-practice }
---
## Example Python Code Samples
### 1. Naive (Fixed-Size) Chunking
Splits text by a fixed number of words, ignoring sentence or semantic boundaries.
```python
def fixed_size_chunk(text, chunk_size=100):
chunks = []
for i in range(0, len(text), chunk_size):
chunks.append(text[i : i + chunk_size])
return chunks
```
However, sentences are often cut awkwardly, losing coherence.
### 2. Sentence-Based Chunking
```python
import nltk
def sentence_based_chunk(text, max_sentences=2):
sentences = nltk.sent_tokenize(text)
chunks = []
for i in range(0, len(sentences), max_sentences):
chunks.append(" ".join(sentences[i : i + max_sentences]))
return chunks
```
However, might not handle very long sentences or paragraphs well.
### 3. Other Chunking
- **Paragraph-Based**: Split text by paragraphs (e.g., newlines). Large paragraphs can create big chunks.
- **Semantic**: Use embeddings or topic modeling to chunk by semantic boundaries.
- **Agentic**: Use an LLM to decide chunk boundaries based on context or meaning.
================================================
FILE: docs/utility_function/embedding.md
================================================
---
layout: default
title: "Embedding"
parent: "Utility Function"
nav_order: 5
---
# Embedding
Below you will find an overview table of various text embedding APIs, along with example Python code.
> Embedding is more a micro optimization, compared to the Flow Design.
>
> It's recommended to start with the most convenient one and optimize later.
{: .best-practice }
| **API** | **Free Tier** | **Pricing Model** | **Docs** |
| --- | --- | --- | --- |
| **OpenAI** | ~$5 credit | ~$0.0001/1K tokens | [OpenAI Embeddings](https://platform.openai.com/docs/api-reference/embeddings) |
| **Azure OpenAI** | $200 credit | Same as OpenAI (~$0.0001/1K tokens) | [Azure OpenAI Embeddings](https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?tabs=portal) |
| **Google Vertex AI** | $300 credit | ~$0.025 / million chars | [Vertex AI Embeddings](https://cloud.google.com/vertex-ai/docs/generative-ai/embeddings/get-text-embeddings) |
| **AWS Bedrock** | No free tier, but AWS credits may apply | ~$0.00002/1K tokens (Titan V2) | [Amazon Bedrock](https://docs.aws.amazon.com/bedrock/) |
| **Cohere** | Limited free tier | ~$0.0001/1K tokens | [Cohere Embeddings](https://docs.cohere.com/docs/cohere-embed) |
| **Hugging Face** | ~$0.10 free compute monthly | Pay per second of compute | [HF Inference API](https://huggingface.co/docs/api-inference) |
| **Jina** | 1M tokens free | Pay per token after | [Jina Embeddings](https://jina.ai/embeddings/) |
## Example Python Code
### 1. OpenAI
```python
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY")
response = client.embeddings.create(
model="text-embedding-ada-002",
input=text
)
# Extract the embedding vector from the response
embedding = response.data[0].embedding
embedding = np.array(embedding, dtype=np.float32)
print(embedding)
```
### 2. Azure OpenAI
```python
import openai
openai.api_type = "azure"
openai.api_base = "https://YOUR_RESOURCE_NAME.openai.azure.com"
openai.api_version = "2023-03-15-preview"
openai.api_key = "YOUR_AZURE_API_KEY"
resp = openai.Embedding.create(engine="ada-embedding", input="Hello world")
vec = resp["data"][0]["embedding"]
print(vec)
```
### 3. Google Vertex AI
```python
from vertexai.preview.language_models import TextEmbeddingModel
import vertexai
vertexai.init(project="YOUR_GCP_PROJECT_ID", location="us-central1")
model = TextEmbeddingModel.from_pretrained("textembedding-gecko@001")
emb = model.get_embeddings(["Hello world"])
print(emb[0])
```
### 4. AWS Bedrock
```python
import boto3, json
client = boto3.client("bedrock-runtime", region_name="us-east-1")
body = {"inputText": "Hello world"}
resp = client.invoke_model(modelId="amazon.titan-embed-text-v2:0", contentType="application/json", body=json.dumps(body))
resp_body = json.loads(resp["body"].read())
vec = resp_body["embedding"]
print(vec)
```
### 5. Cohere
```python
import cohere
co = cohere.Client("YOUR_API_KEY")
resp = co.embed(texts=["Hello world"])
vec = resp.embeddings[0]
print(vec)
```
### 6. Hugging Face
```python
import requests
API_URL = "https://api-inference.huggingface.co/models/sentence-transformers/all-MiniLM-L6-v2"
HEADERS = {"Authorization": "Bearer YOUR_HF_TOKEN"}
res = requests.post(API_URL, headers=HEADERS, json={"inputs": "Hello world"})
vec = res.json()[0]
print(vec)
```
### 7. Jina
```python
import requests
url = "https://api.jina.ai/v2/embed"
headers = {"Authorization": "Bearer YOUR_JINA_TOKEN"}
payload = {"data": ["Hello world"], "model": "jina-embeddings-v3"}
res = requests.post(url, headers=headers, json=payload)
vec = res.json()["data"][0]["embedding"]
print(vec)
```
================================================
FILE: docs/utility_function/index.md
================================================
---
layout: default
title: "Utility Function"
nav_order: 4
has_children: true
---
================================================
FILE: docs/utility_function/llm.md
================================================
---
layout: default
title: "LLM Wrapper"
parent: "Utility Function"
nav_order: 1
---
# LLM Wrappers
Check out libraries like [litellm](https://github.com/BerriAI/litellm).
Here, we provide some minimal example implementations:
1. OpenAI
```python
def call_llm(prompt):
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY_HERE")
r = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
# Example usage
call_llm("How are you?")
```
> Store the API key in an environment variable like OPENAI_API_KEY for security.
{: .best-practice }
2. Claude (Anthropic)
```python
def call_llm(prompt):
from anthropic import Anthropic
client = Anthropic(api_key="YOUR_API_KEY_HERE")
r = client.messages.create(
model="claude-sonnet-4-0",
messages=[
{"role": "user", "content": prompt}
]
)
return r.content[0].text
```
3. Google (Generative AI Studio / PaLM API)
```python
def call_llm(prompt):
from google import genai
client = genai.Client(api_key='GEMINI_API_KEY')
response = client.models.generate_content(
model='gemini-2.5-pro',
contents=prompt
)
return response.text
```
4. Azure (Azure OpenAI)
```python
def call_llm(prompt):
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint="https://.openai.azure.com/",
api_key="YOUR_API_KEY_HERE",
api_version="2023-05-15"
)
r = client.chat.completions.create(
model="",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
```
5. Ollama (Local LLM)
```python
def call_llm(prompt):
from ollama import chat
response = chat(
model="llama2",
messages=[{"role": "user", "content": prompt}]
)
return response.message.content
```
6. DeepSeek
```python
def call_llm(prompt):
from openai import OpenAI
client = OpenAI(api_key="YOUR_DEEPSEEK_API_KEY", base_url="https://api.deepseek.com")
r = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": prompt}]
)
return r.choices[0].message.content
```
## Improvements
Feel free to enhance your `call_llm` function as needed. Here are examples:
- Handle chat history:
```python
def call_llm(messages):
from openai import OpenAI
client = OpenAI(api_key="YOUR_API_KEY_HERE")
r = client.chat.completions.create(
model="gpt-4o",
messages=messages
)
return r.choices[0].message.content
```
- Add in-memory caching
```python
from functools import lru_cache
@lru_cache(maxsize=1000)
def call_llm(prompt):
# Your implementation here
pass
```
> ⚠️ Caching conflicts with Node retries, as retries yield the same result.
>
> To address this, you could use cached results only if not retried.
{: .warning }
```python
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_call(prompt):
pass
def call_llm(prompt, use_cache):
if use_cache:
return cached_call(prompt)
# Call the underlying function directly
return cached_call.__wrapped__(prompt)
class SummarizeNode(Node):
def exec(self, text):
return call_llm(f"Summarize: {text}", self.cur_retry==0)
```
- Enable logging:
```python
def call_llm(prompt):
import logging
logging.info(f"Prompt: {prompt}")
response = ... # Your implementation here
logging.info(f"Response: {response}")
return response
```
================================================
FILE: docs/utility_function/text_to_speech.md
================================================
---
layout: default
title: "Text-to-Speech"
parent: "Utility Function"
nav_order: 7
---
# Text-to-Speech
| **Service** | **Free Tier** | **Pricing Model** | **Docs** |
|----------------------|-----------------------|--------------------------------------------------------------|---------------------------------------------------------------------|
| **Amazon Polly** | 5M std + 1M neural | ~$4 /M (std), ~$16 /M (neural) after free tier | [Polly Docs](https://aws.amazon.com/polly/) |
| **Google Cloud TTS** | 4M std + 1M WaveNet | ~$4 /M (std), ~$16 /M (WaveNet) pay-as-you-go | [Cloud TTS Docs](https://cloud.google.com/text-to-speech) |
| **Azure TTS** | 500K neural ongoing | ~$15 /M (neural), discount at higher volumes | [Azure TTS Docs](https://azure.microsoft.com/products/cognitive-services/text-to-speech/) |
| **IBM Watson TTS** | 10K chars Lite plan | ~$0.02 /1K (i.e. ~$20 /M). Enterprise options available | [IBM Watson Docs](https://www.ibm.com/cloud/watson-text-to-speech) |
| **ElevenLabs** | 10K chars monthly | From ~$5/mo (30K chars) up to $330/mo (2M chars). Enterprise | [ElevenLabs Docs](https://elevenlabs.io) |
## Example Python Code
### Amazon Polly
```python
import boto3
polly = boto3.client("polly", region_name="us-east-1",
aws_access_key_id="YOUR_AWS_ACCESS_KEY_ID",
aws_secret_access_key="YOUR_AWS_SECRET_ACCESS_KEY")
resp = polly.synthesize_speech(
Text="Hello from Polly!",
OutputFormat="mp3",
VoiceId="Joanna"
)
with open("polly.mp3", "wb") as f:
f.write(resp["AudioStream"].read())
```
### Google Cloud TTS
```python
from google.cloud import texttospeech
client = texttospeech.TextToSpeechClient()
input_text = texttospeech.SynthesisInput(text="Hello from Google Cloud TTS!")
voice = texttospeech.VoiceSelectionParams(language_code="en-US")
audio_cfg = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.MP3)
resp = client.synthesize_speech(input=input_text, voice=voice, audio_config=audio_cfg)
with open("gcloud_tts.mp3", "wb") as f:
f.write(resp.audio_content)
```
### Azure TTS
```python
import azure.cognitiveservices.speech as speechsdk
speech_config = speechsdk.SpeechConfig(
subscription="AZURE_KEY", region="AZURE_REGION")
audio_cfg = speechsdk.audio.AudioConfig(filename="azure_tts.wav")
synthesizer = speechsdk.SpeechSynthesizer(
speech_config=speech_config,
audio_config=audio_cfg
)
synthesizer.speak_text_async("Hello from Azure TTS!").get()
```
### IBM Watson TTS
```python
from ibm_watson import TextToSpeechV1
from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
auth = IAMAuthenticator("IBM_API_KEY")
service = TextToSpeechV1(authenticator=auth)
service.set_service_url("IBM_SERVICE_URL")
resp = service.synthesize(
"Hello from IBM Watson!",
voice="en-US_AllisonV3Voice",
accept="audio/mp3"
).get_result()
with open("ibm_tts.mp3", "wb") as f:
f.write(resp.content)
```
### ElevenLabs
```python
import requests
api_key = "ELEVENLABS_KEY"
voice_id = "ELEVENLABS_VOICE"
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
headers = {"xi-api-key": api_key, "Content-Type": "application/json"}
json_data = {
"text": "Hello from ElevenLabs!",
"voice_settings": {"stability": 0.75, "similarity_boost": 0.75}
}
resp = requests.post(url, headers=headers, json=json_data)
with open("elevenlabs.mp3", "wb") as f:
f.write(resp.content)
```
================================================
FILE: docs/utility_function/vector.md
================================================
---
layout: default
title: "Vector Databases"
parent: "Utility Function"
nav_order: 6
---
# Vector Databases
Below is a table of the popular vector search solutions:
| **Tool** | **Free Tier** | **Pricing Model** | **Docs** |
| --- | --- | --- | --- |
| **FAISS** | N/A, self-host | Open-source | [Faiss.ai](https://faiss.ai) |
| **Pinecone** | 2GB free | From $25/mo | [pinecone.io](https://pinecone.io) |
| **Qdrant** | 1GB free cloud | Pay-as-you-go | [qdrant.tech](https://qdrant.tech) |
| **Weaviate** | 14-day sandbox | From $25/mo | [weaviate.io](https://weaviate.io) |
| **Milvus** | 5GB free cloud | PAYG or $99/mo dedicated | [milvus.io](https://milvus.io) |
| **Chroma** | N/A, self-host | Free (Apache 2.0) | [trychroma.com](https://trychroma.com) |
| **Redis** | 30MB free | From $5/mo | [redis.io](https://redis.io) |
---
## Example Python Code
Below are basic usage snippets for each tool.
### FAISS
```python
import faiss
import numpy as np
# Dimensionality of embeddings
d = 128
# Create a flat L2 index
index = faiss.IndexFlatL2(d)
# Random vectors
data = np.random.random((1000, d)).astype('float32')
index.add(data)
# Query
query = np.random.random((1, d)).astype('float32')
D, I = index.search(query, k=5)
print("Distances:", D)
print("Neighbors:", I)
```
### Pinecone
```python
import pinecone
pinecone.init(api_key="YOUR_API_KEY", environment="YOUR_ENV")
index_name = "my-index"
# Create the index if it doesn't exist
if index_name not in pinecone.list_indexes():
pinecone.create_index(name=index_name, dimension=128)
# Connect
index = pinecone.Index(index_name)
# Upsert
vectors = [
("id1", [0.1]*128),
("id2", [0.2]*128)
]
index.upsert(vectors)
# Query
response = index.query([[0.15]*128], top_k=3)
print(response)
```
### Qdrant
```python
import qdrant_client
from qdrant_client.models import Distance, VectorParams, PointStruct
client = qdrant_client.QdrantClient(
url="https://YOUR-QDRANT-CLOUD-ENDPOINT",
api_key="YOUR_API_KEY"
)
collection = "my_collection"
client.recreate_collection(
collection_name=collection,
vectors_config=VectorParams(size=128, distance=Distance.COSINE)
)
points = [
PointStruct(id=1, vector=[0.1]*128, payload={"type": "doc1"}),
PointStruct(id=2, vector=[0.2]*128, payload={"type": "doc2"}),
]
client.upsert(collection_name=collection, points=points)
results = client.search(
collection_name=collection,
query_vector=[0.15]*128,
limit=2
)
print(results)
```
### Weaviate
```python
import weaviate
client = weaviate.Client("https://YOUR-WEAVIATE-CLOUD-ENDPOINT")
schema = {
"classes": [
{
"class": "Article",
"vectorizer": "none"
}
]
}
client.schema.create(schema)
obj = {
"title": "Hello World",
"content": "Weaviate vector search"
}
client.data_object.create(obj, "Article", vector=[0.1]*128)
resp = (
client.query
.get("Article", ["title", "content"])
.with_near_vector({"vector": [0.15]*128})
.with_limit(3)
.do()
)
print(resp)
```
### Milvus
```python
from pymilvus import connections, FieldSchema, CollectionSchema, DataType, Collection
import numpy as np
connections.connect(alias="default", host="localhost", port="19530")
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=128)
]
schema = CollectionSchema(fields)
collection = Collection("MyCollection", schema)
emb = np.random.rand(10, 128).astype('float32')
ids = list(range(10))
collection.insert([ids, emb])
index_params = {
"index_type": "IVF_FLAT",
"params": {"nlist": 128},
"metric_type": "L2"
}
collection.create_index("embedding", index_params)
collection.load()
query_emb = np.random.rand(1, 128).astype('float32')
results = collection.search(query_emb, "embedding", param={"nprobe": 10}, limit=3)
print(results)
```
### Chroma
```python
import chromadb
from chromadb.config import Settings
client = chromadb.Client(Settings(
chroma_db_impl="duckdb+parquet",
persist_directory="./chroma_data"
))
coll = client.create_collection("my_collection")
vectors = [[0.1, 0.2, 0.3], [0.2, 0.2, 0.2]]
metas = [{"doc": "text1"}, {"doc": "text2"}]
ids = ["id1", "id2"]
coll.add(embeddings=vectors, metadatas=metas, ids=ids)
res = coll.query(query_embeddings=[[0.15, 0.25, 0.3]], n_results=2)
print(res)
```
### Redis
```python
import redis
import struct
r = redis.Redis(host="localhost", port=6379)
# Create index
r.execute_command(
"FT.CREATE", "my_idx", "ON", "HASH",
"SCHEMA", "embedding", "VECTOR", "FLAT", "6",
"TYPE", "FLOAT32", "DIM", "128",
"DISTANCE_METRIC", "L2"
)
# Insert
vec = struct.pack('128f', *[0.1]*128)
r.hset("doc1", mapping={"embedding": vec})
# Search
qvec = struct.pack('128f', *[0.15]*128)
q = "*=>[KNN 3 @embedding $BLOB AS dist]"
res = r.ft("my_idx").search(q, query_params={"BLOB": qvec})
print(res.docs)
```
================================================
FILE: docs/utility_function/viz.md
================================================
---
layout: default
title: "Viz and Debug"
parent: "Utility Function"
nav_order: 2
---
# Visualization and Debugging
Similar to LLM wrappers, we **don't** provide built-in visualization and debugging. Here, we recommend some *minimal* (and incomplete) implementations These examples can serve as a starting point for your own tooling.
## 1. Visualization with Mermaid
This code recursively traverses the nested graph, assigns unique IDs to each node, and treats Flow nodes as subgraphs to generate Mermaid syntax for a hierarchical visualization.
{% raw %}
```python
def build_mermaid(start):
ids, visited, lines = {}, set(), ["graph LR"]
ctr = 1
def get_id(n):
nonlocal ctr
return ids[n] if n in ids else (ids.setdefault(n, f"N{ctr}"), (ctr := ctr + 1))[0]
def link(a, b):
lines.append(f" {a} --> {b}")
def walk(node, parent=None):
if node in visited:
return parent and link(parent, get_id(node))
visited.add(node)
if isinstance(node, Flow):
node.start_node and parent and link(parent, get_id(node.start_node))
lines.append(f"\n subgraph sub_flow_{get_id(node)}[{type(node).__name__}]")
node.start_node and walk(node.start_node)
for nxt in node.successors.values():
node.start_node and walk(nxt, get_id(node.start_node)) or (parent and link(parent, get_id(nxt))) or walk(nxt)
lines.append(" end\n")
else:
lines.append(f" {(nid := get_id(node))}['{type(node).__name__}']")
parent and link(parent, nid)
[walk(nxt, nid) for nxt in node.successors.values()]
walk(start)
return "\n".join(lines)
```
{% endraw %}
For example, suppose we have a complex Flow for data science:
```python
class DataPrepBatchNode(BatchNode):
def prep(self,shared): return []
class ValidateDataNode(Node): pass
class FeatureExtractionNode(Node): pass
class TrainModelNode(Node): pass
class EvaluateModelNode(Node): pass
class ModelFlow(Flow): pass
class DataScienceFlow(Flow):pass
feature_node = FeatureExtractionNode()
train_node = TrainModelNode()
evaluate_node = EvaluateModelNode()
feature_node >> train_node >> evaluate_node
model_flow = ModelFlow(start=feature_node)
data_prep_node = DataPrepBatchNode()
validate_node = ValidateDataNode()
data_prep_node >> validate_node >> model_flow
data_science_flow = DataScienceFlow(start=data_prep_node)
result = build_mermaid(start=data_science_flow)
```
The code generates a Mermaid diagram:
```mermaid
graph LR
subgraph sub_flow_N1[DataScienceFlow]
N2['DataPrepBatchNode']
N3['ValidateDataNode']
N2 --> N3
N3 --> N4
subgraph sub_flow_N5[ModelFlow]
N4['FeatureExtractionNode']
N6['TrainModelNode']
N4 --> N6
N7['EvaluateModelNode']
N6 --> N7
end
end
```
For visualization based on d3.js, check out [the cookbook](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-visualization).
## 2. Call Stack Debugging
It would be useful to print the Node call stacks for debugging. This can be achieved by inspecting the runtime call stack:
```python
import inspect
def get_node_call_stack():
stack = inspect.stack()
node_names = []
seen_ids = set()
for frame_info in stack[1:]:
local_vars = frame_info.frame.f_locals
if 'self' in local_vars:
caller_self = local_vars['self']
if isinstance(caller_self, BaseNode) and id(caller_self) not in seen_ids:
seen_ids.add(id(caller_self))
node_names.append(type(caller_self).__name__)
return node_names
```
For example, suppose we have a complex Flow for data science:
```python
class DataPrepBatchNode(BatchNode):
def prep(self, shared): return []
class ValidateDataNode(Node): pass
class FeatureExtractionNode(Node): pass
class TrainModelNode(Node): pass
class EvaluateModelNode(Node):
def prep(self, shared):
stack = get_node_call_stack()
print("Call stack:", stack)
class ModelFlow(Flow): pass
class DataScienceFlow(Flow):pass
feature_node = FeatureExtractionNode()
train_node = TrainModelNode()
evaluate_node = EvaluateModelNode()
feature_node >> train_node >> evaluate_node
model_flow = ModelFlow(start=feature_node)
data_prep_node = DataPrepBatchNode()
validate_node = ValidateDataNode()
data_prep_node >> validate_node >> model_flow
data_science_flow = DataScienceFlow(start=data_prep_node)
data_science_flow.run({})
```
The output would be: `Call stack: ['EvaluateModelNode', 'ModelFlow', 'DataScienceFlow']`
For a more complete implementation, check out [the cookbook](https://github.com/The-Pocket/PocketFlow/tree/main/cookbook/pocketflow-tracing).
================================================
FILE: docs/utility_function/websearch.md
================================================
---
layout: default
title: "Web Search"
parent: "Utility Function"
nav_order: 3
---
# Web Search
We recommend some implementations of commonly used web search tools.
| **API** | **Free Tier** | **Pricing Model** | **Docs** |
|---------------------------------|-----------------------------------------------|-----------------------------------------------------------------|------------------------------------------------------------------------|
| **Google Custom Search JSON API** | 100 queries/day free | $5 per 1000 queries. | [Link](https://developers.google.com/custom-search/v1/overview) |
| **Bing Web Search API** | 1,000 queries/month | $15–$25 per 1,000 queries. | [Link](https://azure.microsoft.com/en-us/services/cognitive-services/bing-web-search-api/) |
| **DuckDuckGo Instant Answer** | Completely free (Instant Answers only, **no URLs**) | No paid plans; usage unlimited, but data is limited | [Link](https://duckduckgo.com/api) |
| **Brave Search API** | 2,000 queries/month free | $3 per 1k queries for Base, $5 per 1k for Pro | [Link](https://brave.com/search/api/) |
| **SerpApi** | 100 searches/month free | Start at $75/month for 5,000 searches| [Link](https://serpapi.com/) |
| **RapidAPI** | Many options | Many options | [Link](https://rapidapi.com/search?term=search&sortBy=ByRelevance) |
## Example Python Code
### 1. Google Custom Search JSON API
```python
import requests
API_KEY = "YOUR_API_KEY"
CX_ID = "YOUR_CX_ID"
query = "example"
url = "https://www.googleapis.com/customsearch/v1"
params = {
"key": API_KEY,
"cx": CX_ID,
"q": query
}
response = requests.get(url, params=params)
results = response.json()
print(results)
```
### 2. Bing Web Search API
```python
import requests
SUBSCRIPTION_KEY = "YOUR_BING_API_KEY"
query = "example"
url = "https://api.bing.microsoft.com/v7.0/search"
headers = {"Ocp-Apim-Subscription-Key": SUBSCRIPTION_KEY}
params = {"q": query}
response = requests.get(url, headers=headers, params=params)
results = response.json()
print(results)
```
### 3. DuckDuckGo Instant Answer
```python
import requests
query = "example"
url = "https://api.duckduckgo.com/"
params = {
"q": query,
"format": "json"
}
response = requests.get(url, params=params)
results = response.json()
print(results)
```
### 4. Brave Search API
```python
import requests
SUBSCRIPTION_TOKEN = "YOUR_BRAVE_API_TOKEN"
query = "example"
url = "https://api.search.brave.com/res/v1/web/search"
headers = {
"X-Subscription-Token": SUBSCRIPTION_TOKEN
}
params = {
"q": query
}
response = requests.get(url, headers=headers, params=params)
results = response.json()
print(results)
```
### 5. SerpApi
```python
import requests
API_KEY = "YOUR_SERPAPI_KEY"
query = "example"
url = "https://serpapi.com/search"
params = {
"engine": "google",
"q": query,
"api_key": API_KEY
}
response = requests.get(url, params=params)
results = response.json()
print(results)
```
================================================
FILE: pocketflow/__init__.py
================================================
import asyncio, warnings, copy, time
class BaseNode:
def __init__(self): self.params,self.successors={},{}
def set_params(self,params): self.params=params
def next(self,node,action="default"):
if action in self.successors: warnings.warn(f"Overwriting successor for action '{action}'")
self.successors[action]=node; return node
def prep(self,shared): pass
def exec(self,prep_res): pass
def post(self,shared,prep_res,exec_res): pass
def _exec(self,prep_res): return self.exec(prep_res)
def _run(self,shared): p=self.prep(shared); e=self._exec(p); return self.post(shared,p,e)
def run(self,shared):
if self.successors: warnings.warn("Node won't run successors. Use Flow.")
return self._run(shared)
def __rshift__(self,other): return self.next(other)
def __sub__(self,action):
if isinstance(action,str): return _ConditionalTransition(self,action)
raise TypeError("Action must be a string")
class _ConditionalTransition:
def __init__(self,src,action): self.src,self.action=src,action
def __rshift__(self,tgt): return self.src.next(tgt,self.action)
class Node(BaseNode):
def __init__(self,max_retries=1,wait=0): super().__init__(); self.max_retries,self.wait=max_retries,wait
def exec_fallback(self,prep_res,exc): raise exc
def _exec(self,prep_res):
for self.cur_retry in range(self.max_retries):
try: return self.exec(prep_res)
except Exception as e:
if self.cur_retry==self.max_retries-1: return self.exec_fallback(prep_res,e)
if self.wait>0: time.sleep(self.wait)
class BatchNode(Node):
def _exec(self,items): return [super(BatchNode,self)._exec(i) for i in (items or [])]
class Flow(BaseNode):
def __init__(self,start=None): super().__init__(); self.start_node=start
def start(self,start): self.start_node=start; return start
def get_next_node(self,curr,action):
nxt=curr.successors.get(action or "default")
if not nxt and curr.successors: warnings.warn(f"Flow ends: '{action}' not found in {list(curr.successors)}")
return nxt
def _orch(self,shared,params=None):
curr,p,last_action =copy.copy(self.start_node),(params or {**self.params}),None
while curr: curr.set_params(p); last_action=curr._run(shared); curr=copy.copy(self.get_next_node(curr,last_action))
return last_action
def _run(self,shared): p=self.prep(shared); o=self._orch(shared); return self.post(shared,p,o)
def post(self,shared,prep_res,exec_res): return exec_res
class BatchFlow(Flow):
def _run(self,shared):
pr=self.prep(shared) or []
for bp in pr: self._orch(shared,{**self.params,**bp})
return self.post(shared,pr,None)
class AsyncNode(Node):
async def prep_async(self,shared): pass
async def exec_async(self,prep_res): pass
async def exec_fallback_async(self,prep_res,exc): raise exc
async def post_async(self,shared,prep_res,exec_res): pass
async def _exec(self,prep_res):
for self.cur_retry in range(self.max_retries):
try: return await self.exec_async(prep_res)
except Exception as e:
if self.cur_retry==self.max_retries-1: return await self.exec_fallback_async(prep_res,e)
if self.wait>0: await asyncio.sleep(self.wait)
async def run_async(self,shared):
if self.successors: warnings.warn("Node won't run successors. Use AsyncFlow.")
return await self._run_async(shared)
async def _run_async(self,shared): p=await self.prep_async(shared); e=await self._exec(p); return await self.post_async(shared,p,e)
def _run(self,shared): raise RuntimeError("Use run_async.")
class AsyncBatchNode(AsyncNode,BatchNode):
async def _exec(self,items): return [await super(AsyncBatchNode,self)._exec(i) for i in items]
class AsyncParallelBatchNode(AsyncNode,BatchNode):
async def _exec(self,items): return await asyncio.gather(*(super(AsyncParallelBatchNode,self)._exec(i) for i in items))
class AsyncFlow(Flow,AsyncNode):
async def _orch_async(self,shared,params=None):
curr,p,last_action =copy.copy(self.start_node),(params or {**self.params}),None
while curr: curr.set_params(p); last_action=await curr._run_async(shared) if isinstance(curr,AsyncNode) else curr._run(shared); curr=copy.copy(self.get_next_node(curr,last_action))
return last_action
async def _run_async(self,shared): p=await self.prep_async(shared); o=await self._orch_async(shared); return await self.post_async(shared,p,o)
async def post_async(self,shared,prep_res,exec_res): return exec_res
class AsyncBatchFlow(AsyncFlow,BatchFlow):
async def _run_async(self,shared):
pr=await self.prep_async(shared) or []
for bp in pr: await self._orch_async(shared,{**self.params,**bp})
return await self.post_async(shared,pr,None)
class AsyncParallelBatchFlow(AsyncFlow,BatchFlow):
async def _run_async(self,shared):
pr=await self.prep_async(shared) or []
await asyncio.gather(*(self._orch_async(shared,{**self.params,**bp}) for bp in pr))
return await self.post_async(shared,pr,None)
================================================
FILE: pocketflow/__init__.pyi
================================================
import asyncio
from typing import Any, Dict, List, Optional, Union, TypeVar, Generic
# Type variables for better type relationships
_PrepResult = TypeVar('_PrepResult')
_ExecResult = TypeVar('_ExecResult')
_PostResult = TypeVar('_PostResult')
# More specific parameter types
ParamValue = Union[str, int, float, bool, None, List[Any], Dict[str, Any]]
SharedData = Dict[str, Any]
Params = Dict[str, ParamValue]
class BaseNode(Generic[_PrepResult, _ExecResult, _PostResult]):
params: Params
successors: Dict[str, BaseNode[Any, Any, Any]]
def __init__(self) -> None: ...
def set_params(self, params: Params) -> None: ...
def next(self, node: BaseNode[Any, Any, Any], action: str = "default") -> BaseNode[Any, Any, Any]: ...
def prep(self, shared: SharedData) -> _PrepResult: ...
def exec(self, prep_res: _PrepResult) -> _ExecResult: ...
def post(self, shared: SharedData, prep_res: _PrepResult, exec_res: _ExecResult) -> _PostResult: ...
def _exec(self, prep_res: _PrepResult) -> _ExecResult: ...
def _run(self, shared: SharedData) -> _PostResult: ...
def run(self, shared: SharedData) -> _PostResult: ...
def __rshift__(self, other: BaseNode[Any, Any, Any]) -> BaseNode[Any, Any, Any]: ...
def __sub__(self, action: str) -> _ConditionalTransition: ...
class _ConditionalTransition:
src: BaseNode[Any, Any, Any]
action: str
def __init__(self, src: BaseNode[Any, Any, Any], action: str) -> None: ...
def __rshift__(self, tgt: BaseNode[Any, Any, Any]) -> BaseNode[Any, Any, Any]: ...
class Node(BaseNode[_PrepResult, _ExecResult, _PostResult]):
max_retries: int
wait: Union[int, float]
cur_retry: int
def __init__(self, max_retries: int = 1, wait: Union[int, float] = 0) -> None: ...
def exec_fallback(self, prep_res: _PrepResult, exc: Exception) -> _ExecResult: ...
def _exec(self, prep_res: _PrepResult) -> _ExecResult: ...
class BatchNode(Node[Optional[List[_PrepResult]], List[_ExecResult], _PostResult]):
def _exec(self, items: Optional[List[_PrepResult]]) -> List[_ExecResult]: ...
class Flow(BaseNode[_PrepResult, Any, _PostResult]):
start_node: Optional[BaseNode[Any, Any, Any]]
def __init__(self, start: Optional[BaseNode[Any, Any, Any]] = None) -> None: ...
def start(self, start: BaseNode[Any, Any, Any]) -> BaseNode[Any, Any, Any]: ...
def get_next_node(
self, curr: BaseNode[Any, Any, Any], action: Optional[str]
) -> Optional[BaseNode[Any, Any, Any]]: ...
def _orch(
self, shared: SharedData, params: Optional[Params] = None
) -> Any: ...
def _run(self, shared: SharedData) -> _PostResult: ...
def post(self, shared: SharedData, prep_res: _PrepResult, exec_res: Any) -> _PostResult: ...
class BatchFlow(Flow[Optional[List[Params]], Any, _PostResult]):
def _run(self, shared: SharedData) -> _PostResult: ...
class AsyncNode(Node[_PrepResult, _ExecResult, _PostResult]):
async def prep_async(self, shared: SharedData) -> _PrepResult: ...
async def exec_async(self, prep_res: _PrepResult) -> _ExecResult: ...
async def exec_fallback_async(self, prep_res: _PrepResult, exc: Exception) -> _ExecResult: ...
async def post_async(
self, shared: SharedData, prep_res: _PrepResult, exec_res: _ExecResult
) -> _PostResult: ...
async def _exec(self, prep_res: _PrepResult) -> _ExecResult: ...
async def run_async(self, shared: SharedData) -> _PostResult: ...
async def _run_async(self, shared: SharedData) -> _PostResult: ...
def _run(self, shared: SharedData) -> _PostResult: ...
class AsyncBatchNode(AsyncNode[Optional[List[_PrepResult]], List[_ExecResult], _PostResult], BatchNode[Optional[List[_PrepResult]], List[_ExecResult], _PostResult]):
async def _exec(self, items: Optional[List[_PrepResult]]) -> List[_ExecResult]: ...
class AsyncParallelBatchNode(AsyncNode[Optional[List[_PrepResult]], List[_ExecResult], _PostResult], BatchNode[Optional[List[_PrepResult]], List[_ExecResult], _PostResult]):
async def _exec(self, items: Optional[List[_PrepResult]]) -> List[_ExecResult]: ...
class AsyncFlow(Flow[_PrepResult, Any, _PostResult], AsyncNode[_PrepResult, Any, _PostResult]):
async def _orch_async(
self, shared: SharedData, params: Optional[Params] = None
) -> Any: ...
async def _run_async(self, shared: SharedData) -> _PostResult: ...
async def post_async(
self, shared: SharedData, prep_res: _PrepResult, exec_res: Any
) -> _PostResult: ...
class AsyncBatchFlow(AsyncFlow[Optional[List[Params]], Any, _PostResult], BatchFlow[Optional[List[Params]], Any, _PostResult]):
async def _run_async(self, shared: SharedData) -> _PostResult: ...
class AsyncParallelBatchFlow(AsyncFlow[Optional[List[Params]], Any, _PostResult], BatchFlow[Optional[List[Params]], Any, _PostResult]):
async def _run_async(self, shared: SharedData) -> _PostResult: ...
================================================
FILE: setup.py
================================================
from setuptools import setup, find_packages
setup(
name="pocketflow",
version="0.0.3",
packages=find_packages(),
author="Zachary Huang",
author_email="zh2408@columbia.edu",
description="Pocket Flow: 100-line LLM framework. Let Agents build Agents!",
url="https://github.com/The-Pocket/PocketFlow",
)
================================================
FILE: tests/test_async_batch_flow.py
================================================
import unittest
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pocketflow import AsyncNode, AsyncBatchFlow
class AsyncDataProcessNode(AsyncNode):
async def prep_async(self, shared_storage):
key = self.params.get('key')
data = shared_storage['input_data'][key]
if 'results' not in shared_storage:
shared_storage['results'] = {}
shared_storage['results'][key] = data
return data
async def post_async(self, shared_storage, prep_result, proc_result):
await asyncio.sleep(0.01) # Simulate async work
key = self.params.get('key')
shared_storage['results'][key] = prep_result * 2 # Double the value
return "processed"
class AsyncErrorNode(AsyncNode):
async def post_async(self, shared_storage, prep_result, proc_result):
key = self.params.get('key')
if key == 'error_key':
raise ValueError(f"Async error processing key: {key}")
return "processed"
class TestAsyncBatchFlow(unittest.TestCase):
def setUp(self):
self.process_node = AsyncDataProcessNode()
def test_basic_async_batch_processing(self):
"""Test basic async batch processing with multiple keys"""
class SimpleTestAsyncBatchFlow(AsyncBatchFlow):
async def prep_async(self, shared_storage):
return [{'key': k} for k in shared_storage['input_data'].keys()]
shared_storage = {
'input_data': {
'a': 1,
'b': 2,
'c': 3
}
}
flow = SimpleTestAsyncBatchFlow(start=self.process_node)
asyncio.run(flow.run_async(shared_storage))
expected_results = {
'a': 2, # 1 * 2
'b': 4, # 2 * 2
'c': 6 # 3 * 2
}
self.assertEqual(shared_storage['results'], expected_results)
def test_empty_async_batch(self):
"""Test async batch processing with empty input"""
class EmptyTestAsyncBatchFlow(AsyncBatchFlow):
async def prep_async(self, shared_storage):
return [{'key': k} for k in shared_storage['input_data'].keys()]
shared_storage = {
'input_data': {}
}
flow = EmptyTestAsyncBatchFlow(start=self.process_node)
asyncio.run(flow.run_async(shared_storage))
self.assertEqual(shared_storage.get('results', {}), {})
def test_async_error_handling(self):
"""Test error handling during async batch processing"""
class ErrorTestAsyncBatchFlow(AsyncBatchFlow):
async def prep_async(self, shared_storage):
return [{'key': k} for k in shared_storage['input_data'].keys()]
shared_storage = {
'input_data': {
'normal_key': 1,
'error_key': 2,
'another_key': 3
}
}
flow = ErrorTestAsyncBatchFlow(start=AsyncErrorNode())
with self.assertRaises(ValueError):
asyncio.run(flow.run_async(shared_storage))
def test_nested_async_flow(self):
"""Test async batch processing with nested flows"""
class AsyncInnerNode(AsyncNode):
async def post_async(self, shared_storage, prep_result, proc_result):
key = self.params.get('key')
if 'intermediate_results' not in shared_storage:
shared_storage['intermediate_results'] = {}
shared_storage['intermediate_results'][key] = shared_storage['input_data'][key] + 1
await asyncio.sleep(0.01)
return "next"
class AsyncOuterNode(AsyncNode):
async def post_async(self, shared_storage, prep_result, proc_result):
key = self.params.get('key')
if 'results' not in shared_storage:
shared_storage['results'] = {}
shared_storage['results'][key] = shared_storage['intermediate_results'][key] * 2
await asyncio.sleep(0.01)
return "done"
class NestedAsyncBatchFlow(AsyncBatchFlow):
async def prep_async(self, shared_storage):
return [{'key': k} for k in shared_storage['input_data'].keys()]
# Create inner flow
inner_node = AsyncInnerNode()
outer_node = AsyncOuterNode()
inner_node - "next" >> outer_node
shared_storage = {
'input_data': {
'x': 1,
'y': 2
}
}
flow = NestedAsyncBatchFlow(start=inner_node)
asyncio.run(flow.run_async(shared_storage))
expected_results = {
'x': 4, # (1 + 1) * 2
'y': 6 # (2 + 1) * 2
}
self.assertEqual(shared_storage['results'], expected_results)
def test_custom_async_parameters(self):
"""Test async batch processing with additional custom parameters"""
class CustomParamAsyncNode(AsyncNode):
async def post_async(self, shared_storage, prep_result, proc_result):
key = self.params.get('key')
multiplier = self.params.get('multiplier', 1)
await asyncio.sleep(0.01)
if 'results' not in shared_storage:
shared_storage['results'] = {}
shared_storage['results'][key] = shared_storage['input_data'][key] * multiplier
return "done"
class CustomParamAsyncBatchFlow(AsyncBatchFlow):
async def prep_async(self, shared_storage):
return [{
'key': k,
'multiplier': i + 1
} for i, k in enumerate(shared_storage['input_data'].keys())]
shared_storage = {
'input_data': {
'a': 1,
'b': 2,
'c': 3
}
}
flow = CustomParamAsyncBatchFlow(start=CustomParamAsyncNode())
asyncio.run(flow.run_async(shared_storage))
expected_results = {
'a': 1 * 1, # first item, multiplier = 1
'b': 2 * 2, # second item, multiplier = 2
'c': 3 * 3 # third item, multiplier = 3
}
self.assertEqual(shared_storage['results'], expected_results)
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_async_batch_node.py
================================================
import unittest
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pocketflow import AsyncNode, AsyncBatchNode, AsyncFlow
class AsyncArrayChunkNode(AsyncBatchNode):
def __init__(self, chunk_size=10):
super().__init__()
self.chunk_size = chunk_size
async def prep_async(self, shared_storage):
# Get array from shared storage and split into chunks
array = shared_storage.get('input_array', [])
chunks = []
for start in range(0, len(array), self.chunk_size):
end = min(start + self.chunk_size, len(array))
chunks.append(array[start:end])
return chunks
async def exec_async(self, chunk):
# Simulate async processing of each chunk
await asyncio.sleep(0.01)
return sum(chunk)
async def post_async(self, shared_storage, prep_result, proc_result):
# Store chunk results in shared storage
shared_storage['chunk_results'] = proc_result
return "processed"
class AsyncSumReduceNode(AsyncNode):
async def prep_async(self, shared_storage):
# Get chunk results from shared storage
chunk_results = shared_storage.get('chunk_results', [])
await asyncio.sleep(0.01) # Simulate async processing
total = sum(chunk_results)
shared_storage['total'] = total
return "reduced"
class TestAsyncBatchNode(unittest.TestCase):
def test_array_chunking(self):
"""
Test that the array is correctly split into chunks and processed asynchronously
"""
shared_storage = {
'input_array': list(range(25)) # [0,1,2,...,24]
}
chunk_node = AsyncArrayChunkNode(chunk_size=10)
asyncio.run(chunk_node.run_async(shared_storage))
results = shared_storage['chunk_results']
self.assertEqual(results, [45, 145, 110]) # Sum of chunks [0-9], [10-19], [20-24]
# def test_async_map_reduce_sum(self):
# """
# Test a complete async map-reduce pipeline that sums a large array:
# 1. Map: Split array into chunks and sum each chunk asynchronously
# 2. Reduce: Sum all the chunk sums asynchronously
# """
# array = list(range(100))
# expected_sum = sum(array) # 4950
# shared_storage = {
# 'input_array': array
# }
# # Create nodes
# chunk_node = AsyncArrayChunkNode(chunk_size=10)
# reduce_node = AsyncSumReduceNode()
# # Connect nodes
# chunk_node - "processed" >> reduce_node
# # Create and run pipeline
# pipeline = AsyncFlow(start=chunk_node)
# asyncio.run(pipeline.run_async(shared_storage))
# self.assertEqual(shared_storage['total'], expected_sum)
# def test_uneven_chunks(self):
# """
# Test that the async map-reduce works correctly with array lengths
# that don't divide evenly by chunk_size
# """
# array = list(range(25))
# expected_sum = sum(array) # 300
# shared_storage = {
# 'input_array': array
# }
# chunk_node = AsyncArrayChunkNode(chunk_size=10)
# reduce_node = AsyncSumReduceNode()
# chunk_node - "processed" >> reduce_node
# pipeline = AsyncFlow(start=chunk_node)
# asyncio.run(pipeline.run_async(shared_storage))
# self.assertEqual(shared_storage['total'], expected_sum)
# def test_custom_chunk_size(self):
# """
# Test that the async map-reduce works with different chunk sizes
# """
# array = list(range(100))
# expected_sum = sum(array)
# shared_storage = {
# 'input_array': array
# }
# # Use chunk_size=15 instead of default 10
# chunk_node = AsyncArrayChunkNode(chunk_size=15)
# reduce_node = AsyncSumReduceNode()
# chunk_node - "processed" >> reduce_node
# pipeline = AsyncFlow(start=chunk_node)
# asyncio.run(pipeline.run_async(shared_storage))
# self.assertEqual(shared_storage['total'], expected_sum)
# def test_single_element_chunks(self):
# """
# Test extreme case where chunk_size=1
# """
# array = list(range(5))
# expected_sum = sum(array)
# shared_storage = {
# 'input_array': array
# }
# chunk_node = AsyncArrayChunkNode(chunk_size=1)
# reduce_node = AsyncSumReduceNode()
# chunk_node - "processed" >> reduce_node
# pipeline = AsyncFlow(start=chunk_node)
# asyncio.run(pipeline.run_async(shared_storage))
# self.assertEqual(shared_storage['total'], expected_sum)
# def test_empty_array(self):
# """
# Test edge case of empty input array
# """
# shared_storage = {
# 'input_array': []
# }
# chunk_node = AsyncArrayChunkNode(chunk_size=10)
# reduce_node = AsyncSumReduceNode()
# chunk_node - "processed" >> reduce_node
# pipeline = AsyncFlow(start=chunk_node)
# asyncio.run(pipeline.run_async(shared_storage))
# self.assertEqual(shared_storage['total'], 0)
# def test_error_handling(self):
# """
# Test error handling in async batch processing
# """
# class ErrorAsyncBatchNode(AsyncBatchNode):
# async def exec_async(self, item):
# if item == 2:
# raise ValueError("Error processing item 2")
# return item
# shared_storage = {
# 'input_array': [1, 2, 3]
# }
# error_node = ErrorAsyncBatchNode()
# with self.assertRaises(ValueError):
# asyncio.run(error_node.run_async(shared_storage))
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_async_flow.py
================================================
import unittest
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pocketflow import Node, AsyncNode, AsyncFlow
class AsyncNumberNode(AsyncNode):
"""
Simple async node that sets 'current' to a given number.
Demonstrates overriding .process() (sync) and using
post_async() for the async portion.
"""
def __init__(self, number):
super().__init__()
self.number = number
async def prep_async(self, shared_storage):
# Synchronous work is allowed inside an AsyncNode,
# but final 'condition' is determined by post_async().
shared_storage['current'] = self.number
return "set_number"
async def post_async(self, shared_storage, prep_result, proc_result):
# Possibly do asynchronous tasks here
await asyncio.sleep(0.01)
# Return a condition for the flow
return "number_set"
class AsyncIncrementNode(AsyncNode):
"""
Demonstrates incrementing the 'current' value asynchronously.
"""
async def prep_async(self, shared_storage):
shared_storage['current'] = shared_storage.get('current', 0) + 1
return "incremented"
async def post_async(self, shared_storage, prep_result, proc_result):
await asyncio.sleep(0.01) # simulate async I/O
return "done"
class AsyncSignalNode(AsyncNode):
""" An async node that returns a specific signal string from post_async. """
def __init__(self, signal="default_async_signal"):
super().__init__()
self.signal = signal
# No prep needed usually if just signaling
async def prep_async(self, shared_storage):
await asyncio.sleep(0.01) # Simulate async work
async def post_async(self, shared_storage, prep_result, exec_result):
# Store the signal in shared storage for verification
shared_storage['last_async_signal_emitted'] = self.signal
await asyncio.sleep(0.01) # Simulate async work
print(self.signal)
return self.signal # Return the specific action string
class AsyncPathNode(AsyncNode):
""" An async node to indicate which path was taken in the outer flow. """
def __init__(self, path_id):
super().__init__()
self.path_id = path_id
async def prep_async(self, shared_storage):
await asyncio.sleep(0.01) # Simulate async work
shared_storage['async_path_taken'] = self.path_id
# post_async implicitly returns None (for default transition out if needed)
async def post_async(self, shared_storage, prep_result, exec_result):
await asyncio.sleep(0.01)
# Return None by default
class TestAsyncNode(unittest.TestCase):
"""
Test the AsyncNode (and descendants) in isolation (not in a flow).
"""
def test_async_number_node_direct_call(self):
"""
Even though AsyncNumberNode is designed for an async flow,
we can still test it directly by calling run_async().
"""
async def run_node():
node = AsyncNumberNode(42)
shared_storage = {}
condition = await node.run_async(shared_storage)
return shared_storage, condition
shared_storage, condition = asyncio.run(run_node())
self.assertEqual(shared_storage['current'], 42)
self.assertEqual(condition, "number_set")
def test_async_increment_node_direct_call(self):
async def run_node():
node = AsyncIncrementNode()
shared_storage = {'current': 10}
condition = await node.run_async(shared_storage)
return shared_storage, condition
shared_storage, condition = asyncio.run(run_node())
self.assertEqual(shared_storage['current'], 11)
self.assertEqual(condition, "done")
class TestAsyncFlow(unittest.TestCase):
"""
Test how AsyncFlow orchestrates multiple async nodes.
"""
def test_simple_async_flow(self):
"""
Flow:
1) AsyncNumberNode(5) -> sets 'current' to 5
2) AsyncIncrementNode() -> increments 'current' to 6
"""
# Create our nodes
start = AsyncNumberNode(5)
inc_node = AsyncIncrementNode()
# Chain them: start >> inc_node
start - "number_set" >> inc_node
# Create an AsyncFlow with start
flow = AsyncFlow(start)
# We'll run the flow synchronously (which under the hood is asyncio.run())
shared_storage = {}
asyncio.run(flow.run_async(shared_storage))
self.assertEqual(shared_storage['current'], 6)
def test_async_flow_branching(self):
"""
Demonstrate a branching scenario where we return different
conditions. For example, you could have an async node that
returns "go_left" or "go_right" in post_async, but here
we'll keep it simpler for demonstration.
"""
class BranchingAsyncNode(AsyncNode):
def exec(self, data):
value = shared_storage.get("value", 0)
shared_storage["value"] = value
# We'll decide branch based on whether 'value' is positive
return None
async def post_async(self, shared_storage, prep_result, proc_result):
await asyncio.sleep(0.01)
if shared_storage["value"] >= 0:
return "positive_branch"
else:
return "negative_branch"
class PositiveNode(Node):
def exec(self, data):
shared_storage["path"] = "positive"
return None
class NegativeNode(Node):
def exec(self, data):
shared_storage["path"] = "negative"
return None
shared_storage = {"value": 10}
start = BranchingAsyncNode()
positive_node = PositiveNode()
negative_node = NegativeNode()
# Condition-based chaining
start - "positive_branch" >> positive_node
start - "negative_branch" >> negative_node
flow = AsyncFlow(start)
asyncio.run(flow.run_async(shared_storage))
self.assertEqual(shared_storage["path"], "positive",
"Should have taken the positive branch")
def test_async_composition_with_action_propagation(self):
"""
Test AsyncFlow branches based on action from nested AsyncFlow's last node.
"""
async def run_test():
shared_storage = {}
# 1. Define an inner async flow ending with AsyncSignalNode
# Use existing AsyncNumberNode which should return None from post_async implicitly
inner_start_node = AsyncNumberNode(200)
inner_end_node = AsyncSignalNode("async_inner_done") # post_async -> "async_inner_done"
inner_start_node - "number_set" >> inner_end_node
# Inner flow will execute start->end, Flow exec returns "async_inner_done"
inner_flow = AsyncFlow(start=inner_start_node)
# 2. Define target async nodes for the outer flow branches
path_a_node = AsyncPathNode("AsyncA") # post_async -> None
path_b_node = AsyncPathNode("AsyncB") # post_async -> None
# 3. Define the outer async flow starting with the inner async flow
outer_flow = AsyncFlow(start=inner_flow)
# 4. Define branches FROM the inner_flow object based on its returned action
inner_flow - "async_inner_done" >> path_b_node # This path should be taken
inner_flow - "other_action" >> path_a_node # This path should NOT be taken
# 5. Run the outer async flow and capture the last action
# Execution: inner_start -> inner_end -> path_b
last_action_outer = await outer_flow.run_async(shared_storage)
# 6. Return results for assertion
return shared_storage, last_action_outer
# Run the async test function
shared_storage, last_action_outer = asyncio.run(run_test())
# 7. Assert the results
# Check state after inner flow execution
self.assertEqual(shared_storage.get('current'), 200) # From AsyncNumberNode
self.assertEqual(shared_storage.get('last_async_signal_emitted'), "async_inner_done")
# Check that the correct outer path was taken
self.assertEqual(shared_storage.get('async_path_taken'), "AsyncB")
# Check the action returned by the outer flow. The last node executed was
# path_b_node, which returns None from its post_async method.
self.assertIsNone(last_action_outer)
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_async_parallel_batch_flow.py
================================================
import unittest
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pocketflow import AsyncNode, AsyncParallelBatchNode, AsyncParallelBatchFlow
class AsyncParallelNumberProcessor(AsyncParallelBatchNode):
def __init__(self, delay=0.1):
super().__init__()
self.delay = delay
async def prep_async(self, shared_storage):
batch = shared_storage['batches'][self.params['batch_id']]
return batch
async def exec_async(self, number):
await asyncio.sleep(self.delay) # Simulate async processing
return number * 2
async def post_async(self, shared_storage, prep_result, exec_result):
if 'processed_numbers' not in shared_storage:
shared_storage['processed_numbers'] = {}
shared_storage['processed_numbers'][self.params['batch_id']] = exec_result
return "processed"
class AsyncAggregatorNode(AsyncNode):
async def prep_async(self, shared_storage):
# Combine all batch results in order
all_results = []
processed = shared_storage.get('processed_numbers', {})
for i in range(len(processed)):
all_results.extend(processed[i])
return all_results
async def exec_async(self, prep_result):
await asyncio.sleep(0.01)
return sum(prep_result)
async def post_async(self, shared_storage, prep_result, exec_result):
shared_storage['total'] = exec_result
return "aggregated"
class TestAsyncParallelBatchFlow(unittest.TestCase):
def setUp(self):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
def tearDown(self):
self.loop.close()
def test_parallel_batch_flow(self):
"""
Test basic parallel batch processing flow with batch IDs
"""
class TestParallelBatchFlow(AsyncParallelBatchFlow):
async def prep_async(self, shared_storage):
return [{'batch_id': i} for i in range(len(shared_storage['batches']))]
shared_storage = {
'batches': [
[1, 2, 3], # batch_id: 0
[4, 5, 6], # batch_id: 1
[7, 8, 9] # batch_id: 2
]
}
processor = AsyncParallelNumberProcessor(delay=0.1)
aggregator = AsyncAggregatorNode()
processor - "processed" >> aggregator
flow = TestParallelBatchFlow(start=processor)
start_time = self.loop.time()
self.loop.run_until_complete(flow.run_async(shared_storage))
execution_time = self.loop.time() - start_time
# Verify each batch was processed correctly
expected_batch_results = {
0: [2, 4, 6], # [1,2,3] * 2
1: [8, 10, 12], # [4,5,6] * 2
2: [14, 16, 18] # [7,8,9] * 2
}
self.assertEqual(shared_storage['processed_numbers'], expected_batch_results)
# Verify total
expected_total = sum(num * 2 for batch in shared_storage['batches'] for num in batch)
self.assertEqual(shared_storage['total'], expected_total)
# Verify parallel execution
self.assertLess(execution_time, 0.2)
def test_error_handling(self):
"""
Test error handling in parallel batch flow
"""
class ErrorProcessor(AsyncParallelNumberProcessor):
async def exec_async(self, item):
if item == 2:
raise ValueError(f"Error processing item {item}")
return item
class ErrorBatchFlow(AsyncParallelBatchFlow):
async def prep_async(self, shared_storage):
return [{'batch_id': i} for i in range(len(shared_storage['batches']))]
shared_storage = {
'batches': [
[1, 2, 3], # Contains error-triggering value
[4, 5, 6]
]
}
processor = ErrorProcessor()
flow = ErrorBatchFlow(start=processor)
with self.assertRaises(ValueError):
self.loop.run_until_complete(flow.run_async(shared_storage))
def test_multiple_batch_sizes(self):
"""
Test parallel batch flow with varying batch sizes
"""
class VaryingBatchFlow(AsyncParallelBatchFlow):
async def prep_async(self, shared_storage):
return [{'batch_id': i} for i in range(len(shared_storage['batches']))]
shared_storage = {
'batches': [
[1], # batch_id: 0
[2, 3, 4], # batch_id: 1
[5, 6], # batch_id: 2
[7, 8, 9, 10] # batch_id: 3
]
}
processor = AsyncParallelNumberProcessor(delay=0.05)
aggregator = AsyncAggregatorNode()
processor - "processed" >> aggregator
flow = VaryingBatchFlow(start=processor)
self.loop.run_until_complete(flow.run_async(shared_storage))
# Verify each batch was processed correctly
expected_batch_results = {
0: [2], # [1] * 2
1: [4, 6, 8], # [2,3,4] * 2
2: [10, 12], # [5,6] * 2
3: [14, 16, 18, 20] # [7,8,9,10] * 2
}
self.assertEqual(shared_storage['processed_numbers'], expected_batch_results)
# Verify total
expected_total = sum(num * 2 for batch in shared_storage['batches'] for num in batch)
self.assertEqual(shared_storage['total'], expected_total)
class AsyncItemNode(AsyncNode):
async def prep_async(self, shared_storage):
return shared_storage['groups'][self.params['group']][self.params['item']]
async def exec_async(self, prep_res):
return prep_res * 2
async def post_async(self, shared_storage, prep_res, exec_res):
group = self.params['group']
if 'results' not in shared_storage:
shared_storage['results'] = {}
if group not in shared_storage['results']:
shared_storage['results'][group] = []
shared_storage['results'][group].append(exec_res)
class InnerAsyncParallelBatchFlow(AsyncParallelBatchFlow):
async def prep_async(self, shared_storage):
group = self.params['group']
return [{'item': i, 'group': group} for i in range(len(shared_storage['groups'][group]))]
class OuterAsyncParallelBatchFlow(AsyncParallelBatchFlow):
async def prep_async(self, shared_storage):
return [{'group': g} for g in shared_storage['groups']]
class TestNestedAsyncParallelBatchFlow(unittest.TestCase):
def setUp(self):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
def tearDown(self):
self.loop.close()
def test_nested_parallel_batch_flow(self):
"""Test AsyncParallelBatchFlow nested inside another (same structure as sync test)"""
item_node = AsyncItemNode()
inner_flow = InnerAsyncParallelBatchFlow(start=item_node)
outer_flow = OuterAsyncParallelBatchFlow(start=inner_flow)
shared_storage = {
'groups': {
'A': [1, 2],
'B': [3, 4],
}
}
self.loop.run_until_complete(outer_flow.run_async(shared_storage))
expected = {
'A': [2, 4],
'B': [6, 8],
}
self.assertEqual(sorted(shared_storage['results'].keys()), sorted(expected.keys()))
for group in expected:
self.assertCountEqual(shared_storage['results'][group], expected[group])
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_async_parallel_batch_node.py
================================================
import unittest
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pocketflow import AsyncParallelBatchNode, AsyncParallelBatchFlow
class AsyncParallelNumberProcessor(AsyncParallelBatchNode):
def __init__(self, delay=0.1):
super().__init__()
self.delay = delay
async def prep_async(self, shared_storage):
numbers = shared_storage.get('input_numbers', [])
return numbers
async def exec_async(self, number):
await asyncio.sleep(self.delay) # Simulate async processing
return number * 2
async def post_async(self, shared_storage, prep_result, exec_result):
shared_storage['processed_numbers'] = exec_result
return "processed"
class TestAsyncParallelBatchNode(unittest.TestCase):
def setUp(self):
# Reset the event loop for each test
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
def tearDown(self):
self.loop.close()
def test_parallel_processing(self):
"""
Test that numbers are processed in parallel by measuring execution time
"""
shared_storage = {
'input_numbers': list(range(5))
}
processor = AsyncParallelNumberProcessor(delay=0.1)
# Run the processor
start_time = asyncio.get_event_loop().time()
self.loop.run_until_complete(processor.run_async(shared_storage))
end_time = asyncio.get_event_loop().time()
# Check results
expected = [0, 2, 4, 6, 8] # Each number doubled
self.assertEqual(shared_storage['processed_numbers'], expected)
# Since processing is parallel, total time should be approximately
# equal to the delay of a single operation, not delay * number_of_items
execution_time = end_time - start_time
self.assertLess(execution_time, 0.2) # Should be around 0.1s plus minimal overhead
def test_empty_input(self):
"""
Test processing of empty input
"""
shared_storage = {
'input_numbers': []
}
processor = AsyncParallelNumberProcessor()
self.loop.run_until_complete(processor.run_async(shared_storage))
self.assertEqual(shared_storage['processed_numbers'], [])
def test_single_item(self):
"""
Test processing of a single item
"""
shared_storage = {
'input_numbers': [42]
}
processor = AsyncParallelNumberProcessor()
self.loop.run_until_complete(processor.run_async(shared_storage))
self.assertEqual(shared_storage['processed_numbers'], [84])
def test_large_batch(self):
"""
Test processing of a large batch of numbers
"""
input_size = 100
shared_storage = {
'input_numbers': list(range(input_size))
}
processor = AsyncParallelNumberProcessor(delay=0.01)
self.loop.run_until_complete(processor.run_async(shared_storage))
expected = [x * 2 for x in range(input_size)]
self.assertEqual(shared_storage['processed_numbers'], expected)
def test_error_handling(self):
"""
Test error handling during parallel processing
"""
class ErrorProcessor(AsyncParallelNumberProcessor):
async def exec_async(self, item):
if item == 2:
raise ValueError(f"Error processing item {item}")
return item
shared_storage = {
'input_numbers': [1, 2, 3]
}
processor = ErrorProcessor()
with self.assertRaises(ValueError):
self.loop.run_until_complete(processor.run_async(shared_storage))
def test_concurrent_execution(self):
"""
Test that tasks are actually running concurrently by tracking execution order
"""
execution_order = []
class OrderTrackingProcessor(AsyncParallelNumberProcessor):
async def exec_async(self, item):
delay = 0.1 if item % 2 == 0 else 0.05
await asyncio.sleep(delay)
execution_order.append(item)
return item
shared_storage = {
'input_numbers': list(range(4)) # [0, 1, 2, 3]
}
processor = OrderTrackingProcessor()
self.loop.run_until_complete(processor.run_async(shared_storage))
# Odd numbers should finish before even numbers due to shorter delay
self.assertLess(execution_order.index(1), execution_order.index(0))
self.assertLess(execution_order.index(3), execution_order.index(2))
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_batch_flow.py
================================================
import unittest
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pocketflow import Node, BatchFlow, Flow
class DataProcessNode(Node):
def prep(self, shared_storage):
key = self.params.get('key')
data = shared_storage['input_data'][key]
if 'results' not in shared_storage:
shared_storage['results'] = {}
shared_storage['results'][key] = data * 2
class ErrorProcessNode(Node):
def prep(self, shared_storage):
key = self.params.get('key')
if key == 'error_key':
raise ValueError(f"Error processing key: {key}")
if 'results' not in shared_storage:
shared_storage['results'] = {}
shared_storage['results'][key] = True
class TestBatchFlow(unittest.TestCase):
def setUp(self):
self.process_node = DataProcessNode()
def test_basic_batch_processing(self):
"""Test basic batch processing with multiple keys"""
class SimpleTestBatchFlow(BatchFlow):
def prep(self, shared_storage):
return [{'key': k} for k in shared_storage['input_data'].keys()]
shared_storage = {
'input_data': {
'a': 1,
'b': 2,
'c': 3
}
}
flow = SimpleTestBatchFlow(start=self.process_node)
flow.run(shared_storage)
expected_results = {
'a': 2,
'b': 4,
'c': 6
}
self.assertEqual(shared_storage['results'], expected_results)
def test_empty_input(self):
"""Test batch processing with empty input dictionary"""
class EmptyTestBatchFlow(BatchFlow):
def prep(self, shared_storage):
return [{'key': k} for k in shared_storage['input_data'].keys()]
shared_storage = {
'input_data': {}
}
flow = EmptyTestBatchFlow(start=self.process_node)
flow.run(shared_storage)
self.assertEqual(shared_storage.get('results', {}), {})
def test_single_item(self):
"""Test batch processing with single item"""
class SingleItemBatchFlow(BatchFlow):
def prep(self, shared_storage):
return [{'key': k} for k in shared_storage['input_data'].keys()]
shared_storage = {
'input_data': {
'single': 5
}
}
flow = SingleItemBatchFlow(start=self.process_node)
flow.run(shared_storage)
expected_results = {
'single': 10
}
self.assertEqual(shared_storage['results'], expected_results)
def test_error_handling(self):
"""Test error handling during batch processing"""
class ErrorTestBatchFlow(BatchFlow):
def prep(self, shared_storage):
return [{'key': k} for k in shared_storage['input_data'].keys()]
shared_storage = {
'input_data': {
'normal_key': 1,
'error_key': 2,
'another_key': 3
}
}
flow = ErrorTestBatchFlow(start=ErrorProcessNode())
with self.assertRaises(ValueError):
flow.run(shared_storage)
def test_nested_flow(self):
"""Test batch processing with nested flows"""
class InnerNode(Node):
def exec(self, prep_result):
key = self.params.get('key')
if 'intermediate_results' not in shared_storage:
shared_storage['intermediate_results'] = {}
shared_storage['intermediate_results'][key] = shared_storage['input_data'][key] + 1
class OuterNode(Node):
def exec(self, prep_result):
key = self.params.get('key')
if 'results' not in shared_storage:
shared_storage['results'] = {}
shared_storage['results'][key] = shared_storage['intermediate_results'][key] * 2
class NestedBatchFlow(BatchFlow):
def prep(self, shared_storage):
return [{'key': k} for k in shared_storage['input_data'].keys()]
# Create inner flow
inner_node = InnerNode()
outer_node = OuterNode()
inner_node >> outer_node
shared_storage = {
'input_data': {
'x': 1,
'y': 2
}
}
flow = NestedBatchFlow(start=inner_node)
flow.run(shared_storage)
expected_results = {
'x': 4, # (1 + 1) * 2
'y': 6 # (2 + 1) * 2
}
self.assertEqual(shared_storage['results'], expected_results)
def test_custom_parameters(self):
"""Test batch processing with additional custom parameters"""
class CustomParamNode(Node):
def exec(self, prep_result):
key = self.params.get('key')
multiplier = self.params.get('multiplier', 1)
if 'results' not in shared_storage:
shared_storage['results'] = {}
shared_storage['results'][key] = shared_storage['input_data'][key] * multiplier
class CustomParamBatchFlow(BatchFlow):
def prep(self, shared_storage):
return [{
'key': k,
'multiplier': i + 1
} for i, k in enumerate(shared_storage['input_data'].keys())]
shared_storage = {
'input_data': {
'a': 1,
'b': 2,
'c': 3
}
}
flow = CustomParamBatchFlow(start=CustomParamNode())
flow.run(shared_storage)
expected_results = {
'a': 1 * 1, # first item, multiplier = 1
'b': 2 * 2, # second item, multiplier = 2
'c': 3 * 3 # third item, multiplier = 3
}
self.assertEqual(shared_storage['results'], expected_results)
def test_nested_batch_flow(self):
"""Test BatchFlow nested inside another BatchFlow (outer iterates groups, inner iterates items)"""
class ItemNode(Node):
def prep(self, shared_storage):
return shared_storage['groups'][self.params['group']][self.params['item']]
def exec(self, prep_res):
return prep_res * 2
def post(self, shared_storage, prep_res, exec_res):
group = self.params['group']
if 'results' not in shared_storage:
shared_storage['results'] = {}
if group not in shared_storage['results']:
shared_storage['results'][group] = []
shared_storage['results'][group].append(exec_res)
class InnerBatchFlow(BatchFlow):
def prep(self, shared_storage):
group = self.params['group']
return [{'item': i, 'group': group} for i in range(len(shared_storage['groups'][group]))]
class OuterBatchFlow(BatchFlow):
def prep(self, shared_storage):
return [{'group': g} for g in shared_storage['groups']]
item_node = ItemNode()
inner_flow = InnerBatchFlow(start=item_node)
outer_flow = OuterBatchFlow(start=inner_flow)
shared_storage = {
'groups': {
'A': [1, 2],
'B': [3, 4],
}
}
outer_flow.run(shared_storage)
expected = {
'A': [2, 4],
'B': [6, 8],
}
self.assertEqual(shared_storage['results'], expected)
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_batch_node.py
================================================
import unittest
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pocketflow import Node, BatchNode, Flow
class ArrayChunkNode(BatchNode):
def __init__(self, chunk_size=10):
super().__init__()
self.chunk_size = chunk_size
def prep(self, shared_storage):
# Get array from shared storage and split into chunks
array = shared_storage.get('input_array', [])
chunks = []
for start in range(0, len(array), self.chunk_size):
end = min(start + self.chunk_size, len(array))
chunks.append(array[start: end])
return chunks
def exec(self, chunk):
# Process the chunk and return its sum
chunk_sum = sum(chunk)
return chunk_sum
def post(self, shared_storage, prep_result, proc_result):
# Store chunk results in shared storage
shared_storage['chunk_results'] = proc_result
return "default"
class SumReduceNode(Node):
def prep(self, shared_storage):
# Get chunk results from shared storage and sum them
chunk_results = shared_storage.get('chunk_results', [])
total = sum(chunk_results)
shared_storage['total'] = total
class TestBatchNode(unittest.TestCase):
def test_array_chunking(self):
"""
Test that the array is correctly split into chunks
"""
shared_storage = {
'input_array': list(range(25)) # [0,1,2,...,24]
}
chunk_node = ArrayChunkNode(chunk_size=10)
chunk_node.run(shared_storage)
results = shared_storage['chunk_results']
self.assertEqual(results, [45, 145, 110])
def test_map_reduce_sum(self):
"""
Test a complete map-reduce pipeline that sums a large array:
1. Map: Split array into chunks and sum each chunk
2. Reduce: Sum all the chunk sums
"""
# Create test array: [0,1,2,...,99]
array = list(range(100))
expected_sum = sum(array) # 4950
shared_storage = {
'input_array': array
}
# Create nodes
chunk_node = ArrayChunkNode(chunk_size=10)
reduce_node = SumReduceNode()
# Connect nodes
chunk_node >> reduce_node
# Create and run pipeline
pipeline = Flow(start=chunk_node)
pipeline.run(shared_storage)
self.assertEqual(shared_storage['total'], expected_sum)
def test_uneven_chunks(self):
"""
Test that the map-reduce works correctly with array lengths
that don't divide evenly by chunk_size
"""
array = list(range(25))
expected_sum = sum(array) # 300
shared_storage = {
'input_array': array
}
chunk_node = ArrayChunkNode(chunk_size=10)
reduce_node = SumReduceNode()
chunk_node >> reduce_node
pipeline = Flow(start=chunk_node)
pipeline.run(shared_storage)
self.assertEqual(shared_storage['total'], expected_sum)
def test_custom_chunk_size(self):
"""
Test that the map-reduce works with different chunk sizes
"""
array = list(range(100))
expected_sum = sum(array)
shared_storage = {
'input_array': array
}
# Use chunk_size=15 instead of default 10
chunk_node = ArrayChunkNode(chunk_size=15)
reduce_node = SumReduceNode()
chunk_node >> reduce_node
pipeline = Flow(start=chunk_node)
pipeline.run(shared_storage)
self.assertEqual(shared_storage['total'], expected_sum)
def test_single_element_chunks(self):
"""
Test extreme case where chunk_size=1
"""
array = list(range(5))
expected_sum = sum(array)
shared_storage = {
'input_array': array
}
chunk_node = ArrayChunkNode(chunk_size=1)
reduce_node = SumReduceNode()
chunk_node >> reduce_node
pipeline = Flow(start=chunk_node)
pipeline.run(shared_storage)
self.assertEqual(shared_storage['total'], expected_sum)
def test_empty_array(self):
"""
Test edge case of empty input array
"""
shared_storage = {
'input_array': []
}
chunk_node = ArrayChunkNode(chunk_size=10)
reduce_node = SumReduceNode()
chunk_node >> reduce_node
pipeline = Flow(start=chunk_node)
pipeline.run(shared_storage)
self.assertEqual(shared_storage['total'], 0)
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_fall_back.py
================================================
import unittest
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pocketflow import Node, AsyncNode, Flow, AsyncFlow
class FallbackNode(Node):
def __init__(self, should_fail=True, max_retries=1):
super().__init__(max_retries=max_retries)
self.should_fail = should_fail
self.attempt_count = 0
def prep(self, shared_storage):
if 'results' not in shared_storage:
shared_storage['results'] = []
return None
def exec(self, prep_result):
self.attempt_count += 1
if self.should_fail:
raise ValueError("Intentional failure")
return "success"
def exec_fallback(self, prep_result, exc):
return "fallback"
def post(self, shared_storage, prep_result, exec_result):
shared_storage['results'].append({
'attempts': self.attempt_count,
'result': exec_result
})
class AsyncFallbackNode(AsyncNode):
def __init__(self, should_fail=True, max_retries=1):
super().__init__(max_retries=max_retries)
self.should_fail = should_fail
self.attempt_count = 0
async def prep_async(self, shared_storage):
if 'results' not in shared_storage:
shared_storage['results'] = []
return None
async def exec_async(self, prep_result):
self.attempt_count += 1
if self.should_fail:
raise ValueError("Intentional async failure")
return "success"
async def exec_fallback_async(self, prep_result, exc):
await asyncio.sleep(0.01) # Simulate async work
return "async_fallback"
async def post_async(self, shared_storage, prep_result, exec_result):
shared_storage['results'].append({
'attempts': self.attempt_count,
'result': exec_result
})
class TestExecFallback(unittest.TestCase):
def test_successful_execution(self):
"""Test that exec_fallback is not called when execution succeeds"""
shared_storage = {}
node = FallbackNode(should_fail=False)
result = node.run(shared_storage)
self.assertEqual(len(shared_storage['results']), 1)
self.assertEqual(shared_storage['results'][0]['attempts'], 1)
self.assertEqual(shared_storage['results'][0]['result'], "success")
def test_fallback_after_failure(self):
"""Test that exec_fallback is called after all retries are exhausted"""
shared_storage = {}
node = FallbackNode(should_fail=True, max_retries=2)
result = node.run(shared_storage)
self.assertEqual(len(shared_storage['results']), 1)
self.assertEqual(shared_storage['results'][0]['attempts'], 2)
self.assertEqual(shared_storage['results'][0]['result'], "fallback")
def test_fallback_in_flow(self):
"""Test that fallback works within a Flow"""
class ResultNode(Node):
def prep(self, shared_storage):
return shared_storage.get('results', [])
def exec(self, prep_result):
return prep_result
def post(self, shared_storage, prep_result, exec_result):
shared_storage['final_result'] = exec_result
return None
shared_storage = {}
fallback_node = FallbackNode(should_fail=True)
result_node = ResultNode()
fallback_node >> result_node
flow = Flow(start=fallback_node)
flow.run(shared_storage)
self.assertEqual(len(shared_storage['results']), 1)
self.assertEqual(shared_storage['results'][0]['result'], "fallback")
self.assertEqual(shared_storage['final_result'], [{'attempts': 1, 'result': 'fallback'}] )
def test_no_fallback_implementation(self):
"""Test that default fallback behavior raises the exception"""
class NoFallbackNode(Node):
def prep(self, shared_storage):
if 'results' not in shared_storage:
shared_storage['results'] = []
return None
def exec(self, prep_result):
raise ValueError("Test error")
def post(self, shared_storage, prep_result, exec_result):
shared_storage['results'].append({'result': exec_result})
return exec_result
shared_storage = {}
node = NoFallbackNode()
with self.assertRaises(ValueError):
node.run(shared_storage)
def test_retry_before_fallback(self):
"""Test that retries are attempted before calling fallback"""
shared_storage = {}
node = FallbackNode(should_fail=True, max_retries=3)
node.run(shared_storage)
self.assertEqual(len(shared_storage['results']), 1)
self.assertEqual(shared_storage['results'][0]['attempts'], 3)
self.assertEqual(shared_storage['results'][0]['result'], "fallback")
class TestAsyncExecFallback(unittest.TestCase):
def setUp(self):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
def tearDown(self):
self.loop.close()
def test_async_successful_execution(self):
"""Test that async exec_fallback is not called when execution succeeds"""
async def run_test():
shared_storage = {}
node = AsyncFallbackNode(should_fail=False)
await node.run_async(shared_storage)
return shared_storage
shared_storage = self.loop.run_until_complete(run_test())
self.assertEqual(len(shared_storage['results']), 1)
self.assertEqual(shared_storage['results'][0]['attempts'], 1)
self.assertEqual(shared_storage['results'][0]['result'], "success")
def test_async_fallback_after_failure(self):
"""Test that async exec_fallback is called after all retries are exhausted"""
async def run_test():
shared_storage = {}
node = AsyncFallbackNode(should_fail=True, max_retries=2)
await node.run_async(shared_storage)
return shared_storage
shared_storage = self.loop.run_until_complete(run_test())
self.assertEqual(len(shared_storage['results']), 1)
self.assertEqual(shared_storage['results'][0]['attempts'], 2)
self.assertEqual(shared_storage['results'][0]['result'], "async_fallback")
def test_async_fallback_in_flow(self):
"""Test that async fallback works within an AsyncFlow"""
class AsyncResultNode(AsyncNode):
async def prep_async(self, shared_storage):
return shared_storage['results'][-1]['result'] # Get last result
async def exec_async(self, prep_result):
return prep_result
async def post_async(self, shared_storage, prep_result, exec_result):
shared_storage['final_result'] = exec_result
return "done"
async def run_test():
shared_storage = {}
fallback_node = AsyncFallbackNode(should_fail=True)
result_node = AsyncResultNode()
fallback_node >> result_node
flow = AsyncFlow(start=fallback_node)
await flow.run_async(shared_storage)
return shared_storage
shared_storage = self.loop.run_until_complete(run_test())
self.assertEqual(len(shared_storage['results']), 1)
self.assertEqual(shared_storage['results'][0]['result'], "async_fallback")
self.assertEqual(shared_storage['final_result'], "async_fallback")
def test_async_no_fallback_implementation(self):
"""Test that default async fallback behavior raises the exception"""
class NoFallbackAsyncNode(AsyncNode):
async def prep_async(self, shared_storage):
if 'results' not in shared_storage:
shared_storage['results'] = []
return None
async def exec_async(self, prep_result):
raise ValueError("Test async error")
async def post_async(self, shared_storage, prep_result, exec_result):
shared_storage['results'].append({'result': exec_result})
return exec_result
async def run_test():
shared_storage = {}
node = NoFallbackAsyncNode()
await node.run_async(shared_storage)
with self.assertRaises(ValueError):
self.loop.run_until_complete(run_test())
def test_async_retry_before_fallback(self):
"""Test that retries are attempted before calling async fallback"""
async def run_test():
shared_storage = {}
node = AsyncFallbackNode(should_fail=True, max_retries=3)
result = await node.run_async(shared_storage)
return result, shared_storage
result, shared_storage = self.loop.run_until_complete(run_test())
self.assertEqual(len(shared_storage['results']), 1)
self.assertEqual(shared_storage['results'][0]['attempts'], 3)
self.assertEqual(shared_storage['results'][0]['result'], "async_fallback")
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_flow_basic.py
================================================
# tests/test_flow_basic.py
import unittest
import sys
from pathlib import Path
import warnings
sys.path.insert(0, str(Path(__file__).parent.parent))
from pocketflow import Node, Flow
# --- Node Definitions ---
# Nodes intended for default transitions (>>) should NOT return a specific
# action string from post. Let it return None by default.
# Nodes intended for conditional transitions (-) MUST return the action string.
class NumberNode(Node):
def __init__(self, number):
super().__init__()
self.number = number
def prep(self, shared_storage):
shared_storage['current'] = self.number
# post implicitly returns None - used for default transition
class AddNode(Node):
def __init__(self, number):
super().__init__()
self.number = number
def prep(self, shared_storage):
shared_storage['current'] += self.number
# post implicitly returns None - used for default transition
class MultiplyNode(Node):
def __init__(self, number):
super().__init__()
self.number = number
def prep(self, shared_storage):
shared_storage['current'] *= self.number
# post implicitly returns None - used for default transition
class CheckPositiveNode(Node):
# This node IS designed for conditional branching
def prep(self, shared_storage):
pass
def post(self, shared_storage, prep_result, proc_result):
# MUST return the specific action string for branching
if shared_storage['current'] >= 0:
return 'positive'
else:
return 'negative'
class NoOpNode(Node):
# Just a placeholder node
pass # post implicitly returns None
class EndSignalNode(Node):
# A node specifically to return a value when it's the end
def __init__(self, signal="finished"):
super().__init__()
self.signal = signal
def post(self, shared_storage, prep_result, exec_result):
return self.signal # Return a specific signal
# --- Test Class ---
class TestFlowBasic(unittest.TestCase):
def test_start_method_initialization(self):
"""Test initializing flow with start() after creation."""
shared_storage = {}
n1 = NumberNode(5)
pipeline = Flow()
pipeline.start(n1)
last_action = pipeline.run(shared_storage)
self.assertEqual(shared_storage['current'], 5)
# NumberNode.post returns None (default)
self.assertIsNone(last_action)
def test_start_method_chaining(self):
"""Test fluent chaining using start().next()..."""
shared_storage = {}
pipeline = Flow()
# Chain: NumberNode -> AddNode -> MultiplyNode
# All use default transitions (post returns None)
pipeline.start(NumberNode(5)).next(AddNode(3)).next(MultiplyNode(2))
last_action = pipeline.run(shared_storage)
self.assertEqual(shared_storage['current'], 16)
# Last node (MultiplyNode) post returns None
self.assertIsNone(last_action)
def test_sequence_with_rshift(self):
"""Test a simple linear pipeline using >>"""
shared_storage = {}
n1 = NumberNode(5)
n2 = AddNode(3)
n3 = MultiplyNode(2)
pipeline = Flow()
# All default transitions (post returns None)
pipeline.start(n1) >> n2 >> n3
last_action = pipeline.run(shared_storage)
self.assertEqual(shared_storage['current'], 16)
# Last node (n3: MultiplyNode) post returns None
self.assertIsNone(last_action)
def test_branching_positive(self):
"""Test positive branch: CheckPositiveNode returns 'positive'"""
shared_storage = {}
start_node = NumberNode(5) # post -> None
check_node = CheckPositiveNode() # post -> 'positive' or 'negative'
add_if_positive = AddNode(10) # post -> None
add_if_negative = AddNode(-20) # post -> None (won't run)
pipeline = Flow()
# start -> check (default); check branches on 'positive'/'negative'
pipeline.start(start_node) >> check_node
check_node - "positive" >> add_if_positive
check_node - "negative" >> add_if_negative
# Execution: start_node -> check_node -> add_if_positive
last_action = pipeline.run(shared_storage)
self.assertEqual(shared_storage['current'], 15) # 5 + 10
# Last node executed was add_if_positive, its post returns None
self.assertIsNone(last_action)
def test_branching_negative(self):
"""Test negative branch: CheckPositiveNode returns 'negative'"""
shared_storage = {}
start_node = NumberNode(-5) # post -> None
check_node = CheckPositiveNode() # post -> 'positive' or 'negative'
add_if_positive = AddNode(10) # post -> None (won't run)
add_if_negative = AddNode(-20) # post -> None
pipeline = Flow()
pipeline.start(start_node) >> check_node
check_node - "positive" >> add_if_positive
check_node - "negative" >> add_if_negative
# Execution: start_node -> check_node -> add_if_negative
last_action = pipeline.run(shared_storage)
self.assertEqual(shared_storage['current'], -25) # -5 + -20
# Last node executed was add_if_negative, its post returns None
self.assertIsNone(last_action)
def test_cycle_until_negative_ends_with_signal(self):
"""Test cycle, ending on a node that returns a signal"""
shared_storage = {}
n1 = NumberNode(10) # post -> None
check = CheckPositiveNode() # post -> 'positive' or 'negative'
subtract3 = AddNode(-3) # post -> None
end_node = EndSignalNode("cycle_done") # post -> "cycle_done"
pipeline = Flow()
pipeline.start(n1) >> check
# Branching from CheckPositiveNode
check - 'positive' >> subtract3
check - 'negative' >> end_node # End on negative branch
# After subtracting, go back to check (default transition)
subtract3 >> check
# Execution: n1->check->sub3->check->sub3->check->sub3->check->sub3->check->end_node
last_action = pipeline.run(shared_storage)
self.assertEqual(shared_storage['current'], -2) # 10 -> 7 -> 4 -> 1 -> -2
# Last node executed was end_node, its post returns "cycle_done"
self.assertEqual(last_action, "cycle_done")
def test_flow_ends_warning_default_missing(self):
"""Test warning when default transition is needed but not found"""
shared_storage = {}
# Node that returns a specific action from post
class ActionNode(Node):
def post(self, *args): return "specific_action"
start_node = ActionNode()
next_node = NoOpNode()
pipeline = Flow()
pipeline.start(start_node)
# Define successor only for the specific action
start_node - "specific_action" >> next_node
# Make start_node return None instead, triggering default search
start_node.post = lambda *args: None
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
# Run flow. start_node runs, post returns None.
# Flow looks for "default", but only "specific_action" exists.
last_action = pipeline.run(shared_storage)
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, UserWarning))
# Warning message should indicate "default" wasn't found
self.assertIn("Flow ends: 'None' not found in ['specific_action']", str(w[-1].message))
# Last action is from start_node's post
self.assertIsNone(last_action)
def test_flow_ends_warning_specific_missing(self):
"""Test warning when specific action is returned but not found"""
shared_storage = {}
# Node that returns a specific action from post
class ActionNode(Node):
def post(self, *args): return "specific_action"
start_node = ActionNode()
next_node = NoOpNode()
pipeline = Flow()
pipeline.start(start_node)
# Define successor only for "default"
start_node >> next_node # same as start_node.next(next_node, "default")
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
# Run flow. start_node runs, post returns "specific_action".
# Flow looks for "specific_action", but only "default" exists.
last_action = pipeline.run(shared_storage)
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, UserWarning))
# Warning message should indicate "specific_action" wasn't found
self.assertIn("Flow ends: 'specific_action' not found in ['default']", str(w[-1].message))
# Last action is from start_node's post
self.assertEqual(last_action, "specific_action")
if __name__ == '__main__':
unittest.main()
================================================
FILE: tests/test_flow_composition.py
================================================
# tests/test_flow_composition.py
import unittest
import asyncio # Keep import, might be needed if other tests use it indirectly
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from pocketflow import Node, Flow
# --- Existing Nodes ---
class NumberNode(Node):
def __init__(self, number):
super().__init__()
self.number = number
def prep(self, shared_storage):
shared_storage['current'] = self.number
# post implicitly returns None
class AddNode(Node):
def __init__(self, number):
super().__init__()
self.number = number
def prep(self, shared_storage):
shared_storage['current'] += self.number
# post implicitly returns None
class MultiplyNode(Node):
def __init__(self, number):
super().__init__()
self.number = number
def prep(self, shared_storage):
shared_storage['current'] *= self.number
# post implicitly returns None
# --- New Nodes for Action Propagation Test ---
class SignalNode(Node):
"""A node that returns a specific signal string from its post method."""
def __init__(self, signal="default_signal"):
super().__init__()
self.signal = signal
# No prep needed usually if just signaling
def post(self, shared_storage, prep_result, exec_result):
# Store the signal in shared storage for verification
shared_storage['last_signal_emitted'] = self.signal
return self.signal # Return the specific action string
class PathNode(Node):
"""A node to indicate which path was taken in the outer flow."""
def __init__(self, path_id):
super().__init__()
self.path_id = path_id
def prep(self, shared_storage):
shared_storage['path_taken'] = self.path_id
# post implicitly returns None
# --- Test Class ---
class TestFlowComposition(unittest.TestCase):
# --- Existing Tests (Unchanged) ---
def test_flow_as_node(self):
"""
1) Create a Flow (f1) starting with NumberNode(5), then AddNode(10), then MultiplyNode(2).
2) Create a second Flow (f2) whose start is f1.
3) Create a wrapper Flow (f3) that contains f2 to ensure proper execution.
Expected final result in shared_storage['current']: (5 + 10) * 2 = 30.
"""
shared_storage = {}
f1 = Flow(start=NumberNode(5))
f1 >> AddNode(10) >> MultiplyNode(2)
f2 = Flow(start=f1)
f3 = Flow(start=f2)
f3.run(shared_storage)
self.assertEqual(shared_storage['current'], 30)
def test_nested_flow(self):
"""
Demonstrates nested flows with proper wrapping:
inner_flow: NumberNode(5) -> AddNode(3)
middle_flow: starts with inner_flow -> MultiplyNode(4)
wrapper_flow: contains middle_flow to ensure proper execution
Expected final result: (5 + 3) * 4 = 32.
"""
shared_storage = {}
inner_flow = Flow(start=NumberNode(5))
inner_flow >> AddNode(3)
middle_flow = Flow(start=inner_flow)
middle_flow >> MultiplyNode(4)
wrapper_flow = Flow(start=middle_flow)
wrapper_flow.run(shared_storage)
self.assertEqual(shared_storage['current'], 32)
def test_flow_chaining_flows(self):
"""
Demonstrates chaining two flows with proper wrapping:
flow1: NumberNode(10) -> AddNode(10) # final = 20
flow2: MultiplyNode(2) # final = 40
wrapper_flow: contains both flow1 and flow2 to ensure proper execution
Expected final result: (10 + 10) * 2 = 40.
"""
shared_storage = {}
numbernode = NumberNode(10)
numbernode >> AddNode(10)
flow1 = Flow(start=numbernode)
flow2 = Flow(start=MultiplyNode(2))
flow1 >> flow2 # Default transition based on flow1 returning None
wrapper_flow = Flow(start=flow1)
wrapper_flow.run(shared_storage)
self.assertEqual(shared_storage['current'], 40)
def test_composition_with_action_propagation(self):
"""
Test that an outer flow can branch based on the action returned
by the last node's post() within an inner flow.
"""
shared_storage = {}
# 1. Define an inner flow that ends with a node returning a specific action
inner_start_node = NumberNode(100) # current = 100, post -> None
inner_end_node = SignalNode("inner_done") # post -> "inner_done"
inner_start_node >> inner_end_node
# Inner flow will execute start->end, and the Flow's execution will return "inner_done"
inner_flow = Flow(start=inner_start_node)
# 2. Define target nodes for the outer flow branches
path_a_node = PathNode("A") # post -> None
path_b_node = PathNode("B") # post -> None
# 3. Define the outer flow starting with the inner flow
outer_flow = Flow()
outer_flow.start(inner_flow) # Use the start() method
# 4. Define branches FROM the inner_flow object based on its returned action
inner_flow - "inner_done" >> path_b_node # This path should be taken
inner_flow - "other_action" >> path_a_node # This path should NOT be taken
# 5. Run the outer flow and capture the last action
# Execution: inner_start -> inner_end -> path_b
last_action_outer = outer_flow.run(shared_storage)
# 6. Assert the results
# Check state after inner flow execution
self.assertEqual(shared_storage.get('current'), 100)
self.assertEqual(shared_storage.get('last_signal_emitted'), "inner_done")
# Check that the correct outer path was taken
self.assertEqual(shared_storage.get('path_taken'), "B")
# Check the action returned by the outer flow. The last node executed was
# path_b_node, which returns None from its post method.
self.assertIsNone(last_action_outer)
if __name__ == '__main__':
unittest.main()
================================================
FILE: utils/update_pocketflow_mdc.py
================================================
#!/usr/bin/env python3
"""
Script to generate MDC files from the PocketFlow docs folder, creating one MDC file per MD file.
Usage:
python update_pocketflow_mdc.py [--docs-dir PATH] [--rules-dir PATH]
"""
import os
import re
import shutil
from pathlib import Path
import sys
import html.parser
class HTMLTagStripper(html.parser.HTMLParser):
"""HTML Parser subclass to strip HTML tags from content"""
def __init__(self):
super().__init__()
self.reset()
self.strict = False
self.convert_charrefs = True
self.text = []
def handle_data(self, data):
self.text.append(data)
def get_text(self):
return ''.join(self.text)
def strip_html_tags(html_content):
"""Remove HTML tags from content"""
stripper = HTMLTagStripper()
stripper.feed(html_content)
return stripper.get_text()
def extract_frontmatter(file_path):
"""Extract title, parent, and nav_order from markdown frontmatter"""
frontmatter = {}
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Extract frontmatter between --- markers
fm_match = re.search(r'^---\s*(.+?)\s*---', content, re.DOTALL)
if fm_match:
frontmatter_text = fm_match.group(1)
# Extract fields
title_match = re.search(r'title:\s*"?([^"\n]+)"?', frontmatter_text)
parent_match = re.search(r'parent:\s*"?([^"\n]+)"?', frontmatter_text)
nav_order_match = re.search(r'nav_order:\s*(\d+)', frontmatter_text)
if title_match:
frontmatter['title'] = title_match.group(1)
if parent_match:
frontmatter['parent'] = parent_match.group(1)
if nav_order_match:
frontmatter['nav_order'] = int(nav_order_match.group(1))
except Exception as e:
print(f"Error reading frontmatter from {file_path}: {e}")
return frontmatter
def extract_first_heading(file_path):
"""Extract the first heading from markdown content"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Remove frontmatter
content = re.sub(r'^---.*?---\s*', '', content, flags=re.DOTALL)
# Find first heading
heading_match = re.search(r'#\s+(.+)', content)
if heading_match:
return heading_match.group(1).strip()
except Exception as e:
print(f"Error extracting heading from {file_path}: {e}")
# Fallback to filename if no heading found
return Path(file_path).stem.replace('_', ' ').title()
def get_mdc_description(md_file, frontmatter, heading):
"""Generate a description for the MDC file based on file metadata"""
section = ""
subsection = ""
# Determine section from path
path_parts = Path(md_file).parts
if 'core_abstraction' in path_parts:
section = "Core Abstraction"
elif 'design_pattern' in path_parts:
section = "Design Pattern"
elif 'utility_function' in path_parts:
section = "Utility Function"
# Use frontmatter title or heading as subsection
if 'title' in frontmatter:
subsection = frontmatter['title']
else:
subsection = heading
# For the combined guide and index
if Path(md_file).name == "guide.md":
return "Guidelines for using PocketFlow, Agentic Coding"
# For index.md at root level, use a different format
if Path(md_file).name == "index.md" and section == "":
return "Guidelines for using PocketFlow, a minimalist LLM framework"
# For other files, create a more specific description
if section:
return f"Guidelines for using PocketFlow, {section}, {subsection}"
else:
return f"Guidelines for using PocketFlow, {subsection}"
def process_markdown_content(content, remove_local_refs=False):
"""Process markdown content to make it suitable for MDC file"""
# Remove frontmatter
content = re.sub(r'^---.*?---\s*', '', content, flags=re.DOTALL)
# Replace HTML div tags and their content
content = re.sub(r'.*?', '', content, flags=re.DOTALL)
if remove_local_refs:
# Replace markdown links to local documentation with just the text in brackets
# This prevents automatically including all docs when the file is loaded
# Keep the brackets around the text for better discoverability
content = re.sub(r'\[([^\]]+)\]\(\./[^)]+\)', r'[\1]', content)
else:
# Adjust relative links to maintain references within the docs structure
content = re.sub(r'\]\(\./([^)]+)\)', r'](mdc:./\1)', content)
# Ensure links to md files work correctly
content = re.sub(r'\]\(mdc:\./(.+?)\.md\)', r'](mdc:./\1.md)', content)
content = re.sub(r'\]\(mdc:\./(.+?)\.html\)', r'](mdc:./\1.md)', content)
# Strip remaining HTML tags
content = strip_html_tags(content)
return content
def get_documentation_first_policy():
"""Return the DOCUMENTATION FIRST POLICY text to be included in the guide"""
return """# DOCUMENTATION FIRST POLICY
**CRITICAL INSTRUCTION**: When implementing a Pocket Flow app:
1. **ALWAYS REQUEST MDC FILES FIRST** - Before writing any code, request and review all relevant MDC documentation files. This doc provides an explaination of the documents.
2. **UNDERSTAND THE FRAMEWORK** - Gain comprehensive understanding of the Pocket Flow framework from documentation
3. **AVOID ASSUMPTION-DRIVEN DEVELOPMENT** - Do not base your implementation on assumptions or guesswork. Even if the human didn't explicitly mention pocket flow in their request, if the code you are editing is using pocket flow, you should request relevant docs to help you understand best practice as well before editing.
**VERIFICATION**: Begin each implementation with a brief summary of the documentation you've reviewed to inform your approach.
"""
def generate_mdc_header(md_file, description, always_apply=False):
"""Generate MDC file header with appropriate frontmatter"""
# Determine if we should include globs
# For index.md and guide.md, we include **/*.py to provide high-level context for Python files
# For other files, leave it empty to be less intrusive
globs = "**/*.py" if always_apply else ""
return f"""---
description: {description}
globs: {globs}
alwaysApply: {"true" if always_apply else "false"}
---
"""
def has_substantive_content(content):
"""Check if the processed content has substantive content beyond the frontmatter"""
# Remove frontmatter
content_without_frontmatter = re.sub(r'^---.*?---\s*', '', content, flags=re.DOTALL)
# Remove whitespace and common HTML/markdown formatting
cleaned_content = re.sub(r'\s+', '', content_without_frontmatter)
cleaned_content = re.sub(r'{:.*?}', '', cleaned_content)
# If there's almost nothing left after cleaning, consider it empty
return len(cleaned_content) > 20 # Arbitrary threshold, adjust as needed
def create_combined_guide(docs_dir, rules_dir):
"""Create a combined guide that includes both the guide and index content"""
docs_path = Path(docs_dir)
rules_path = Path(rules_dir)
guide_file = docs_path / "guide.md"
index_file = docs_path / "index.md"
if not guide_file.exists() or not index_file.exists():
print("Warning: guide.md or index.md not found, skipping combined guide creation")
return False
# Get guide content and index content
with open(guide_file, 'r', encoding='utf-8') as f:
guide_content = f.read()
with open(index_file, 'r', encoding='utf-8') as f:
index_content = f.read()
# Process the content
processed_guide = process_markdown_content(guide_content, remove_local_refs=True)
processed_index = process_markdown_content(index_content, remove_local_refs=True)
# Get the documentation first policy
doc_first_policy = get_documentation_first_policy()
# Combine the content with the documentation first policy at the beginning
combined_content = doc_first_policy + processed_guide + "\n\n" + processed_index
# Generate the MDC header
description = "Guidelines for using PocketFlow, Agentic Coding"
mdc_header = generate_mdc_header(guide_file, description, always_apply=True)
# Combine header and processed content
mdc_content = mdc_header + combined_content
# Create the output path with the new filename
output_path = rules_path / "guide_for_pocketflow.mdc"
# Write the MDC file
with open(output_path, 'w', encoding='utf-8') as f:
f.write(mdc_content)
print(f"Created combined guide MDC file: {output_path}")
return True
def convert_md_to_mdc(md_file, output_dir, docs_dir, special_treatment=False):
"""Convert a markdown file to MDC format and save to the output directory"""
try:
print(f"Processing: {md_file}")
# Skip guide.md and index.md as they'll be handled separately
file_name = Path(md_file).name
if file_name in ["guide.md", "index.md"]:
print(f"Skipping {file_name} for individual processing - it will be included in the combined guide")
return True
# Skip empty index.md files in subfolders
parent_dir = Path(md_file).parent.name
# Check if this is an index.md in a subfolder (not the main index.md)
if (file_name == "index.md" and parent_dir != "docs" and
parent_dir in ["core_abstraction", "design_pattern", "utility_function"]):
# Read the content
with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()
# Skip if it doesn't have substantive content
if not has_substantive_content(content):
print(f"Skipping empty subfolder index: {md_file}")
return True
# Extract metadata from file
frontmatter = extract_frontmatter(md_file)
heading = extract_first_heading(md_file)
description = get_mdc_description(md_file, frontmatter, heading)
# Read the content
with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()
# Process the content
processed_content = process_markdown_content(content, remove_local_refs=special_treatment)
# Generate the MDC header
mdc_header = generate_mdc_header(md_file, description, always_apply=special_treatment)
# Combine header and processed content
mdc_content = mdc_header + processed_content
# Perform a final check to ensure the processed content is substantive
if not has_substantive_content(processed_content):
print(f"Skipping file with no substantive content after processing: {md_file}")
return True
# Get the path relative to the docs directory
rel_path = os.path.relpath(md_file, start=Path(docs_dir))
# Extract just the filename and directory structure without the 'docs/' prefix
path_parts = Path(rel_path).parts
if len(path_parts) > 1 and path_parts[0] == 'docs':
# Remove the 'docs/' prefix from the path
rel_path = os.path.join(*path_parts[1:])
# Create the output path
output_path = Path(output_dir) / rel_path
# Create output directory if it doesn't exist
output_path.parent.mkdir(parents=True, exist_ok=True)
# Change extension from .md to .mdc
output_path = output_path.with_suffix('.mdc')
# Write the MDC file
with open(output_path, 'w', encoding='utf-8') as f:
f.write(mdc_content)
print(f"Created MDC file: {output_path}")
return True
except Exception as e:
print(f"Error converting {md_file} to MDC: {e}")
return False
def generate_mdc_files(docs_dir, rules_dir):
"""Generate MDC files from all markdown files in the docs directory"""
docs_path = Path(docs_dir)
rules_path = Path(rules_dir)
# Make sure the docs directory exists
if not docs_path.exists() or not docs_path.is_dir():
raise ValueError(f"Directory not found: {docs_dir}")
print(f"Generating MDC files from docs in: {docs_dir}")
print(f"Output will be written to: {rules_dir}")
# Create the rules directory if it doesn't exist
rules_path.mkdir(parents=True, exist_ok=True)
# Create the combined guide file first (includes both guide.md and index.md)
create_combined_guide(docs_dir, rules_dir)
# Process all other markdown files
success_count = 0
failure_count = 0
# Find all markdown files
md_files = list(docs_path.glob("**/*.md"))
# Skip the main index.md and guide.md files as we've already processed them in create_combined_guide
md_files = [f for f in md_files if f.name != "index.md" and f.name != "guide.md"]
# Process each markdown file
for md_file in md_files:
if convert_md_to_mdc(md_file, rules_path, docs_dir):
success_count += 1
else:
failure_count += 1
print(f"\nProcessed {len(md_files) + 1} markdown files:") # +1 for the combined guide
print(f" - Successfully converted: {success_count + 1}") # +1 for the combined guide
print(f" - Failed conversions: {failure_count}")
return success_count > 0 and failure_count == 0
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Generate MDC files from PocketFlow docs")
# Get script directory
script_dir = Path(__file__).parent.absolute()
# Default to PocketFlow/docs directory relative to script location
default_docs_dir = (script_dir.parent / "docs").as_posix()
# Default rules directory - changed to .cursor/rules
default_rules_dir = (script_dir.parent / ".cursor" / "rules").as_posix()
parser.add_argument("--docs-dir",
default=default_docs_dir,
help="Path to PocketFlow docs directory")
parser.add_argument("--rules-dir",
default=default_rules_dir,
help="Output directory for MDC files")
args = parser.parse_args()
try:
success = generate_mdc_files(args.docs_dir, args.rules_dir)
sys.exit(0 if success else 1)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)