Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions AUTO_RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Auto Release

Simple automated release process for AdalFlow pip package.

## How to Release

### Option 1: Tag Release (Automatic)
```bash
git tag v1.1.2
git push origin v1.1.2
```

### Option 2: Manual Trigger
1. Go to GitHub Actions → Release workflow
2. Click "Run workflow"
3. Enter version number
4. Click "Run workflow"

## What Happens Automatically

✅ Updates version in all files
✅ Builds package
✅ Publishes to PyPI
✅ Creates GitHub release

## Requirements

- Tag from `main` branch
- Update `CHANGELOG.md` first
- Ensure tests pass

## Version Format

- `v1.2.3` (patch: bug fixes)
- `v1.3.0` (minor: new features)
- `v2.0.0` (major: breaking changes)

## Troubleshooting

**Failed release?** Check [Actions logs](https://github.com/SylphAI-Inc/AdalFlow/actions)

**Need details?** See [scripts/release.md](scripts/release.md)
119 changes: 91 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,54 +121,117 @@ pip install adalflow
```python
from adalflow import Agent, Runner
from adalflow.components.model_client.openai_client import OpenAIClient

# Create a simple agent
agent = Agent(
name="Assistant",
model_client=OpenAIClient(),
model_kwargs={"model": "gpt-4o", "temperature": 0.3}
from adalflow.core.types import (
ToolCallActivityRunItem,
RunItemStreamEvent,
ToolCallRunItem,
ToolOutputRunItem,
FinalOutputItem
)
import asyncio

runner = Runner(agent=agent)

result = runner.call(prompt_kwargs={"input_str": "Write a haiku about AI and coding"})
print(result.answer)

# Output:
# Code flows like water,
# AI minds think in patterns,
# Logic blooms in bytes.
```

_Set your `OPENAI_API_KEY` environment variable to run this example._

### Agent with Tools

```python
# Define tools
def calculator(expression: str) -> str:
"""Evaluate a mathematical expression."""
try:
result = eval(expression)
return f"Result: {result}"
return f"The result of {expression} is {result}"
except Exception as e:
return f"Error: {e}"

async def web_search(query: str="what is the weather in SF today?") -> str:
"""Web search on query."""
await asyncio.sleep(0.5)
return "San Francisco will be mostly cloudy today with some afternoon sun, reaching about 67 °F (20 °C)."

def counter(limit: int):
"""A counter that counts up to a limit."""
final_output = []
for i in range(1, limit + 1):
stream_item = f"Count: {i}/{limit}"
final_output.append(stream_item)
yield ToolCallActivityRunItem(data=stream_item)
yield final_output

# Create agent with tools
agent = Agent(
name="CalculatorAgent",
tools=[calculator],
name="MyAgent",
tools=[calculator, web_search, counter],
model_client=OpenAIClient(),
model_kwargs={"model": "gpt-4o", "temperature": 0.3}
model_kwargs={"model": "gpt-4o", "temperature": 0.3},
max_steps=5
)

runner = Runner(agent=agent)
```

### 1. Synchronous Call Mode

```python
# Sync call - returns RunnerResult with complete execution history
result = runner.call(
prompt_kwargs={"input_str": "Calculate 15 * 7 + 23 and count to 5"}
)

result = runner.call(prompt_kwargs={"input_str": "Calculate 15 * 7 + 23"})
print(result.answer)
# Output: The result of 15 * 7 + 23 is 128. The counter counted up to 5: 1, 2, 3, 4, 5.

# Access step history
for step in result.step_history:
print(f"Step {step.step}: {step.function.name} -> {step.observation}")
# Output:
# Step 0: calculator -> The result of 15 * 7 + 23 is 128
# Step 1: counter -> ['Count: 1/5', 'Count: 2/5', 'Count: 3/5', 'Count: 4/5', 'Count: 5/5']
```

### 2. Asynchronous Call Mode

# Output: The result of 15 * 7 + 23 is 128.
```python
# Async call - similar output structure to sync call
result = await runner.acall(
prompt_kwargs={"input_str": "What's the weather in SF and calculate 42 * 3"}
)

print(result.answer)
# Output: San Francisco will be mostly cloudy today with some afternoon sun, reaching about 67 °F (20 °C).
# The result of 42 * 3 is 126.
```

### 3. Async Streaming Mode

```python
# Async streaming - real-time event processing
streaming_result = runner.astream(
prompt_kwargs={"input_str": "Calculate 100 + 50 and count to 3"},
)

# Process streaming events in real-time
async for event in streaming_result.stream_events():
if isinstance(event, RunItemStreamEvent):
if isinstance(event.item, ToolCallRunItem):
print(f"🔧 Calling: {event.item.data.name}")
elif isinstance(event.item, ToolCallActivityRunItem):
print(f"📝 Activity: {event.item.data}")
elif isinstance(event.item, ToolOutputRunItem):
print(f"✅ Output: {event.item.data.output}")
elif isinstance(event.item, FinalOutputItem):
print(f"🎯 Final: {event.item.data.answer}")

# Output:
# 🔧 Calling: calculator
# ✅ Output: The result of 100 + 50 is 150
# 🔧 Calling: counter
# 📝 Activity: Count: 1/3
# 📝 Activity: Count: 2/3
# 📝 Activity: Count: 3/3
# ✅ Output: ['Count: 1/3', 'Count: 2/3', 'Count: 3/3']
# 🎯 Final: The result of 100 + 50 is 150. Counted to 3 successfully.
```

_Set your `OPENAI_API_KEY` environment variable to run these examples._

**Try the full Agent tutorial in Colab:** [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/SylphAI-Inc/AdalFlow/blob/main/notebooks/agents/agent_tutorial.ipynb)

<!-- Please refer to the [full installation guide](https://adalflow.sylph.ai/get_started/installation.html) for more details.
[Package changelog](https://github.com/SylphAI-Inc/AdalFlow/blob/main/adalflow/CHANGELOG.md). -->
View [Quickstart](https://colab.research.google.com/drive/1_YnD4HshzPRARvishoU4IA-qQuX9jHrT?usp=sharing): Learn How `AdalFlow` optimizes LM workflows end-to-end in 15 mins.
Expand Down
28 changes: 28 additions & 0 deletions adalflow/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@

## [1.1.2] - 2025-08-16

### Improved

#### Runner (`adalflow/components/agent/runner.py`)
- Restructured `call()`, `acall()`, `astream()` for better generator processing performance
- Fixed error handling for unrecoverable errors with proper recovery logic
- Improved async generator consumption without blocking event loops
- Removed debug print statements

#### FunctionTool (`adalflow/core/func_tool.py`)
- Added `FunctionType` enum (SYNC, ASYNC, SYNC_GENERATOR, ASYNC_GENERATOR)
- Added `detect_function_type()` class method for accurate type detection
- Fixed generator functions to return generator objects instead of consuming them
- Cleaned up commented legacy code

#### ToolManager (`adalflow/core/tool_manager.py`)
- Updated `execute_func()` and `execute_func_async()` for proper generator handling
- Enhanced error propagation in async contexts

### Fixed
- Fixed memory handling to check `_pending_user_query` before adding assistant response
- Fixed async generator handling in sync contexts to avoid event loop blocking

### Tests
- Added comprehensive runner tests (`adalflow/tests/test_runner.py`)
- Added generator function tests (`adalflow/tests/test_tool.py`)

## [1.1.1] - 2025-08-11

### Added
Expand Down
50 changes: 23 additions & 27 deletions adalflow/adalflow/apps/fastapi_permission_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ class ApprovalResponse(BaseModel):


class ApprovalQueue:
"""Manages pending approval requests with expiration."""
"""Manages pending approval requests with optional timeout."""

def __init__(self, timeout_seconds: int = 30):
def __init__(self, timeout_seconds: Optional[int] = None):
self.pending_requests: Dict[str, FunctionRequest] = {}
self.responses: Dict[str, asyncio.Future] = {}
self.request_metadata: Dict[str, ApprovalRequest] = {}
self.timeout_seconds = timeout_seconds
self.timeout_seconds = timeout_seconds # None means no timeout

async def create_request(self, request: FunctionRequest) -> str:
"""Create a new approval request and return its ID."""
Expand All @@ -68,19 +68,27 @@ async def create_request(self, request: FunctionRequest) -> str:
return request_id

async def wait_for_response(self, request_id: str) -> ApprovalOutcome:
"""Wait for approval response with timeout."""
"""Wait for approval response with optional timeout."""
if request_id not in self.responses:
raise ValueError("Invalid request ID")

try:
log.info(f"Starting to wait for Future for request {request_id}")
result = await asyncio.wait_for(
self.responses[request_id], timeout=self.timeout_seconds
)
log.info(f"Starting to wait for Future for request {request_id} (timeout: {self.timeout_seconds or 'indefinite'})")

if self.timeout_seconds is None:
# Wait indefinitely for user approval
result = await self.responses[request_id]
else:
# Wait with timeout
result = await asyncio.wait_for(
self.responses[request_id], timeout=self.timeout_seconds
)

log.info(f"Future resolved for request {request_id} with result {result}")
# Status already set in approve endpoint
return result
except asyncio.TimeoutError:
log.warning(f"Request {request_id} timed out after {self.timeout_seconds} seconds")
self.request_metadata[request_id].status = "expired"
return ApprovalOutcome.CANCEL
finally:
Expand Down Expand Up @@ -120,23 +128,7 @@ def set_response(

def get_pending_requests(self) -> List[ApprovalRequest]:
"""Get all pending approval requests."""
# Clean up expired requests
now = datetime.now()
expired_ids = []

for req_id, metadata in self.request_metadata.items():
if metadata.status == "pending":
age = (now - metadata.timestamp).total_seconds()
if age > self.timeout_seconds:
metadata.status = "expired"
expired_ids.append(req_id)

# Clean up expired futures
for req_id in expired_ids:
if req_id in self.responses and not self.responses[req_id].done():
self.responses[req_id].set_result(ApprovalOutcome.CANCEL)

# Return only pending requests
# Return only pending requests - they wait indefinitely for user approval
return [
metadata
for metadata in self.request_metadata.values()
Expand All @@ -153,7 +145,7 @@ def __init__(
self,
app: Optional[FastAPI] = None,
approval_mode: str = "default",
timeout_seconds: int = 30,
timeout_seconds: Optional[int] = None,
api_prefix: str = "/api/v1/approvals",
):
"""
Expand All @@ -162,7 +154,7 @@ def __init__(
Args:
app: FastAPI application instance. If None, creates a new one.
approval_mode: Mode for handling approvals
timeout_seconds: Timeout for approval requests
timeout_seconds: Timeout for approval requests in seconds. None means no timeout.
api_prefix: URL prefix for API endpoints
"""
self.approval_queue = ApprovalQueue(timeout_seconds)
Expand Down Expand Up @@ -207,6 +199,10 @@ async def approve_request(request_id: str, response: ApprovalResponse):
log.info(
f"Approval endpoint called with URL request_id={request_id}, body={response}"
)
log.info(f"Current queue state - Pending requests: {list(self.approval_queue.pending_requests.keys())}")
log.info(f"Current queue state - Response futures: {list(self.approval_queue.responses.keys())}")
log.info(f"Current queue state - Metadata: {list(self.approval_queue.request_metadata.keys())}")

if request_id not in self.approval_queue.request_metadata:
log.error(
f"Request {request_id} not found in metadata. Available: {list(self.approval_queue.request_metadata.keys())}"
Expand Down
Loading
Loading