update the doc structure
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
---
|
||||
layout: default
|
||||
title: "Agent"
|
||||
parent: "Design Pattern"
|
||||
nav_order: 6
|
||||
---
|
||||
|
||||
# Agent
|
||||
|
||||
Agent is a powerful design pattern, where node can take dynamic actions based on the context it receives.
|
||||
To express an agent, create a Node (the agent) with [branching](../core_abstraction/flow.md) to other nodes (Actions).
|
||||
|
||||
> The core of build **performant** and **reliable** agents boils down to:
|
||||
>
|
||||
> 1. **Context Management:** Provide *clear, relevant context* so agents can understand the problem.E.g., Rather than dumping an entire chat history or entire files, use a [Workflow](./workflow.md) that filters out and includes only the most relevant information.
|
||||
>
|
||||
> 2. **Action Space:** Define *a well-structured, unambiguous, and easy-to-use* set of actions. For instance, avoid creating overlapping actions like `read_databases` and `read_csvs`. Instead, unify data sources (e.g., move CSVs into a database) and design a single action. The action can be parameterized (e.g., string for search) or programmable (e.g., SQL queries).
|
||||
{: .best-practice }
|
||||
|
||||
### 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?"})
|
||||
```
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
layout: default
|
||||
title: "Map Reduce"
|
||||
parent: "Design Pattern"
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
# 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 MapSummaries(BatchNode):
|
||||
def prep(self, shared): return [shared["text"][i:i+10000] for i in range(0, len(shared["text"]), 10000)]
|
||||
def exec(self, chunk): return call_llm(f"Summarize this chunk: {chunk}")
|
||||
def post(self, shared, prep_res, exec_res_list): shared["summaries"] = exec_res_list
|
||||
|
||||
class ReduceSummaries(Node):
|
||||
def prep(self, shared): return shared["summaries"]
|
||||
def exec(self, summaries): return call_llm(f"Combine these summaries: {summaries}")
|
||||
def post(self, shared, prep_res, exec_res): shared["final_summary"] = exec_res
|
||||
|
||||
# Connect nodes
|
||||
map_node = MapSummaries()
|
||||
reduce_node = ReduceSummaries()
|
||||
map_node >> reduce_node
|
||||
|
||||
# Create flow
|
||||
summarize_flow = Flow(start=map_node)
|
||||
summarize_flow.run(shared)
|
||||
```
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
layout: default
|
||||
title: "Chat Memory"
|
||||
parent: "Design Pattern"
|
||||
nav_order: 5
|
||||
---
|
||||
|
||||
# Chat Memory
|
||||
|
||||
Multi-turn conversations require memory management to maintain context while avoiding overwhelming the LLM.
|
||||
|
||||
### 1. Naive Approach: Full History
|
||||
|
||||
Sending the full chat history may overwhelm LLMs.
|
||||
|
||||
```python
|
||||
class ChatNode(Node):
|
||||
def prep(self, shared):
|
||||
if "history" not in shared:
|
||||
shared["history"] = []
|
||||
user_input = input("You: ")
|
||||
return shared["history"], user_input
|
||||
|
||||
def exec(self, inputs):
|
||||
history, user_input = inputs
|
||||
messages = [{"role": "system", "content": "You are a helpful assistant"}]
|
||||
for h in history:
|
||||
messages.append(h)
|
||||
messages.append({"role": "user", "content": user_input})
|
||||
response = call_llm(messages)
|
||||
return response
|
||||
|
||||
def post(self, shared, prep_res, exec_res):
|
||||
shared["history"].append({"role": "user", "content": prep_res[1]})
|
||||
shared["history"].append({"role": "assistant", "content": exec_res})
|
||||
return "continue"
|
||||
|
||||
chat = ChatNode()
|
||||
chat - "continue" >> chat
|
||||
flow = Flow(start=chat)
|
||||
```
|
||||
|
||||
### 2. Improved Memory Management
|
||||
|
||||
We can:
|
||||
1. Limit the chat history to the most recent 4.
|
||||
2. Use [vector search](./tool.md) to retrieve relevant exchanges beyond the last 4.
|
||||
|
||||
```python
|
||||
################################
|
||||
# Node A: Retrieve user input & relevant messages
|
||||
################################
|
||||
class ChatRetrieve(Node):
|
||||
def prep(self, s):
|
||||
s.setdefault("history", [])
|
||||
s.setdefault("memory_index", None)
|
||||
user_input = input("You: ")
|
||||
return user_input
|
||||
|
||||
def exec(self, user_input):
|
||||
emb = get_embedding(user_input)
|
||||
relevant = []
|
||||
if len(shared["history"]) > 8 and shared["memory_index"]:
|
||||
idx, _ = search_index(shared["memory_index"], emb, top_k=2)
|
||||
relevant = [shared["history"][i[0]] for i in idx]
|
||||
return (user_input, relevant)
|
||||
|
||||
def post(self, s, p, r):
|
||||
user_input, relevant = r
|
||||
s["user_input"] = user_input
|
||||
s["relevant"] = relevant
|
||||
return "continue"
|
||||
|
||||
################################
|
||||
# Node B: Call LLM, update history + index
|
||||
################################
|
||||
class ChatReply(Node):
|
||||
def prep(self, s):
|
||||
user_input = s["user_input"]
|
||||
recent = s["history"][-8:]
|
||||
relevant = s.get("relevant", [])
|
||||
return user_input, recent, relevant
|
||||
|
||||
def exec(self, inputs):
|
||||
user_input, recent, relevant = inputs
|
||||
msgs = [{"role":"system","content":"You are a helpful assistant."}]
|
||||
if relevant:
|
||||
msgs.append({"role":"system","content":f"Relevant: {relevant}"})
|
||||
msgs.extend(recent)
|
||||
msgs.append({"role":"user","content":user_input})
|
||||
ans = call_llm(msgs)
|
||||
return ans
|
||||
|
||||
def post(self, s, pre, ans):
|
||||
user_input, _, _ = pre
|
||||
s["history"].append({"role":"user","content":user_input})
|
||||
s["history"].append({"role":"assistant","content":ans})
|
||||
|
||||
# Manage memory index
|
||||
if len(s["history"]) == 8:
|
||||
embs = []
|
||||
for i in range(0, 8, 2):
|
||||
text = s["history"][i]["content"] + " " + s["history"][i+1]["content"]
|
||||
embs.append(get_embedding(text))
|
||||
s["memory_index"] = create_index(embs)
|
||||
elif len(s["history"]) > 8:
|
||||
text = s["history"][-2]["content"] + " " + s["history"][-1]["content"]
|
||||
new_emb = np.array([get_embedding(text)]).astype('float32')
|
||||
s["memory_index"].add(new_emb)
|
||||
|
||||
print(f"Assistant: {ans}")
|
||||
return "continue"
|
||||
|
||||
################################
|
||||
# Flow wiring
|
||||
################################
|
||||
retrieve = ChatRetrieve()
|
||||
reply = ChatReply()
|
||||
retrieve - "continue" >> reply
|
||||
reply - "continue" >> retrieve
|
||||
|
||||
flow = Flow(start=retrieve)
|
||||
shared = {}
|
||||
flow.run(shared)
|
||||
```
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
layout: default
|
||||
title: "(Advanced) Multi-Agents"
|
||||
parent: "Design Pattern"
|
||||
nav_order: 7
|
||||
---
|
||||
|
||||
# (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!
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
layout: default
|
||||
title: "Design Pattern"
|
||||
nav_order: 4
|
||||
has_children: true
|
||||
---
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
layout: default
|
||||
title: "RAG"
|
||||
parent: "Design Pattern"
|
||||
nav_order: 4
|
||||
---
|
||||
|
||||
# RAG (Retrieval Augmented Generation)
|
||||
|
||||
For certain LLM tasks like answering questions, providing context is essential.
|
||||
Use [vector search](../utility_function/tool.md) to find relevant context for LLM responses.
|
||||
|
||||
### Example: Question Answering
|
||||
|
||||
```python
|
||||
class PrepareEmbeddings(Node):
|
||||
def prep(self, shared):
|
||||
return shared["texts"]
|
||||
|
||||
def exec(self, texts):
|
||||
# Embed each text chunk
|
||||
embs = [get_embedding(t) for t in texts]
|
||||
return embs
|
||||
|
||||
def post(self, shared, prep_res, exec_res):
|
||||
shared["search_index"] = create_index(exec_res)
|
||||
# no action string means "default"
|
||||
|
||||
class AnswerQuestion(Node):
|
||||
def prep(self, shared):
|
||||
question = input("Enter question: ")
|
||||
return question
|
||||
|
||||
def exec(self, question):
|
||||
q_emb = get_embedding(question)
|
||||
idx, _ = search_index(shared["search_index"], q_emb, top_k=1)
|
||||
best_id = idx[0][0]
|
||||
relevant_text = shared["texts"][best_id]
|
||||
prompt = f"Question: {question}\nContext: {relevant_text}\nAnswer:"
|
||||
return call_llm(prompt)
|
||||
|
||||
def post(self, shared, p, answer):
|
||||
print("Answer:", answer)
|
||||
|
||||
############################################
|
||||
# Wire up the flow
|
||||
prep = PrepareEmbeddings()
|
||||
qa = AnswerQuestion()
|
||||
prep >> qa
|
||||
|
||||
flow = Flow(start=prep)
|
||||
|
||||
# Example usage
|
||||
shared = {"texts": ["I love apples", "Cats are great", "The sky is blue"]}
|
||||
flow.run(shared)
|
||||
```
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
layout: default
|
||||
title: "Structured Output"
|
||||
parent: "Design Pattern"
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# 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`.
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
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 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).
|
||||
Reference in New Issue
Block a user