update the doc structure
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
layout: default
|
||||
title: "Utility Function"
|
||||
nav_order: 3
|
||||
has_children: true
|
||||
---
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
layout: default
|
||||
title: "LLM Wrapper"
|
||||
parent: "Utility Function"
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# LLM Wrappers
|
||||
|
||||
We **don't** provide built-in LLM wrappers. Instead, please implement your own, for example by asking an assistant like ChatGPT or Claude. If you ask ChatGPT to "implement a `call_llm` function that takes a prompt and returns the LLM response," you shall get something like:
|
||||
|
||||
```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.
|
||||
{: .note }
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Why Not Provide Built-in LLM Wrappers?
|
||||
I believe it is a **bad practice** to provide LLM-specific implementations in a general framework:
|
||||
- **LLM APIs change frequently**. Hardcoding them makes maintenance a nightmare.
|
||||
- You may need **flexibility** to switch vendors, use fine-tuned models, or deploy local LLMs.
|
||||
- You may need **optimizations** like prompt caching, request batching, or response streaming.
|
||||
@@ -0,0 +1,216 @@
|
||||
---
|
||||
layout: default
|
||||
title: "Tool"
|
||||
parent: "Utility Function"
|
||||
nav_order: 2
|
||||
---
|
||||
|
||||
# Tool
|
||||
|
||||
Similar to LLM wrappers, we **don't** provide built-in tools. Here, we recommend some *minimal* (and incomplete) implementations of commonly used tools. These examples can serve as a starting point for your own tooling.
|
||||
|
||||
---
|
||||
|
||||
## 1. Embedding Calls
|
||||
|
||||
```python
|
||||
def get_embedding(text):
|
||||
from openai import OpenAI
|
||||
client = OpenAI(api_key="YOUR_API_KEY_HERE")
|
||||
r = client.embeddings.create(
|
||||
model="text-embedding-ada-002",
|
||||
input=text
|
||||
)
|
||||
return r.data[0].embedding
|
||||
|
||||
get_embedding("What's the meaning of life?")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Vector Database (Faiss)
|
||||
|
||||
```python
|
||||
import faiss
|
||||
import numpy as np
|
||||
|
||||
def create_index(embeddings):
|
||||
dim = len(embeddings[0])
|
||||
index = faiss.IndexFlatL2(dim)
|
||||
index.add(np.array(embeddings).astype('float32'))
|
||||
return index
|
||||
|
||||
def search_index(index, query_embedding, top_k=5):
|
||||
D, I = index.search(
|
||||
np.array([query_embedding]).astype('float32'),
|
||||
top_k
|
||||
)
|
||||
return I, D
|
||||
|
||||
index = create_index(embeddings)
|
||||
search_index(index, query_embedding)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Local Database
|
||||
|
||||
```python
|
||||
import sqlite3
|
||||
|
||||
def execute_sql(query):
|
||||
conn = sqlite3.connect("mydb.db")
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(query)
|
||||
result = cursor.fetchall()
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return result
|
||||
```
|
||||
|
||||
|
||||
> ⚠️ Beware of SQL injection risk
|
||||
{: .warning }
|
||||
|
||||
---
|
||||
|
||||
## 4. Python Function Execution
|
||||
|
||||
```python
|
||||
def run_code(code_str):
|
||||
env = {}
|
||||
exec(code_str, env)
|
||||
return env
|
||||
|
||||
run_code("print('Hello, world!')")
|
||||
```
|
||||
|
||||
> ⚠️ exec() is dangerous with untrusted input
|
||||
{: .warning }
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 5. PDF Extraction
|
||||
|
||||
If your PDFs are text-based, use PyMuPDF:
|
||||
|
||||
```python
|
||||
import fitz # PyMuPDF
|
||||
|
||||
def extract_text(pdf_path):
|
||||
doc = fitz.open(pdf_path)
|
||||
text = ""
|
||||
for page in doc:
|
||||
text += page.get_text()
|
||||
doc.close()
|
||||
return text
|
||||
|
||||
extract_text("document.pdf")
|
||||
```
|
||||
|
||||
For image-based PDFs (e.g., scanned), OCR is needed. A easy and fast option is using an LLM with vision capabilities:
|
||||
|
||||
```python
|
||||
from openai import OpenAI
|
||||
import base64
|
||||
|
||||
def call_llm_vision(prompt, image_data):
|
||||
client = OpenAI(api_key="YOUR_API_KEY_HERE")
|
||||
img_base64 = base64.b64encode(image_data).decode('utf-8')
|
||||
|
||||
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
|
||||
|
||||
pdf_document = fitz.open("document.pdf")
|
||||
page_num = 0
|
||||
page = pdf_document[page_num]
|
||||
pix = page.get_pixmap()
|
||||
img_data = pix.tobytes("png")
|
||||
|
||||
call_llm_vision("Extract text from this image", img_data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Web Crawling
|
||||
|
||||
```python
|
||||
def crawl_web(url):
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
html = requests.get(url).text
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
return soup.title.string, soup.get_text()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Basic Search (SerpAPI example)
|
||||
|
||||
```python
|
||||
def search_google(query):
|
||||
import requests
|
||||
params = {
|
||||
"engine": "google",
|
||||
"q": query,
|
||||
"api_key": "YOUR_API_KEY"
|
||||
}
|
||||
r = requests.get("https://serpapi.com/search", params=params)
|
||||
return r.json()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 8. Audio Transcription (OpenAI Whisper)
|
||||
|
||||
```python
|
||||
def transcribe_audio(file_path):
|
||||
import openai
|
||||
audio_file = open(file_path, "rb")
|
||||
transcript = openai.Audio.transcribe("whisper-1", audio_file)
|
||||
return transcript["text"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Text-to-Speech (TTS)
|
||||
|
||||
```python
|
||||
def text_to_speech(text):
|
||||
import pyttsx3
|
||||
engine = pyttsx3.init()
|
||||
engine.say(text)
|
||||
engine.runAndWait()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Sending Email
|
||||
|
||||
```python
|
||||
def send_email(to_address, subject, body, from_address, password):
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
msg = MIMEText(body)
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = from_address
|
||||
msg["To"] = to_address
|
||||
|
||||
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
|
||||
server.login(from_address, password)
|
||||
server.sendmail(from_address, [to_address], msg.as_string())
|
||||
```
|
||||
@@ -0,0 +1,142 @@
|
||||
---
|
||||
layout: default
|
||||
title: "Viz and Debug"
|
||||
parent: "Utility Function"
|
||||
nav_order: 3
|
||||
---
|
||||
|
||||
# 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 and parent and link(parent, get_id(node.start))
|
||||
lines.append(f"\n subgraph sub_flow_{get_id(node)}[{type(node).__name__}]")
|
||||
node.start and walk(node.start)
|
||||
for nxt in node.successors.values():
|
||||
node.start and walk(nxt, get_id(node.start)) 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
|
||||
```
|
||||
|
||||
## 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']`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user