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 ================================================
Pocket Flow – 100-line minimalist LLM framework
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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](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!
| | **Abstraction** | **App-Specific Wrappers** | **Vendor-Specific Wrappers** | **Lines** | **Size** | |----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:| | LangChain | Agent, Chain | Many
(e.g., QA, Summarization) | Many
(e.g., OpenAI, Pinecone, etc.) | 405K | +166MB | | CrewAI | Agent, Chain | Many
(e.g., FileReadTool, SerperDevTool) | Many
(e.g., OpenAI, Anthropic, Pinecone, etc.) | 18K | +173MB | | SmolAgent | Agent | Some
(e.g., CodeAgent, VisitWebTool) | Some
(e.g., DuckDuckGo, Hugging Face, etc.) | 8K | +198MB | | LangGraph | Agent, Graph | Some
(e.g., Semantic Search) | Some
(e.g., PostgresStore, SqliteSaver, etc.) | 37K | +51MB | | AutoGen | Agent | Some
(e.g., Tool Agent, Chat Agent) | Many [Optional]
(e.g., OpenAI, Pinecone, etc.)
| 7K
(core-only) | +26MB
(core-only) | | **PocketFlow** | **Graph** | **None** | **None** | **100** | **+56KB** |
## 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 ================================================
Pocket Flow – 100-line minimalist LLM framework
[English](https://github.com/The-Pocket/PocketFlow/blob/main/README.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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](https://the-pocket.github.io/PocketFlow/) Pocket Flow 是一个[100行代码](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)的极简主义LLM框架 - **轻量级**:仅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)**:让AI智能体(例如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)。 - 有问题?查看这个[AI助手](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框架过于臃肿... 你只需要100行代码就能构建LLM框架!
| | **抽象** | **应用特定包装器** | **供应商特定包装器** | **代码行数** | **大小** | |----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:| | 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 | 一些
(例如,语义搜索) | 一些
(例如,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框架的核心抽象:图(Graph)!

从这里开始,很容易实现流行的设计模式,如([多-](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更新的人工审核循环的最小Web服务 |
👀 想看其他入门教程?[创建一个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) | [Flow代码](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) | [Flow代码](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)
[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) | [Flow代码](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py) | [Youtube摘要器](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple)
像你5岁一样向你解释YouTube视频 | ★☆☆
*初级* | [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) | [Flow代码](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)
[Web搜索](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [设计文档](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Flow代码](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_FRENCH.md ================================================
Pocket Flow – framework LLM minimaliste en 100 lignes
[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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](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 !
| | **Abstraction** | **Wrappers spécifiques aux applications** | **Wrappers spécifiques aux fournisseurs** | **Lignes** | **Taille** | |----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:| | LangChain | Agent, Chain | Nombreux
(ex., QA, Résumé) | Nombreux
(ex., OpenAI, Pinecone, etc.) | 405K | +166MB | | CrewAI | Agent, Chain | Nombreux
(ex., FileReadTool, SerperDevTool) | Nombreux
(ex., OpenAI, Anthropic, Pinecone, etc.) | 18K | +173MB | | SmolAgent | Agent | Quelques
(ex., CodeAgent, VisitWebTool) | Quelques
(ex., DuckDuckGo, Hugging Face, etc.) | 8K | +198MB | | LangGraph | Agent, Graph | Quelques
(ex., Recherche Sémantique) | Quelques
(ex., PostgresStore, SqliteSaver, etc.) | 37K | +51MB | | AutoGen | Agent | Quelques
(ex., Tool Agent, Chat Agent) | Nombreux [Optionnel]
(ex., OpenAI, Pinecone, etc.)
| 7K
(core-only) | +26MB
(core-only) | | **PocketFlow** | **Graph** | **Aucun** | **Aucun** | **100** | **+56KB** |
## 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 ================================================
Pocket Flow – 100-Zeilen minimalistisches LLM-Framework
[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) ![Lizenz: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](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!
| | **Abstraktion** | **App-spezifische Wrapper** | **Anbieter-spezifische Wrapper** | **Zeilen** | **Größe** | |----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:| | LangChain | Agent, Chain | Viele
(z.B. QA, Zusammenfassung) | Viele
(z.B. OpenAI, Pinecone, etc.) | 405K | +166MB | | CrewAI | Agent, Chain | Viele
(z.B. FileReadTool, SerperDevTool) | Viele
(z.B. OpenAI, Anthropic, Pinecone, etc.) | 18K | +173MB | | SmolAgent | Agent | Einige
(z.B. CodeAgent, VisitWebTool) | Einige
(z.B. DuckDuckGo, Hugging Face, etc.) | 8K | +198MB | | LangGraph | Agent, Graph | Einige
(z.B. Semantic Search) | Einige
(z.B. PostgresStore, SqliteSaver, etc.) | 37K | +51MB | | AutoGen | Agent | Einige
(z.B. Tool Agent, Chat Agent) | Viele [Optional]
(z.B. OpenAI, Pinecone, etc.)
| 7K
(nur Kern) | +26MB
(nur Kern) | | **PocketFlow** | **Graph** | **Keine** | **Keine** | **100** | **+56KB** |
## 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行のミニマリストLLMフレームワーク
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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](https://the-pocket.github.io/PocketFlow/) Pocket Flowは[たった100行](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)のミニマリストLLMフレームワークです - **軽量**: わずか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)**: AIエージェント(例: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)をお読みください。 - 質問がありますか?この[AIアシスタント](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant)をチェックするか、[問題を作成してください!](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 | エージェント、チェーン | 多数
(例:QA、要約) | 多数
(例:OpenAI、Pineconeなど) | 405K | +166MB | | CrewAI | エージェント、チェーン | 多数
(例:FileReadTool、SerperDevTool) | 多数
(例:OpenAI、Anthropic、Pineconeなど) | 18K | +173MB | | SmolAgent | エージェント | 一部
(例:CodeAgent、VisitWebTool) | 一部
(例:DuckDuckGo、Hugging Faceなど) | 8K | +198MB | | LangGraph | エージェント、グラフ | 一部
(例:セマンティック検索) | 一部
(例:PostgresStore、SqliteSaverなど) | 37K | +51MB | | AutoGen | エージェント | 一部
(例:ツールエージェント、チャットエージェント) | 多数 [オプション]
(例:OpenAI、Pineconeなど)
| 7K
(コアのみ) | +26MB
(コアのみ) | | **PocketFlow** | **グラフ** | **なし** | **なし** | **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) | ☆☆☆
*超簡単* | マークダウンコンテンツを複数の言語に翻訳するバッチプロセッサ | | [ストリーミング](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) | ★☆☆
*初級* | 2つのエージェント間の非同期通信のためのタブーワードゲーム | | [スーパーバイザー](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更新を備えた人間レビューループのためのミニマルなウェブサービス |
👀 他の超初心者向けチュートリアルを見たいですか?[問題を作成してください!](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) | [Youtubeサマライザー](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_KOREAN.md ================================================
Pocket Flow – 100-line minimalist LLM framework
[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_FRENCH.md) | 한국어 ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](https://the-pocket.github.io/PocketFlow/) Pocket Flow는 [100줄](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)의 미니멀리스트 LLM 프레임워크입니다 - **경량화**: 단 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)**: AI 에이전트(예: 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)를 읽어보세요. - 질문이 있으신가요? [AI 어시스턴트](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant)를 확인하거나, [이슈를 생성하세요!](https://github.com/The-Pocket/PocketFlow/issues/new) - 🎉 Pocket Flow로 개발하는 다른 개발자들과 소통하려면 [Discord](https://discord.gg/hUHHE9Sa6T)에 가입하세요! - 🎉 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) | ☆☆☆
*초보* | 마크다운 콘텐츠를 여러 언어로 번역하는 배치 프로세서 | | [스트리밍](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 ================================================
Pocket Flow – 100-line minimalist LLM framework
[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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](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!
| | **Abstração** | **Wrappers Específicos para Aplicações** | **Wrappers Específicos para Fornecedores** | **Linhas** | **Tamanho** | |----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:| | LangChain | Agente, Cadeia | Muitos
(ex: QA, Summarization) | Muitos
(ex: OpenAI, Pinecone, etc.) | 405K | +166MB | | CrewAI | Agente, Cadeia | Muitos
(ex: FileReadTool, SerperDevTool) | Muitos
(ex: OpenAI, Anthropic, Pinecone, etc.) | 18K | +173MB | | SmolAgent | Agente | Alguns
(ex: CodeAgent, VisitWebTool) | Alguns
(ex: DuckDuckGo, Hugging Face, etc.) | 8K | +198MB | | LangGraph | Agente, Grafo | Alguns
(ex: Semantic Search) | Alguns
(ex: PostgresStore, SqliteSaver, etc.) | 37K | +51MB | | AutoGen | Agente | Alguns
(ex: Tool Agent, Chat Agent) | Muitos [Opcional]
(ex: OpenAI, Pinecone, etc.)
| 7K
(somente core) | +26MB
(somente core) | | **PocketFlow** | **Grafo** | **Nenhum** | **Nenhum** | **100** | **+56KB** |
## 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 ================================================
Pocket Flow – 100-line minimalist LLM framework
[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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](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 ================================================
Pocket Flow – 100-line minimalist LLM framework
[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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](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!
| | **Abstracción** | **Envolturas Específicas de Aplicación** | **Envolturas Específicas de Proveedor** | **Líneas** | **Tamaño** | |----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:| | LangChain | Agente, Cadena | Muchas
(p.ej., QA, Resumen) | Muchas
(p.ej., OpenAI, Pinecone, etc.) | 405K | +166MB | | CrewAI | Agente, Cadena | Muchas
(p.ej., FileReadTool, SerperDevTool) | Muchas
(p.ej., OpenAI, Anthropic, Pinecone, etc.) | 18K | +173MB | | SmolAgent | Agente | Algunas
(p.ej., CodeAgent, VisitWebTool) | Algunas
(p.ej., DuckDuckGo, Hugging Face, etc.) | 8K | +198MB | | LangGraph | Agente, Grafo | Algunas
(p.ej., Búsqueda Semántica) | Algunas
(p.ej., PostgresStore, SqliteSaver, etc.) | 37K | +51MB | | AutoGen | Agente | Algunas
(p.ej., Tool Agent, Chat Agent) | Muchas [Opcional]
(p.ej., OpenAI, Pinecone, etc.)
| 7K
(solo-núcleo) | +26MB
(solo-núcleo) | | **PocketFlow** | **Grafo** | **Ninguna** | **Ninguna** | **100** | **+56KB** |
## ¿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

Generate engaging articles with AI assistance

Popular Topics

AI Safety Climate Change Space Exploration Renewable Energy Mental Health Future of Work
================================================ FILE: cookbook/pocketflow-fastapi-background/static/progress.html ================================================ Generating Article...
Generating your article...
0%
Initializing...
1
Outline
2
Content
3
Style
Getting started...
Preparing to generate your article. This may take a few moments.

Your Article is Ready! 🎉

Generate Another
================================================ 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.
================================================ 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 ![flow visualization](./assets/flow_visualization.png) ## Sample Output Here's an example of book hotel: ![book hotel](./assets/book_hotel.png) Here's an example of changing intention mid-conversation: ![change intention](./assets/change_intention.png) ## 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 ================================================
Pocket Flow – 100-line minimalist LLM framework
[English](https://github.com/The-Pocket/PocketFlow/blob/main/README.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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](https://the-pocket.github.io/PocketFlow/) Pocket Flow 是一个[100行代码](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)的极简主义LLM框架 - **轻量级**:仅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)**:让AI智能体(例如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)。 - 有问题?查看这个[AI助手](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框架过于臃肿... 你只需要100行代码就能构建LLM框架!
| | **抽象** | **应用特定包装器** | **供应商特定包装器** | **代码行数** | **大小** | |----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:| | 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 | 一些
(例如,语义搜索) | 一些
(例如,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框架的核心抽象:图(Graph)!

从这里开始,很容易实现流行的设计模式,如([多-](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更新的人工审核循环的最小Web服务 |
👀 想看其他入门教程?[创建一个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) | [Flow代码](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) | [Flow代码](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)
[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) | [Flow代码](https://github.com/The-Pocket/Tutorial-AI-Paul-Graham/blob/main/flow.py) | [Youtube摘要器](https://github.com/The-Pocket/Tutorial-Youtube-Made-Simple)
像你5岁一样向你解释YouTube视频 | ★☆☆
*初级* | [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) | [Flow代码](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)
[Web搜索](https://the-pocket.github.io/PocketFlow/utility_function/websearch.html) | [设计文档](https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization/blob/master/docs/design.md) | [Flow代码](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_FRENCH.md ================================================
Pocket Flow – framework LLM minimaliste en 100 lignes
[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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](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 !
| | **Abstraction** | **Wrappers spécifiques aux applications** | **Wrappers spécifiques aux fournisseurs** | **Lignes** | **Taille** | |----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:| | LangChain | Agent, Chain | Nombreux
(ex., QA, Résumé) | Nombreux
(ex., OpenAI, Pinecone, etc.) | 405K | +166MB | | CrewAI | Agent, Chain | Nombreux
(ex., FileReadTool, SerperDevTool) | Nombreux
(ex., OpenAI, Anthropic, Pinecone, etc.) | 18K | +173MB | | SmolAgent | Agent | Quelques
(ex., CodeAgent, VisitWebTool) | Quelques
(ex., DuckDuckGo, Hugging Face, etc.) | 8K | +198MB | | LangGraph | Agent, Graph | Quelques
(ex., Recherche Sémantique) | Quelques
(ex., PostgresStore, SqliteSaver, etc.) | 37K | +51MB | | AutoGen | Agent | Quelques
(ex., Tool Agent, Chat Agent) | Nombreux [Optionnel]
(ex., OpenAI, Pinecone, etc.)
| 7K
(core-only) | +26MB
(core-only) | | **PocketFlow** | **Graph** | **Aucun** | **Aucun** | **100** | **+56KB** |
## 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 ================================================
Pocket Flow – 100-Zeilen minimalistisches LLM-Framework
[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) ![Lizenz: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](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!
| | **Abstraktion** | **App-spezifische Wrapper** | **Anbieter-spezifische Wrapper** | **Zeilen** | **Größe** | |----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:| | LangChain | Agent, Chain | Viele
(z.B. QA, Zusammenfassung) | Viele
(z.B. OpenAI, Pinecone, etc.) | 405K | +166MB | | CrewAI | Agent, Chain | Viele
(z.B. FileReadTool, SerperDevTool) | Viele
(z.B. OpenAI, Anthropic, Pinecone, etc.) | 18K | +173MB | | SmolAgent | Agent | Einige
(z.B. CodeAgent, VisitWebTool) | Einige
(z.B. DuckDuckGo, Hugging Face, etc.) | 8K | +198MB | | LangGraph | Agent, Graph | Einige
(z.B. Semantic Search) | Einige
(z.B. PostgresStore, SqliteSaver, etc.) | 37K | +51MB | | AutoGen | Agent | Einige
(z.B. Tool Agent, Chat Agent) | Viele [Optional]
(z.B. OpenAI, Pinecone, etc.)
| 7K
(nur Kern) | +26MB
(nur Kern) | | **PocketFlow** | **Graph** | **Keine** | **Keine** | **100** | **+56KB** |
## 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行のミニマリストLLMフレームワーク
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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](https://the-pocket.github.io/PocketFlow/) Pocket Flowは[たった100行](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)のミニマリストLLMフレームワークです - **軽量**: わずか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)**: AIエージェント(例: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)をお読みください。 - 質問がありますか?この[AIアシスタント](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant)をチェックするか、[問題を作成してください!](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 | エージェント、チェーン | 多数
(例:QA、要約) | 多数
(例:OpenAI、Pineconeなど) | 405K | +166MB | | CrewAI | エージェント、チェーン | 多数
(例:FileReadTool、SerperDevTool) | 多数
(例:OpenAI、Anthropic、Pineconeなど) | 18K | +173MB | | SmolAgent | エージェント | 一部
(例:CodeAgent、VisitWebTool) | 一部
(例:DuckDuckGo、Hugging Faceなど) | 8K | +198MB | | LangGraph | エージェント、グラフ | 一部
(例:セマンティック検索) | 一部
(例:PostgresStore、SqliteSaverなど) | 37K | +51MB | | AutoGen | エージェント | 一部
(例:ツールエージェント、チャットエージェント) | 多数 [オプション]
(例:OpenAI、Pineconeなど)
| 7K
(コアのみ) | +26MB
(コアのみ) | | **PocketFlow** | **グラフ** | **なし** | **なし** | **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) | ☆☆☆
*超簡単* | マークダウンコンテンツを複数の言語に翻訳するバッチプロセッサ | | [ストリーミング](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) | ★☆☆
*初級* | 2つのエージェント間の非同期通信のためのタブーワードゲーム | | [スーパーバイザー](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更新を備えた人間レビューループのためのミニマルなウェブサービス |
👀 他の超初心者向けチュートリアルを見たいですか?[問題を作成してください!](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) | [Youtubeサマライザー](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_KOREAN.md ================================================
Pocket Flow – 100-line minimalist LLM framework
[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_FRENCH.md) | 한국어 ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](https://the-pocket.github.io/PocketFlow/) Pocket Flow는 [100줄](https://github.com/The-Pocket/PocketFlow/blob/main/pocketflow/__init__.py)의 미니멀리스트 LLM 프레임워크입니다 - **경량화**: 단 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)**: AI 에이전트(예: 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)를 읽어보세요. - 질문이 있으신가요? [AI 어시스턴트](https://chatgpt.com/g/g-677464af36588191b9eba4901946557b-pocket-flow-assistant)를 확인하거나, [이슈를 생성하세요!](https://github.com/The-Pocket/PocketFlow/issues/new) - 🎉 Pocket Flow로 개발하는 다른 개발자들과 소통하려면 [Discord](https://discord.gg/hUHHE9Sa6T)에 가입하세요! - 🎉 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) | ☆☆☆
*초보* | 마크다운 콘텐츠를 여러 언어로 번역하는 배치 프로세서 | | [스트리밍](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 ================================================
Pocket Flow – 100-line minimalist LLM framework
[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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](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!
| | **Abstração** | **Wrappers Específicos para Aplicações** | **Wrappers Específicos para Fornecedores** | **Linhas** | **Tamanho** | |----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:| | LangChain | Agente, Cadeia | Muitos
(ex: QA, Summarization) | Muitos
(ex: OpenAI, Pinecone, etc.) | 405K | +166MB | | CrewAI | Agente, Cadeia | Muitos
(ex: FileReadTool, SerperDevTool) | Muitos
(ex: OpenAI, Anthropic, Pinecone, etc.) | 18K | +173MB | | SmolAgent | Agente | Alguns
(ex: CodeAgent, VisitWebTool) | Alguns
(ex: DuckDuckGo, Hugging Face, etc.) | 8K | +198MB | | LangGraph | Agente, Grafo | Alguns
(ex: Semantic Search) | Alguns
(ex: PostgresStore, SqliteSaver, etc.) | 37K | +51MB | | AutoGen | Agente | Alguns
(ex: Tool Agent, Chat Agent) | Muitos [Opcional]
(ex: OpenAI, Pinecone, etc.)
| 7K
(somente core) | +26MB
(somente core) | | **PocketFlow** | **Grafo** | **Nenhum** | **Nenhum** | **100** | **+56KB** |
## 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 ================================================
Pocket Flow – 100-line minimalist LLM framework
[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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](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 ================================================
Pocket Flow – 100-line minimalist LLM framework
[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) ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg) [![Docs](https://img.shields.io/badge/docs-latest-blue)](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!
| | **Abstracción** | **Envolturas Específicas de Aplicación** | **Envolturas Específicas de Proveedor** | **Líneas** | **Tamaño** | |----------------|:-----------------------------: |:-----------------------------------------------------------:|:------------------------------------------------------------:|:---------------:|:----------------------------:| | LangChain | Agente, Cadena | Muchas
(p.ej., QA, Resumen) | Muchas
(p.ej., OpenAI, Pinecone, etc.) | 405K | +166MB | | CrewAI | Agente, Cadena | Muchas
(p.ej., FileReadTool, SerperDevTool) | Muchas
(p.ej., OpenAI, Anthropic, Pinecone, etc.) | 18K | +173MB | | SmolAgent | Agente | Algunas
(p.ej., CodeAgent, VisitWebTool) | Algunas
(p.ej., DuckDuckGo, Hugging Face, etc.) | 8K | +198MB | | LangGraph | Agente, Grafo | Algunas
(p.ej., Búsqueda Semántica) | Algunas
(p.ej., PostgresStore, SqliteSaver, etc.) | 37K | +51MB | | AutoGen | Agente | Algunas
(p.ej., Tool Agent, Chat Agent) | Muchas [Opcional]
(p.ej., OpenAI, Pinecone, etc.)
| 7K
(solo-núcleo) | +26MB
(solo-núcleo) | | **PocketFlow** | **Grafo** | **Ninguna** | **Ninguna** | **100** | **+56KB** |
## ¿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. ![alt text](screenshots/chrome_2025-06-27_12-05-28.png) Detailed tracing for a node. ![langfuse](screenshots/chrome_2025-06-27_12-07-56.png) ## 🛠️ 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", "Cookbook: Pocket Flow + Cursor AI\n", "

" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

\n", "1. Utility Function\n", "

\n", "\n", "

\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", "
  1. \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", "
  2. \n", "\n", "
  3. \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", "
  4. \n", "\n", "
  5. \n", "

    \n", " post(shared, prep_res, exec_res)\n", "

    \n", "

    \n", " - Writes data back to the shared store.\n", "

    \n", "
  6. \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", "
  • \n", " Directed Branching \n", " (node_1 - \"action\" ->> node_2): \n", " Agentic decisions—where a Node’s \n", " post() return the action string.\n", "
  • \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.
Pocket Flow – 100-line minimalist LLM framework
## 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.
Pocket Flow – Core Abstraction
## 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.
Pocket Flow – Design Pattern
## 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)