Spaces:
Sleeping
Sleeping
Commit
·
2c060c5
0
Parent(s):
Clean repository with latest todo-agent code and article
Browse files- All agent code (storage.py, todo_agent.py) up to date
- Latest article: 'Out-of-the-Box Observability: OpenAI, Phoenix, and Weave Compared'
- Complete test suite and documentation
- No binary files - HF compatible
- .env.example +16 -0
- .gitattributes +1 -0
- .gitignore +143 -0
- README.md +175 -0
- agent/__init__.py +1 -0
- agent/storage.py +229 -0
- agent/todo_agent.py +233 -0
- data/seed_todos.json +20 -0
- instructional_docs/agent_mermaid_diagram.md +14 -0
- instructional_docs/article_draft.md +178 -0
- instructional_docs/community_voice.md +82 -0
- main.py +137 -0
- manage.py +74 -0
- pyproject.toml +19 -0
- requirements.txt +140 -0
- tests/README.md +146 -0
- tests/run_demo_tests.py +146 -0
- tests/test_basic_crud.py +195 -0
- tests/test_natural_language.py +177 -0
- tests/test_web_search_brainstorming.py +177 -0
- todo_gradio/gradio_app.py +160 -0
- uv.lock +0 -0
.env.example
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Example .env for todo-agent
|
2 |
+
|
3 |
+
# Required for OpenAI Agents SDK
|
4 |
+
OPENAI_API_KEY=sk-...
|
5 |
+
|
6 |
+
# Optional: Enable OpenAI Platform tracing
|
7 |
+
OPENAI_TRACING_ENABLED=1
|
8 |
+
|
9 |
+
# Optional: Weights & Biases Weave
|
10 |
+
# WANDB_API_KEY=your-wandb-api-key
|
11 |
+
|
12 |
+
# Optional: Arize Phoenix
|
13 |
+
# PHOENIX_API_KEY=your-arize-api-key
|
14 |
+
# PHOENIX_PROJECT=your-arize-project
|
15 |
+
# PHOENIX_CLIENT_HEADERS=...
|
16 |
+
# PHOENIX_COLLECTOR_ENDPOINT=...
|
.gitattributes
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Byte-compiled / optimized / DLL files
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
|
6 |
+
# C extensions
|
7 |
+
*.so
|
8 |
+
|
9 |
+
# Distribution / packaging
|
10 |
+
.Python
|
11 |
+
build/
|
12 |
+
develop-eggs/
|
13 |
+
dist/
|
14 |
+
downloads/
|
15 |
+
eggs/
|
16 |
+
.eggs/
|
17 |
+
lib/
|
18 |
+
lib64/
|
19 |
+
parts/
|
20 |
+
sdist/
|
21 |
+
var/
|
22 |
+
wheels/
|
23 |
+
*.egg-info/
|
24 |
+
.installed.cfg
|
25 |
+
*.egg
|
26 |
+
MANIFEST
|
27 |
+
|
28 |
+
# PyInstaller
|
29 |
+
# Usually these files are written by a python script from a template
|
30 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
31 |
+
*.manifest
|
32 |
+
*.spec
|
33 |
+
|
34 |
+
# Installer logs
|
35 |
+
pip-log.txt
|
36 |
+
pip-delete-this-directory.txt
|
37 |
+
|
38 |
+
# Unit test / coverage reports
|
39 |
+
htmlcov/
|
40 |
+
.tox/
|
41 |
+
.nox/
|
42 |
+
.coverage
|
43 |
+
.coverage.*
|
44 |
+
.cache
|
45 |
+
nosetests.xml
|
46 |
+
coverage.xml
|
47 |
+
*.cover
|
48 |
+
*.py,cover
|
49 |
+
.hypothesis/
|
50 |
+
.pytest_cache/
|
51 |
+
|
52 |
+
# Translations
|
53 |
+
*.mo
|
54 |
+
*.pot
|
55 |
+
|
56 |
+
# Django stuff:
|
57 |
+
*.log
|
58 |
+
local_settings.py
|
59 |
+
db.sqlite3
|
60 |
+
db.sqlite3-journal
|
61 |
+
|
62 |
+
# Flask stuff:
|
63 |
+
instance/
|
64 |
+
.webassets-cache
|
65 |
+
|
66 |
+
# Scrapy stuff:
|
67 |
+
.scrapy
|
68 |
+
|
69 |
+
# Sphinx documentation
|
70 |
+
docs/_build/
|
71 |
+
|
72 |
+
# PyBuilder
|
73 |
+
target/
|
74 |
+
|
75 |
+
# Jupyter Notebook
|
76 |
+
.ipynb_checkpoints
|
77 |
+
|
78 |
+
# IPython
|
79 |
+
profile_default/
|
80 |
+
ipython_config.py
|
81 |
+
|
82 |
+
# pyenv
|
83 |
+
.python-version
|
84 |
+
|
85 |
+
# pipenv
|
86 |
+
# According to an official Pipenv poll, the Pipfile.lock file should not be
|
87 |
+
# ignored: https://github.com/pypa/pipenv/pull/2607
|
88 |
+
#Pipfile.lock
|
89 |
+
|
90 |
+
# PEP 582; __pypackages__
|
91 |
+
__pypackages__/
|
92 |
+
|
93 |
+
# Celery stuff
|
94 |
+
celerybeat-schedule
|
95 |
+
celerybeat.pid
|
96 |
+
|
97 |
+
# SageMath parsed files
|
98 |
+
*.sage.py
|
99 |
+
|
100 |
+
# Environments
|
101 |
+
.env
|
102 |
+
.venv
|
103 |
+
env/
|
104 |
+
venv/
|
105 |
+
ENV/
|
106 |
+
env.bak/
|
107 |
+
venv.bak/
|
108 |
+
|
109 |
+
# Spyder project settings
|
110 |
+
.spyderproject
|
111 |
+
.spyproject
|
112 |
+
|
113 |
+
# Rope project settings
|
114 |
+
.ropeproject
|
115 |
+
|
116 |
+
# mkdocs documentation
|
117 |
+
/site
|
118 |
+
|
119 |
+
# mypy
|
120 |
+
.mypy_cache/
|
121 |
+
.dmypy.json
|
122 |
+
dmypy.json
|
123 |
+
|
124 |
+
# Pyre type checker
|
125 |
+
.pyre/
|
126 |
+
|
127 |
+
# pytype static analyzer
|
128 |
+
.pytype/
|
129 |
+
|
130 |
+
# Cython debug symbols
|
131 |
+
cython_debug/
|
132 |
+
|
133 |
+
# User-specific data files
|
134 |
+
data/todos.json
|
135 |
+
data/session_default.json
|
136 |
+
|
137 |
+
# Test logs and reports
|
138 |
+
tests/logs/
|
139 |
+
|
140 |
+
# Cursor editor files
|
141 |
+
.cursor/
|
142 |
+
.DS_Store
|
143 |
+
instructional_docs/images/
|
README.md
ADDED
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: To-Do Agent
|
3 |
+
emoji: ✅
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: purple
|
6 |
+
sdk: gradio
|
7 |
+
python_version: 3.12
|
8 |
+
app_file: todo_gradio/gradio_app.py
|
9 |
+
---
|
10 |
+
|
11 |
+
# todo-agent
|
12 |
+
|
13 |
+
A minimal OpenAI Agents SDK to-do list app with a full CRUD toolset and built-in web search. This project includes tracing/observation integrations for:
|
14 |
+
- OpenAI Platform Tracing
|
15 |
+
- Arize Phoenix Cloud
|
16 |
+
- Weights & Biases Weave
|
17 |
+
- A Gradio web UI for interactive use
|
18 |
+
|
19 |
+
This project demonstrates a 101-level AI engineering workflow: building a modular agent, observing traces, and following best practices for Python project management.
|
20 |
+
|
21 |
+
---
|
22 |
+
|
23 |
+
## Project Structure
|
24 |
+
|
25 |
+
```
|
26 |
+
todo-agent/
|
27 |
+
├── main.py # Entry point for the CLI app
|
28 |
+
├── manage.py # CLI for managing data (reset, seed)
|
29 |
+
├── agent/
|
30 |
+
│ ├── __init__.py
|
31 |
+
│ ├── todo_agent.py # Defines the agent, its tools, and prompt
|
32 |
+
│ └── storage.py # Data access layer for todos.json
|
33 |
+
├── todo_gradio/
|
34 |
+
│ └── gradio_app.py # Gradio web UI application
|
35 |
+
├── tests/
|
36 |
+
│ ├── run_demo_tests.py # Test runner for demo scenarios
|
37 |
+
│ ├── test_basic_operations.py
|
38 |
+
│ └── test_web_search_demo.py
|
39 |
+
├── data/
|
40 |
+
│ ├── todos.json # User-specific to-do items (auto-created, gitignored)
|
41 |
+
│ ├── session_default.json # Conversation history (auto-created, gitignored)
|
42 |
+
│ └── seed_todos.json # Example data for the `manage.py seed` command
|
43 |
+
├── .gitignore
|
44 |
+
├── .python-version
|
45 |
+
├── pyproject.toml
|
46 |
+
├── README.md
|
47 |
+
└── uv.lock
|
48 |
+
```
|
49 |
+
|
50 |
+
---
|
51 |
+
|
52 |
+
## Agent Capabilities
|
53 |
+
|
54 |
+
The agent has access to a suite of tools to be a proactive assistant:
|
55 |
+
|
56 |
+
- **`create_todo`**: Adds a new task to the list.
|
57 |
+
- **`read_todos`**: Lists all tasks or filters by project.
|
58 |
+
- **`update_todo`**: Modifies an existing task (e.g., renames it or marks it as complete).
|
59 |
+
- **`delete_todo`**: Removes a task.
|
60 |
+
- **`web_search`**: Searches the web to find information and clarify tasks. For example, if you ask it to "plan a trip," it will offer to research destinations for you.
|
61 |
+
|
62 |
+
---
|
63 |
+
|
64 |
+
## Setup
|
65 |
+
|
66 |
+
1. **Install [uv](https://github.com/astral-sh/uv)**
|
67 |
+
2. **Install dependencies:**
|
68 |
+
```sh
|
69 |
+
uv pip install
|
70 |
+
```
|
71 |
+
3. **Environment variables:**
|
72 |
+
- Copy `.env.example` to `.env` and fill in your API keys and secrets. All required and optional variables are documented in `.env.example`.
|
73 |
+
|
74 |
+
---
|
75 |
+
|
76 |
+
## Environment Variables
|
77 |
+
|
78 |
+
All required and optional environment variables are documented in the `.env.example` file. Copy this file to `.env` and update the values as needed for your environment and integrations.
|
79 |
+
|
80 |
+
---
|
81 |
+
|
82 |
+
## Running the App
|
83 |
+
|
84 |
+
You can run the agent in two ways:
|
85 |
+
|
86 |
+
### 1. Interactive Web UI (Gradio)
|
87 |
+
|
88 |
+
This is the recommended way to use the agent.
|
89 |
+
```sh
|
90 |
+
uv run todo_gradio/gradio_app.py
|
91 |
+
```
|
92 |
+
The app will be available at a local URL (e.g., `http://127.0.0.1:7860`).
|
93 |
+
|
94 |
+
### 2. Command-Line Interface (CLI)
|
95 |
+
```sh
|
96 |
+
uv run main.py
|
97 |
+
```
|
98 |
+
- Interact with the agent in natural language.
|
99 |
+
- Type `exit` or `quit` to end the session.
|
100 |
+
|
101 |
+
---
|
102 |
+
|
103 |
+
## Managing Data for Testing
|
104 |
+
|
105 |
+
This project includes a `manage.py` script with commands to help you reset or seed your data, which is useful for testing or running evaluations.
|
106 |
+
|
107 |
+
- **Resetting Data**: To clear all to-do items and conversation history, run:
|
108 |
+
```sh
|
109 |
+
uv run manage.py reset --yes
|
110 |
+
```
|
111 |
+
- **Seeding Data**: To load a specific set of to-dos for a test, run:
|
112 |
+
```sh
|
113 |
+
uv run manage.py seed
|
114 |
+
```
|
115 |
+
This command uses `data/seed_todos.json` by default, but you can provide a path to a different file.
|
116 |
+
|
117 |
+
## Test Suite
|
118 |
+
|
119 |
+
The project includes a comprehensive test suite for automated testing and demonstration. See `tests/README.md` for detailed documentation.
|
120 |
+
|
121 |
+
```sh
|
122 |
+
# Run all demo tests
|
123 |
+
uv run tests/run_demo_tests.py all
|
124 |
+
|
125 |
+
# Run individual demos
|
126 |
+
uv run tests/run_demo_tests.py basic
|
127 |
+
uv run tests/run_demo_tests.py websearch
|
128 |
+
```
|
129 |
+
|
130 |
+
The test suite demonstrates:
|
131 |
+
- **Basic Operations**: AI Engineers 101 article creation workflow with technical content planning, status progression, and multi-project organization.
|
132 |
+
- **Web Search Demo**: AI engineering research and technical writing with web search integration, knowledge synthesis, and research-driven task creation.
|
133 |
+
|
134 |
+
Each test includes minimal validation and educational comments, using the same tracing setup as the main application for clean trace separation. **Use the observability dashboards to evaluate test quality and agent performance.**
|
135 |
+
|
136 |
+
---
|
137 |
+
|
138 |
+
## Data Files
|
139 |
+
|
140 |
+
The `data/` directory holds both user-generated data and example data:
|
141 |
+
|
142 |
+
- **`todos.json` & `session_default.json`**: These files are created automatically when you first run the app. They are listed in the `.gitignore` file, so your local conversation history and to-do items will not be committed to the repository.
|
143 |
+
- **`seed_todos.json`**: This file is included in the repository and provides a default set of to-dos that you can load using the `uv run manage.py seed` command. It serves as a good starting point for testing.
|
144 |
+
|
145 |
+
---
|
146 |
+
|
147 |
+
## Tracing Integrations
|
148 |
+
|
149 |
+
All tracing integrations are enabled by default in `main.py`:
|
150 |
+
- **OpenAI Platform**: Native support, see [OpenAI docs](https://platform.openai.com/docs/observability/overview)
|
151 |
+
- **Arize Phoenix Cloud**: See [Phoenix Otel Python SDK](https://arize.com/docs/phoenix/sdk-api-reference/python-pacakges/arize-phoenix-otel)
|
152 |
+
- **Weights & Biases Weave**: See [W&B Weave docs](https://docs.wandb.ai/guides/weave)
|
153 |
+
|
154 |
+
You can view traces in each provider's dashboard after running the agent.
|
155 |
+
|
156 |
+
---
|
157 |
+
|
158 |
+
## Development Standards
|
159 |
+
|
160 |
+
- Use Python 3.12+
|
161 |
+
- Use `uv` for all dependency and environment management
|
162 |
+
- Only add dependencies with `uv add ...` or `uv pip install ...`
|
163 |
+
- Never edit `pyproject.toml` directly
|
164 |
+
- Use `python-dotenv` for environment variables
|
165 |
+
- Follow PEP 8 and use type hints
|
166 |
+
- Modular, reusable, and well-documented code
|
167 |
+
- Comment complex logic and include Google-style docstrings
|
168 |
+
- Implement proper error handling
|
169 |
+
- Focus on security and performance
|
170 |
+
|
171 |
+
---
|
172 |
+
|
173 |
+
## License
|
174 |
+
|
175 |
+
MIT
|
agent/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
|
agent/storage.py
ADDED
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Data access layer for the to-do list.
|
3 |
+
|
4 |
+
Provides abstract base class and concrete implementations:
|
5 |
+
- JsonTodoStorage: Persists to local JSON file
|
6 |
+
- InMemoryTodoStorage: Memory-based storage for transient sessions
|
7 |
+
|
8 |
+
This separation allows different app entry points to use appropriate storage.
|
9 |
+
"""
|
10 |
+
|
11 |
+
import os
|
12 |
+
import json
|
13 |
+
from abc import ABC, abstractmethod
|
14 |
+
from enum import Enum
|
15 |
+
from typing import List, Optional, Dict, Any
|
16 |
+
from datetime import datetime, timezone
|
17 |
+
from pydantic import BaseModel, Field
|
18 |
+
|
19 |
+
DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "todos.json")
|
20 |
+
|
21 |
+
class TodoStatus(str, Enum):
|
22 |
+
"""Status enumeration, inherits from str for JSON serialization."""
|
23 |
+
NOT_STARTED = "Not Started"
|
24 |
+
IN_PROGRESS = "In Progress"
|
25 |
+
COMPLETED = "Completed"
|
26 |
+
|
27 |
+
class TodoItem(BaseModel):
|
28 |
+
"""Represents a single to-do item."""
|
29 |
+
id: int
|
30 |
+
name: str = Field(..., description="Short, clear task name")
|
31 |
+
description: Optional[str] = Field(default=None, description="Optional detailed description")
|
32 |
+
project: Optional[str] = Field(default=None, description="Optional project name for grouping")
|
33 |
+
status: TodoStatus = Field(default=TodoStatus.NOT_STARTED, description="Current status")
|
34 |
+
created_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="Creation timestamp (UTC ISO 8601)")
|
35 |
+
updated_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="Last update timestamp (UTC ISO 8601)")
|
36 |
+
|
37 |
+
# =============================================================================
|
38 |
+
# Storage Interface
|
39 |
+
# =============================================================================
|
40 |
+
|
41 |
+
class AbstractTodoStorage(ABC):
|
42 |
+
"""Abstract base class defining the contract for to-do storage."""
|
43 |
+
|
44 |
+
@abstractmethod
|
45 |
+
def create(self, name: str, description: Optional[str], project: Optional[str]) -> TodoItem:
|
46 |
+
"""Creates a new to-do item and saves it."""
|
47 |
+
pass
|
48 |
+
|
49 |
+
@abstractmethod
|
50 |
+
def read_all(self) -> List[TodoItem]:
|
51 |
+
"""Returns all to-do items."""
|
52 |
+
pass
|
53 |
+
|
54 |
+
@abstractmethod
|
55 |
+
def read_by_id(self, item_id: int) -> Optional[TodoItem]:
|
56 |
+
"""Finds a single to-do item by its ID."""
|
57 |
+
pass
|
58 |
+
|
59 |
+
@abstractmethod
|
60 |
+
def read_by_project(self, project: str) -> List[TodoItem]:
|
61 |
+
"""Finds all to-do items belonging to a specific project."""
|
62 |
+
pass
|
63 |
+
|
64 |
+
@abstractmethod
|
65 |
+
def update(self, item_id: int, update_data: Dict[str, Any]) -> Optional[TodoItem]:
|
66 |
+
"""Updates an existing to-do item and saves the list."""
|
67 |
+
pass
|
68 |
+
|
69 |
+
@abstractmethod
|
70 |
+
def delete(self, item_id: int) -> bool:
|
71 |
+
"""Deletes a to-do item by its ID and saves the list."""
|
72 |
+
pass
|
73 |
+
|
74 |
+
# =============================================================================
|
75 |
+
# JSON File Storage
|
76 |
+
# =============================================================================
|
77 |
+
|
78 |
+
class JsonTodoStorage(AbstractTodoStorage):
|
79 |
+
"""Handles persistence using a JSON file."""
|
80 |
+
def __init__(self, path: str = DATA_PATH):
|
81 |
+
self._path = path
|
82 |
+
self._ensure_data_file()
|
83 |
+
|
84 |
+
def _ensure_data_file(self):
|
85 |
+
"""Ensure the todos.json file exists."""
|
86 |
+
if not os.path.exists(self._path):
|
87 |
+
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
88 |
+
with open(self._path, "w") as f:
|
89 |
+
json.dump([], f)
|
90 |
+
|
91 |
+
def _load_todos(self) -> List[TodoItem]:
|
92 |
+
"""Load all todos from JSON file and validate with Pydantic."""
|
93 |
+
with open(self._path, "r") as f:
|
94 |
+
data = json.load(f)
|
95 |
+
return [TodoItem(**item) for item in data]
|
96 |
+
|
97 |
+
def _save_todos(self, todos: List[TodoItem]):
|
98 |
+
"""Save all todos to JSON file."""
|
99 |
+
with open(self._path, "w") as f:
|
100 |
+
json.dump([item.model_dump() for item in todos], f, indent=2)
|
101 |
+
|
102 |
+
def _get_next_id(self, todos: List[TodoItem]) -> int:
|
103 |
+
"""Get the next available ID for a new to-do item."""
|
104 |
+
return max([t.id for t in todos], default=0) + 1
|
105 |
+
|
106 |
+
def create(self, name: str, description: Optional[str], project: Optional[str]) -> TodoItem:
|
107 |
+
"""Creates a new to-do item and saves it."""
|
108 |
+
todos = self._load_todos()
|
109 |
+
now = datetime.now(timezone.utc).isoformat()
|
110 |
+
new_item = TodoItem(
|
111 |
+
id=self._get_next_id(todos),
|
112 |
+
name=name,
|
113 |
+
description=description,
|
114 |
+
project=project,
|
115 |
+
created_at=now,
|
116 |
+
updated_at=now,
|
117 |
+
)
|
118 |
+
todos.append(new_item)
|
119 |
+
self._save_todos(todos)
|
120 |
+
return new_item
|
121 |
+
|
122 |
+
def read_all(self) -> List[TodoItem]:
|
123 |
+
"""Returns all to-do items."""
|
124 |
+
return self._load_todos()
|
125 |
+
|
126 |
+
def read_by_id(self, item_id: int) -> Optional[TodoItem]:
|
127 |
+
"""Finds a single to-do item by its ID."""
|
128 |
+
todos = self.read_all()
|
129 |
+
return next((t for t in todos if t.id == item_id), None)
|
130 |
+
|
131 |
+
def read_by_project(self, project: str) -> List[TodoItem]:
|
132 |
+
"""Finds all to-do items belonging to a specific project."""
|
133 |
+
todos = self.read_all()
|
134 |
+
return [t for t in todos if t.project and t.project.lower() == project.lower()]
|
135 |
+
|
136 |
+
def update(self, item_id: int, update_data: Dict[str, Any]) -> Optional[TodoItem]:
|
137 |
+
"""Updates an existing to-do item and saves the list."""
|
138 |
+
todos = self.read_all()
|
139 |
+
|
140 |
+
for i, item in enumerate(todos):
|
141 |
+
if item.id == item_id:
|
142 |
+
# Convert status string to enum if needed
|
143 |
+
if "status" in update_data and isinstance(update_data["status"], str):
|
144 |
+
try:
|
145 |
+
update_data["status"] = TodoStatus(update_data["status"])
|
146 |
+
except ValueError:
|
147 |
+
pass
|
148 |
+
|
149 |
+
update_data["updated_at"] = datetime.now(timezone.utc).isoformat()
|
150 |
+
updated_item = todos[i].model_copy(update=update_data)
|
151 |
+
todos[i] = updated_item
|
152 |
+
self._save_todos(todos)
|
153 |
+
return updated_item
|
154 |
+
|
155 |
+
return None
|
156 |
+
|
157 |
+
def delete(self, item_id: int) -> bool:
|
158 |
+
"""Deletes a to-do item by its ID and saves the list."""
|
159 |
+
todos = self._load_todos()
|
160 |
+
original_count = len(todos)
|
161 |
+
new_todos = [t for t in todos if t.id != item_id]
|
162 |
+
|
163 |
+
if len(new_todos) == original_count:
|
164 |
+
return False
|
165 |
+
|
166 |
+
self._save_todos(new_todos)
|
167 |
+
return True
|
168 |
+
|
169 |
+
# =============================================================================
|
170 |
+
# In-Memory Storage
|
171 |
+
# =============================================================================
|
172 |
+
|
173 |
+
class InMemoryTodoStorage(AbstractTodoStorage):
|
174 |
+
"""Handles persistence in memory for transient sessions."""
|
175 |
+
def __init__(self):
|
176 |
+
self._todos: List[TodoItem] = []
|
177 |
+
self._next_id = 1
|
178 |
+
|
179 |
+
def _get_next_id(self) -> int:
|
180 |
+
"""Get the next available ID for a new to-do item."""
|
181 |
+
current_id = self._next_id
|
182 |
+
self._next_id += 1
|
183 |
+
return current_id
|
184 |
+
|
185 |
+
def create(self, name: str, description: Optional[str], project: Optional[str]) -> TodoItem:
|
186 |
+
now = datetime.now(timezone.utc).isoformat()
|
187 |
+
new_item = TodoItem(
|
188 |
+
id=self._get_next_id(),
|
189 |
+
name=name,
|
190 |
+
description=description,
|
191 |
+
project=project,
|
192 |
+
created_at=now,
|
193 |
+
updated_at=now,
|
194 |
+
)
|
195 |
+
self._todos.append(new_item)
|
196 |
+
return new_item
|
197 |
+
|
198 |
+
def read_all(self) -> List[TodoItem]:
|
199 |
+
return self._todos
|
200 |
+
|
201 |
+
def read_by_id(self, item_id: int) -> Optional[TodoItem]:
|
202 |
+
return next((t for t in self._todos if t.id == item_id), None)
|
203 |
+
|
204 |
+
def read_by_project(self, project: str) -> List[TodoItem]:
|
205 |
+
return [t for t in self._todos if t.project and t.project.lower() == project.lower()]
|
206 |
+
|
207 |
+
def update(self, item_id: int, update_data: dict) -> Optional[TodoItem]:
|
208 |
+
item_to_update = self.read_by_id(item_id)
|
209 |
+
if not item_to_update:
|
210 |
+
return None
|
211 |
+
|
212 |
+
# Convert status string to enum if needed
|
213 |
+
if "status" in update_data and isinstance(update_data["status"], str):
|
214 |
+
try:
|
215 |
+
update_data["status"] = TodoStatus(update_data["status"])
|
216 |
+
except ValueError:
|
217 |
+
pass
|
218 |
+
|
219 |
+
for key, value in update_data.items():
|
220 |
+
if hasattr(item_to_update, key):
|
221 |
+
setattr(item_to_update, key, value)
|
222 |
+
|
223 |
+
item_to_update.updated_at = datetime.now(timezone.utc).isoformat()
|
224 |
+
return item_to_update
|
225 |
+
|
226 |
+
def delete(self, item_id: int) -> bool:
|
227 |
+
original_count = len(self._todos)
|
228 |
+
self._todos = [t for t in self._todos if t.id != item_id]
|
229 |
+
return len(self._todos) < original_count
|
agent/todo_agent.py
ADDED
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Agent identity, instructions, and tools.
|
3 |
+
|
4 |
+
Tools bridge agent reasoning with the data layer in storage.py.
|
5 |
+
"""
|
6 |
+
|
7 |
+
import json
|
8 |
+
from typing import Optional, Any
|
9 |
+
from agents import Agent, function_tool, WebSearchTool
|
10 |
+
from agent.storage import AbstractTodoStorage, JsonTodoStorage, TodoStatus
|
11 |
+
|
12 |
+
# =============================================================================
|
13 |
+
# Tool Definitions
|
14 |
+
# =============================================================================
|
15 |
+
|
16 |
+
# Factory uses closure to inject storage dependency, keeping tool signatures clean for LLM
|
17 |
+
def get_tools(storage: AbstractTodoStorage):
|
18 |
+
"""Factory to create tool functions with a specific storage backend."""
|
19 |
+
|
20 |
+
@function_tool
|
21 |
+
def create_todo(
|
22 |
+
name: str,
|
23 |
+
description: Optional[str] = None,
|
24 |
+
project: Optional[str] = None
|
25 |
+
) -> str:
|
26 |
+
"""Creates a new to-do item.
|
27 |
+
|
28 |
+
Use when users ask to add, create, or remember tasks.
|
29 |
+
Be proactive about organizing tasks into projects.
|
30 |
+
|
31 |
+
Args:
|
32 |
+
name: Brief, clear task title
|
33 |
+
description: Optional details or subtasks
|
34 |
+
project: Optional project/category for organization
|
35 |
+
|
36 |
+
Returns:
|
37 |
+
Confirmation message with the created item's ID and details.
|
38 |
+
"""
|
39 |
+
try:
|
40 |
+
item = storage.create(name, description, project)
|
41 |
+
return f"Created to-do item {item.id} ('{item.name}') in project '{item.project or 'None'}' with status '{item.status.value}'."
|
42 |
+
except Exception as e:
|
43 |
+
return f"Error creating to-do: {e}"
|
44 |
+
|
45 |
+
@function_tool
|
46 |
+
def read_todos(
|
47 |
+
item_id: Optional[int] = None,
|
48 |
+
project: Optional[str] = None
|
49 |
+
) -> str:
|
50 |
+
"""Reads all to-do items, or filters by ID/project.
|
51 |
+
|
52 |
+
Use without parameters to see everything, or filter to find specific items.
|
53 |
+
Always check the list before updating or deleting items.
|
54 |
+
|
55 |
+
Args:
|
56 |
+
item_id: Optional - specific todo item ID to retrieve
|
57 |
+
project: Optional - filter by project name (case-insensitive)
|
58 |
+
|
59 |
+
Returns:
|
60 |
+
JSON formatted list of todos or specific todo details.
|
61 |
+
"""
|
62 |
+
try:
|
63 |
+
if item_id is not None:
|
64 |
+
item = storage.read_by_id(item_id)
|
65 |
+
return item.model_dump_json(indent=2) if item else f"To-do item with ID {item_id} not found."
|
66 |
+
|
67 |
+
if project:
|
68 |
+
project_todos = storage.read_by_project(project)
|
69 |
+
if not project_todos:
|
70 |
+
return f"No to-do items found for project '{project}'."
|
71 |
+
return '[\n' + ',\n'.join([t.model_dump_json(indent=2) for t in project_todos]) + '\n]'
|
72 |
+
|
73 |
+
all_todos = storage.read_all()
|
74 |
+
return '[\n' + ',\n'.join([t.model_dump_json(indent=2) for t in all_todos]) + '\n]'
|
75 |
+
except Exception as e:
|
76 |
+
return f"Error reading to-dos: {e}"
|
77 |
+
|
78 |
+
@function_tool
|
79 |
+
def update_todo(
|
80 |
+
item_id: int,
|
81 |
+
name: Optional[str] = None,
|
82 |
+
description: Optional[str] = None,
|
83 |
+
project: Optional[str] = None,
|
84 |
+
status: Optional[str] = None
|
85 |
+
) -> str:
|
86 |
+
"""Updates an existing to-do item's attributes.
|
87 |
+
|
88 |
+
Use to modify tasks or mark them complete. Pay attention to user's
|
89 |
+
language - past tense usually means update status to "Completed".
|
90 |
+
|
91 |
+
Args:
|
92 |
+
item_id: The ID of the to-do item to update (required)
|
93 |
+
name: The new name of the to-do item
|
94 |
+
description: The new description of the to-do item
|
95 |
+
project: The new project name for the to-do item
|
96 |
+
status: Use exact status values: "Not Started", "In Progress", or "Completed" (case-sensitive)
|
97 |
+
|
98 |
+
Returns:
|
99 |
+
Confirmation of update or error message if item not found.
|
100 |
+
"""
|
101 |
+
try:
|
102 |
+
# Validate status against enum values to prevent hallucination
|
103 |
+
if status and status not in [s.value for s in TodoStatus]:
|
104 |
+
return f"Error: Invalid status '{status}'. Please use one of: {[s.value for s in TodoStatus]}."
|
105 |
+
|
106 |
+
update_data = {'name': name, 'description': description, 'project': project, 'status': status}
|
107 |
+
update_fields = {k: v for k, v in update_data.items() if v is not None}
|
108 |
+
|
109 |
+
if not update_fields:
|
110 |
+
return "Error: No fields to update were provided."
|
111 |
+
|
112 |
+
updated_item = storage.update(item_id, update_fields)
|
113 |
+
return f"Updated to-do item {item_id}." if updated_item else f"To-do item with id {item_id} not found."
|
114 |
+
except Exception as e:
|
115 |
+
return f"Error updating to-do: {e}"
|
116 |
+
|
117 |
+
@function_tool
|
118 |
+
def delete_todo(
|
119 |
+
item_id: int
|
120 |
+
) -> str:
|
121 |
+
"""Deletes a to-do item from the list by its ID.
|
122 |
+
|
123 |
+
Use to remove completed or cancelled tasks. Best practice:
|
124 |
+
always confirm with the user before deleting.
|
125 |
+
|
126 |
+
Args:
|
127 |
+
item_id: The ID of the to-do item to delete (required)
|
128 |
+
|
129 |
+
Returns:
|
130 |
+
Confirmation of deletion or error if item not found.
|
131 |
+
"""
|
132 |
+
try:
|
133 |
+
if storage.delete(item_id):
|
134 |
+
return f"Deleted to-do item {item_id}."
|
135 |
+
else:
|
136 |
+
return f"To-do item with id {item_id} not found."
|
137 |
+
except Exception as e:
|
138 |
+
return f"Error deleting to-do: {e}"
|
139 |
+
|
140 |
+
return [create_todo, read_todos, update_todo, delete_todo, WebSearchTool()]
|
141 |
+
|
142 |
+
# =============================================================================
|
143 |
+
# Agent Configuration
|
144 |
+
# =============================================================================
|
145 |
+
|
146 |
+
AGENT_PROMPT = """
|
147 |
+
You are a professional Executive Assistant. Your sole responsibility is to manage the user's to-do list with precision and initiative.
|
148 |
+
|
149 |
+
You have a set of office supplies (tools) to manage the to-do list:
|
150 |
+
- `create_todo`: Use this to add a new task.
|
151 |
+
- `read_todos`: Use this to review existing tasks. You can view all tasks, or filter by a specific project.
|
152 |
+
- `update_todo`: Use this to modify an existing task, such as changing its name or marking it as complete.
|
153 |
+
- `delete_todo`: Use this to remove a task from the list.
|
154 |
+
|
155 |
+
You also have a `web_search` tool for research. Use it proactively to help the user clarify vague tasks. Your goal is to turn ambiguous requests into actionable to-do items.
|
156 |
+
|
157 |
+
**Your Capabilities & Boundaries:**
|
158 |
+
- Primary focus: Managing and organizing the user's to-do list
|
159 |
+
- Supporting capabilities: Use web search, basic math, and logical reasoning to help users create better, more actionable tasks
|
160 |
+
- Always ground your help in task creation or organization - if a user asks something unrelated, acknowledge it briefly then guide them back to their task list
|
161 |
+
|
162 |
+
**Communication Principles:**
|
163 |
+
- Be concise but thorough - provide the right amount of detail for the task
|
164 |
+
- Confirm actions before asking follow-ups: "I've added X to your list. Would you like..."
|
165 |
+
- Use formatting for clarity (bullets for lists, bold for emphasis)
|
166 |
+
- Show your reasoning when it helps: "Based on my research, I suggest breaking this into 3 tasks..."
|
167 |
+
- When assigning projects, use consistent naming (e.g., "Writing" not "writing" or "WRITING")
|
168 |
+
|
169 |
+
**Your Professional Workflow:**
|
170 |
+
- When a user adds tasks, think about how they could be grouped. If a user adds "Buy milk" and later "Buy bread," assign both to a "Groceries" project. Be proactive in organizing the user's life.
|
171 |
+
- When a user gives a vague task (e.g., "plan a trip"), don't just add it. Confirm the entry, then immediately offer to perform web research to gather necessary details.
|
172 |
+
- After research, propose specific, actionable to-do items. For example, after researching Mexico, suggest creating tasks like "Book flights to Mexico" and "Reserve hotel in Cancun."
|
173 |
+
- Always confirm actions with the user and use the precise tool for each operation. Maintain a professional and helpful tone.
|
174 |
+
|
175 |
+
**Interpreting User Updates:**
|
176 |
+
- When a user provides an update about an existing task, pay close attention to their phrasing.
|
177 |
+
- If the user uses past-tense language (e.g., "I just finished the report," "I already bought the groceries," "I joined the gym"), it's a strong signal that the task is complete. First, find the relevant task ID, then confirm with the user before calling `update_todo` with `status='Completed'`.
|
178 |
+
- If the user describes a change to the task's requirements (e.g., "add X to the shopping list," "change the meeting to 3 PM"), update the task's name or description using `update_todo`.
|
179 |
+
|
180 |
+
**When Things Go Wrong:**
|
181 |
+
- If a tool operation fails, explain clearly and suggest alternatives
|
182 |
+
- If you can't find a todo item, offer to show the full list or search by keywords
|
183 |
+
- If web search returns no results, acknowledge this and ask for clarification
|
184 |
+
|
185 |
+
**Example Interaction Flow:**
|
186 |
+
- **User**: "Add 'plan my trip' to my list."
|
187 |
+
- **Assistant**: (Calls `create_todo` with name="plan my trip"). "Of course. I've added 'plan my trip' to your list. To make this more actionable, may I research potential destinations for you?"
|
188 |
+
- **User**: "I'm not sure, maybe somewhere warm in December. Can you look up some ideas?"
|
189 |
+
- **Assistant**: (Calls `web_search`). "My research shows that popular warm destinations in December include Hawaii, Mexico, and the Caribbean. Do any of these appeal to you?"
|
190 |
+
- **User**: "Mexico sounds great."
|
191 |
+
- **Assistant**: "Excellent. I will update the task to 'Plan trip to Mexico'." (Calls `update_todo` to change name). "Shall I also add 'Book flights to Mexico' and 'Book hotel in Mexico' to your to-do list?"
|
192 |
+
|
193 |
+
**Example - Multi-Task Efficiency:**
|
194 |
+
- **User**: "Add these three tasks: draft report, schedule meeting, and buy coffee"
|
195 |
+
- **Assistant**: "I'll add all three tasks for you." (Calls `create_todo` three times efficiently). "I've added 'draft report', 'schedule meeting', and 'buy coffee' to your list. Should I group these under a specific project like 'Work' or 'Weekly Tasks'?"
|
196 |
+
|
197 |
+
**Example - Using Math for Better Tasks:**
|
198 |
+
- **User**: "I need to save money for a $3,000 vacation in 10 months"
|
199 |
+
- **Assistant**: "Let me help you plan this. You'll need to save $300/month to reach $3,000 in 10 months. I'll create a task 'Set aside $300 for vacation fund' with a monthly recurrence. Would you also like me to research ways to reduce expenses or find side income opportunities?"
|
200 |
+
|
201 |
+
Your objective is to be a proactive partner who adds value, not just a passive note-taker.
|
202 |
+
"""
|
203 |
+
|
204 |
+
# =============================================================================
|
205 |
+
# Agent Factory
|
206 |
+
# =============================================================================
|
207 |
+
|
208 |
+
def create_agent(
|
209 |
+
storage: AbstractTodoStorage,
|
210 |
+
agent_name: str = "To-Do Agent"
|
211 |
+
):
|
212 |
+
"""Factory function to create a new To-Do Agent instance.
|
213 |
+
|
214 |
+
This centralizes agent configuration and makes it easy to create
|
215 |
+
consistent instances across different parts of the application.
|
216 |
+
|
217 |
+
Args:
|
218 |
+
storage: An instance of a storage class (e.g., JsonTodoStorage).
|
219 |
+
agent_name: The name for the agent instance.
|
220 |
+
"""
|
221 |
+
# OpenAI: Add minimal metadata that appears in OpenAI Platform traces
|
222 |
+
import os
|
223 |
+
os.environ.setdefault("OPENAI_TRACE_TAGS", f"app-name:todo-agent,environment:production,agent-type:conversational")
|
224 |
+
|
225 |
+
return Agent(
|
226 |
+
name=agent_name,
|
227 |
+
model="gpt-4.1-mini",
|
228 |
+
instructions=AGENT_PROMPT,
|
229 |
+
tools=get_tools(storage),
|
230 |
+
)
|
231 |
+
|
232 |
+
# Default agent instance using file-based storage for CLI usage
|
233 |
+
agent = create_agent(JsonTodoStorage())
|
data/seed_todos.json
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[
|
2 |
+
{
|
3 |
+
"id": 1,
|
4 |
+
"name": "Plan a vacation",
|
5 |
+
"description": "Need to decide on a destination.",
|
6 |
+
"project": "Travel",
|
7 |
+
"completed": false,
|
8 |
+
"created_at": "2025-07-01T12:00:00+00:00",
|
9 |
+
"updated_at": "2025-07-01T12:00:00+00:00"
|
10 |
+
},
|
11 |
+
{
|
12 |
+
"id": 2,
|
13 |
+
"name": "Buy a new laptop",
|
14 |
+
"description": "Current one is getting slow. Need to research models.",
|
15 |
+
"project": "Personal Tech",
|
16 |
+
"completed": false,
|
17 |
+
"created_at": "2025-07-01T12:01:00+00:00",
|
18 |
+
"updated_at": "2025-07-01T12:01:00+00:00"
|
19 |
+
}
|
20 |
+
]
|
instructional_docs/agent_mermaid_diagram.md
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
```mermaid
|
2 |
+
graph TD;
|
3 |
+
UserInput["User Input<br/>e.g., 'add buy milk'"] --> MainLoop["main.py: Main Loop"];
|
4 |
+
MainLoop -- "Captures input &<br/>manages history" --> Runner["main.py: Agent Runner"];
|
5 |
+
Runner -- "Invokes agent" --> Agent["agent/todo_agent.py: Agent (LLM + Prompt)"];
|
6 |
+
Agent -- "Decides to use a tool" --> ToolSelection["agent/todo_agent.py: Tool Selection<br/>e.g., create_todo"];
|
7 |
+
ToolSelection -- "Executes function" --> ToolFunction["agent/todo_agent.py: Tool Function<br/>create_todo(...)"];
|
8 |
+
ToolFunction -- "Calls storage layer" --> Storage["agent/storage.py: TodoStorage"];
|
9 |
+
Storage -- "Reads/writes file" --> Data["data/todos.json"];
|
10 |
+
ToolFunction -- "Returns result" --> Agent;
|
11 |
+
Agent -- "Formulates final response" --> Runner;
|
12 |
+
Runner -- "Returns output" --> MainLoop;
|
13 |
+
MainLoop -- "Prints to console" --> AgentOutput["Agent Output<br/>e.g., 'Created to-do...'"];
|
14 |
+
```
|
instructional_docs/article_draft.md
ADDED
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
# Out-of-the-Box Observability: OpenAI, Phoenix, and Weave Compared
|
3 |
+
|
4 |
+
Hey fellow MLOps practitioners, here's the reality: when you're working with foundation models, you're essentially flying blind unless you have proper observability. The catch? Most teams think they need to build complex custom tracking from scratch. **Plot twist: you don't.**
|
5 |
+
|
6 |
+
Three major platforms—OpenAI's native tracing, Arize Phoenix, and W&B Weave—give you powerful observability capabilities right out of the box. With just a few lines of setup code, you get detailed traces, spans, and metrics that would take weeks to build yourself. But here's the question: which platform gives you the best default experience?
|
7 |
+
|
8 |
+
I spent time testing all three with a simple multi-tool agent, and the results surprised me. Each platform has a distinct personality when it comes to out-of-the-box observability—from OpenAI's clean simplicity to Phoenix's analytics depth to Weave's collaboration focus. In this post, I'll show you exactly what each platform gives you by default, so you can pick the right one for your workflow without reinventing the wheel.
|
9 |
+
|
10 |
+
Let's dive into what you actually get for free (or nearly free) when it comes to foundation model observability.
|
11 |
+
|
12 |
+
---
|
13 |
+
|
14 |
+
## Part 1: Test Case - Multi-Tool Agent Architecture
|
15 |
+
|
16 |
+
I built this simple CRUD to-do agent to mimic the kind of multi-tool workflows we see in production—like an overworked intern juggling database ops and quick web searches. It's powered by `gpt-4.1-mini`, with a detailed prompt for reasoning, tools for task management, and persistent storage.
|
17 |
+
|
18 |
+
### Quick-Start: Run the Agent
|
19 |
+
|
20 |
+
Get it running locally in minutes:
|
21 |
+
|
22 |
+
```bash
|
23 |
+
git clone https://github.com/leowalker89/todo-agent.git
|
24 |
+
cd todo-agent
|
25 |
+
uv sync # Install deps with uv
|
26 |
+
cp .env.example .env # Add your API keys
|
27 |
+
uv run main.py
|
28 |
+
```
|
29 |
+
|
30 |
+
Now you're ready to test commands like 'Add a task to research MLOps tools.'
|
31 |
+
|
32 |
+
### Why This Makes a Good Test Case
|
33 |
+
|
34 |
+
This agent combines LLM reasoning, tool selection, and multi-step execution—perfect for evaluating what each platform shows you by default. In production terms, it's like a lightweight RAG system: vague user input → tool calls → refined output.
|
35 |
+
|
36 |
+
### Core Components
|
37 |
+
|
38 |
+
- **Model**: `gpt-4.1-mini` for cost-effective intelligence.
|
39 |
+
- **Prompt**: Guides proactive task management.
|
40 |
+
- **Tools**:
|
41 |
+
- `create_todo`: Add tasks.
|
42 |
+
- `read_todos`: List or filter tasks.
|
43 |
+
- `update_todo`: Modify tasks (e.g., mark complete).
|
44 |
+
- `delete_todo`: Remove tasks.
|
45 |
+
- `web_search`: Research to clarify vague requests.
|
46 |
+
|
47 |
+
### Execution Flow
|
48 |
+
|
49 |
+
The agent interprets your request, selects tools, executes them, and responds—mirroring real MLOps pipelines where observability becomes crucial for understanding what's happening under the hood.
|
50 |
+
|
51 |
+
---
|
52 |
+
|
53 |
+
## Part 2: Traces & Spans: The Building Blocks
|
54 |
+
|
55 |
+
Before we dive into what each platform gives you, let's quickly cover the fundamentals. Understanding traces and spans is key to appreciating what these platforms deliver out-of-the-box.
|
56 |
+
|
57 |
+
### What is a Trace?
|
58 |
+
|
59 |
+
A trace is the full execution record of a single workflow, documenting every step from input to output. Think of it as your AI's 'flight recorder'—essential for understanding what happened when things go sideways.
|
60 |
+
|
61 |
+
### Breaking It Down: Spans
|
62 |
+
|
63 |
+
Each trace consists of spans, which are isolated steps in the process. For our agent, a typical trace might include:
|
64 |
+
1. **Input Processing**: The LLM interprets the user's request.
|
65 |
+
2. **Tool Selection & Execution**: Calling tools like `web_search` or `update_todo`.
|
66 |
+
3. **Output Generation**: Formulating the final response.
|
67 |
+
|
68 |
+
### Key Metadata You Get For Free
|
69 |
+
|
70 |
+
Here's what all three platforms capture automatically:
|
71 |
+
- **Latency**: Time per step (in ms)—crucial for spotting bottlenecks.
|
72 |
+
- **Tokens**: Usage and cost—helps with budget tracking.
|
73 |
+
- **Input/Output**: Exact data flowing through—perfect for debugging.
|
74 |
+
- **Status**: Success or error—basic but expandable.
|
75 |
+
- **Tool Calls**: Which tools were selected and their parameters.
|
76 |
+
|
77 |
+
The beauty? You don't have to manually instrument any of this. These platforms capture it all with minimal setup.
|
78 |
+
|
79 |
+
---
|
80 |
+
|
81 |
+
## Part 3: Platform Comparison - What You Get Out of the Box
|
82 |
+
|
83 |
+
To test these platforms, I ran the agent's built-in demos—things like article planning and web research—that simulate real MLOps workflows. Here's what each platform delivered with minimal configuration:
|
84 |
+
|
85 |
+
### OpenAI Platform
|
86 |
+
|
87 |
+
- **Out-of-Box Strengths:** Native integration with OpenAI Agent SDK with zero dependencies. Clean UI optimized for tool call debugging. Real-time trace streaming.
|
88 |
+
- **Default Metadata:** Latency, tool calls (input/output), model, tokens (input/output/total), timestamps, request IDs.
|
89 |
+
- **Parent/Child Structure:** Clickable trace hierarchy shows agent → tool sequences with clear flow visualization.
|
90 |
+
- **Setup:** ~1 line of code
|
91 |
+
|
92 |
+
### Arize Phoenix
|
93 |
+
|
94 |
+
- **Out-of-Box Strengths:** Model-agnostic platform with waterfall timeline views and automatic bottleneck detection. Built-in cost analytics and session grouping.
|
95 |
+
- **Default Metadata:** Workflow names, detailed timings, token counts, cost calculations, span relationships, LLM parameters.
|
96 |
+
- **Parent/Child Structure:** Hierarchical waterfall view with automatic parent/child span linking and performance insights.
|
97 |
+
- **Setup:** ~2 lines of code
|
98 |
+
|
99 |
+
### W&B Weave
|
100 |
+
|
101 |
+
- **Out-of-Box Strengths:** Framework-agnostic platform with trace trees and built-in feedback collection systems. Automatic experiment grouping and versioning.
|
102 |
+
- **Default Metadata:** Full inputs/outputs, execution status, automatic run grouping, model versions, experiment metadata.
|
103 |
+
- **Parent/Child Structure:** Interactive tree view tracking complete workflow → operation → sub-call hierarchies.
|
104 |
+
- **Setup:** ~2 lines of code
|
105 |
+
|
106 |
+
### Comparison Summary
|
107 |
+
|
108 |
+
| Platform | Default Visualization | Key Auto-Captured Metadata | Built-in Collaboration | Setup Lines |
|
109 |
+
| --- | --- | --- | --- | --- |
|
110 |
+
| OpenAI | Chronological span log | Latency, cost, tool calls | No | ~1 line |
|
111 |
+
| Phoenix | Waterfall timeline | Latency, tokens, cost, analytics | No | ~2 lines |
|
112 |
+
| W&B Weave | Hierarchical tree view | Inputs, outputs, feedback, experiments | Yes | ~2 lines |
|
113 |
+
|
114 |
+
**The Bottom Line:** All three capture core metadata automatically. The differences lie in visualization style, analytics depth, and collaboration features.
|
115 |
+
|
116 |
+
---
|
117 |
+
|
118 |
+
## Part 4: When to Use What - Out-of-the-Box Recommendations
|
119 |
+
|
120 |
+
Based on my experiments, here's when each platform's default capabilities shine:
|
121 |
+
|
122 |
+
### For Quick Debugging: OpenAI Platform
|
123 |
+
|
124 |
+
- **Why:** Requires no additional dependencies if you're already using the OpenAI Python SDK.
|
125 |
+
- **Best Default Feature:** Clean, readable tool call traces.
|
126 |
+
- **Trade-off:** Limited analytics depth, but sometimes simple is better.
|
127 |
+
- **Perfect When:** You need to quickly verify your agent is working correctly.
|
128 |
+
|
129 |
+
### For Rich Analytics: Arize Phoenix
|
130 |
+
|
131 |
+
- **Why:** Gives you professional-grade analytics dashboards with minimal configuration.
|
132 |
+
- **Best Default Feature:** Automatic waterfall charts that immediately show performance bottlenecks.
|
133 |
+
- **Trade-off:** Slightly more setup, but the payoff in insights is immediate.
|
134 |
+
- **Perfect When:** You want to understand performance patterns without building custom dashboards.
|
135 |
+
|
136 |
+
### For Team Collaboration: W&B Weave
|
137 |
+
|
138 |
+
- **Why:** Built-in experiment tracking and team features from day one.
|
139 |
+
- **Best Default Feature:** Automatic experiment organization and sharing capabilities.
|
140 |
+
- **Trade-off:** More complex interface, but scales with team needs.
|
141 |
+
- **Perfect When:** Multiple people need to review and compare agent performance.
|
142 |
+
|
143 |
+
### The Hybrid Approach
|
144 |
+
|
145 |
+
Here's what I actually do in practice:
|
146 |
+
1. **Start with OpenAI** for immediate validation
|
147 |
+
2. **Add Phoenix** when I need deeper performance analysis
|
148 |
+
3. **Layer in Weave** when working with a team or running experiments
|
149 |
+
|
150 |
+
The beauty is that all three have generous free tiers, so you can test their default capabilities without commitment.
|
151 |
+
|
152 |
+
---
|
153 |
+
|
154 |
+
## When You Outgrow the Defaults
|
155 |
+
|
156 |
+
While these out-of-the-box capabilities are impressive, there are scenarios where you'll need custom instrumentation:
|
157 |
+
|
158 |
+
- **Custom Metrics**: Domain-specific KPIs like task completion rates or user satisfaction scores
|
159 |
+
- **Advanced Analytics**: Complex performance analysis, A/B testing, or custom dashboards
|
160 |
+
- **Specialized Workflows**: Multi-model pipelines, custom evaluation frameworks, or integration with existing monitoring systems
|
161 |
+
- **Compliance Requirements**: Specific logging formats, data retention policies, or audit trails
|
162 |
+
|
163 |
+
The good news? Starting with these default capabilities gives you a solid foundation to build upon. You'll understand your observability needs better before investing in custom solutions.
|
164 |
+
|
165 |
+
---
|
166 |
+
|
167 |
+
## Conclusion: The Power of Defaults
|
168 |
+
|
169 |
+
Here's the key insight: **you don't need to build observability from scratch**. These platforms give you professional-grade tracing capabilities with just a few lines of setup code. The question isn't whether you should instrument your foundation models—it's which platform's defaults best match your workflow.
|
170 |
+
|
171 |
+
Key takeaways:
|
172 |
+
- **Start Simple**: OpenAI's built-in tracing gets you 80% of what you need with zero overhead.
|
173 |
+
- **Scale Smart**: Phoenix and Weave offer more sophisticated defaults when you need deeper insights.
|
174 |
+
- **Mix and Match**: There's no rule saying you can't use multiple platforms—they complement each other well.
|
175 |
+
|
176 |
+
The real power here is speed to insight. Instead of spending weeks building custom observability, you can have professional-grade tracing running in minutes. That's time better spent on what actually matters: building better AI.
|
177 |
+
|
178 |
+
What's your experience with out-of-the-box observability? Have you found any hidden gems in these platforms' default features?
|
instructional_docs/community_voice.md
ADDED
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Of course\! Here is a unified guide for your blog post, combining the best of both opinions into a single, actionable reference.
|
2 |
+
|
3 |
+
# The Ultimate MLOps Blog Post Guide: Audience, Voice, and Structure
|
4 |
+
|
5 |
+
This guide synthesizes key strategies to help you write a compelling and effective blog post for the MLOps community. Use it as a checklist to ensure your content resonates with practitioners and drives engagement.
|
6 |
+
|
7 |
+
-----
|
8 |
+
|
9 |
+
## 1\. Your Target Audience: The MLOps Practitioner
|
10 |
+
|
11 |
+
This profile details who you're writing for, what they care about, and why it matters for your post.
|
12 |
+
|
13 |
+
| Dimension | Details | Why It Matters for Your Post |
|
14 |
+
| :--- | :--- | :--- |
|
15 |
+
| **Primary Roles** | • **ML/MLOps Engineers & Platform Owners** \<br\> • Data Scientists shipping models \<br\> • DevOps/SREs moving into ML \<br\> • Staff/Principal Engineers & Tech Leads | Ground your advice in real-world pipeline challenges and deployment pain points. They've been "paged at 3 a.m." and appreciate solutions that prevent it. |
|
16 |
+
| **Experience Level** | **Intermediate to Senior (3-10 years)**. They are "advanced beginners" and up—proficient but not all-knowing. They can `pip install` anything but value guidance on tricky implementations and "gotchas." | Provide a simple on-ramp for context, then dive deep into the technical details. Don't over-explain basics, but don't skip the "why" behind a specific command or parameter. |
|
17 |
+
| **Organizational Context** | Start-ups, scale-ups, and cloud-first enterprise teams. They often run on Kubernetes or managed cloud services. | **Show examples that work both locally AND in a CI/CD pipeline.** This duality is crucial for credibility and practical application. |
|
18 |
+
| **Geography** | **Global, with hubs in the US & EU.** Think San Francisco, New York, London, Berlin, etc. | Use globally available, open-source tooling. Avoid region-locked services and consider time-zone-friendly collaboration (e.g., linking to a global Slack/Discord). |
|
19 |
+
| **Core Motivations** | • **Ship reliable models to production.** This is their primary goal. \<br\> • Keep up with the fast-moving tooling landscape. \<br\> • Learn from peers and share war stories about what works (and what doesn't). | Focus on **actionable patterns** and battle-tested solutions. Frame your post as a shared learning experience. |
|
20 |
+
|
21 |
+
-----
|
22 |
+
|
23 |
+
## 2\. Voice and Tone: Your Expert-Peer Persona
|
24 |
+
|
25 |
+
Speak to your audience like a knowledgeable colleague you'd chat with on Slack—not like a stuffy academic.
|
26 |
+
|
27 |
+
* **Be Conversational, Not Corporate:** Use a witty, direct, and authentic tone. It's okay to use humor and personality, but do it sparingly. **One smart quip per section is plenty.**
|
28 |
+
* **Be Pragmatic, Not Hype-Driven:** Acknowledge trade-offs, limitations, and dependencies. Honesty builds trust. Avoid marketing fluff and overselling your solution as a silver bullet.
|
29 |
+
* **Be Code-First:** Show, don't just tell. Let runnable code snippets do most of the talking. Your prose should provide context, explain the *why* behind the code, and highlight key takeaways.
|
30 |
+
* **Empathize with Shared Pains:** Use relatable scenarios. Phrases like *"We've all been there"* or *"Ever tried explaining to your PM why..."* create an immediate connection.
|
31 |
+
|
32 |
+
-----
|
33 |
+
|
34 |
+
## 3\. The Perfect Blog Post Structure
|
35 |
+
|
36 |
+
Organize your post for maximum clarity and skimmability. Your reader is busy and wants to get to the good stuff—fast.
|
37 |
+
|
38 |
+
1. **The Hook (1-2 Sentences):** Start with a catchy, relatable one-liner that references a common pain point.
|
39 |
+
|
40 |
+
* *Example: "Ever tried explaining to your PM why the 'simple' model update broke prod at 3am? (Spoiler: it's never simple when feature schemas drift.)"*
|
41 |
+
|
42 |
+
2. **The Problem & Solution (1 Paragraph):** Briefly state the problem you're solving in plain English. Then, introduce your solution and what it accomplishes.
|
43 |
+
|
44 |
+
* *Example: "Here's how we built a feature validation pipeline that catches schema changes before they wake you up."*
|
45 |
+
|
46 |
+
3. **The Quick-Start (Code Block):** Give them an immediate win. Provide a copy-pasteable code block that gets them set up in under two minutes.
|
47 |
+
|
48 |
+
* *Example:*
|
49 |
+
```bash
|
50 |
+
# Quick start
|
51 |
+
pip install feature-guardian
|
52 |
+
feature-guardian init --prod-schema=s3://your-bucket/schema.json
|
53 |
+
```
|
54 |
+
|
55 |
+
4. **The Implementation Deep-Dive (Bulleted Steps & Code):** This is the core of your post.
|
56 |
+
|
57 |
+
* Use bullet points, numbered lists, and subheadings.
|
58 |
+
* Embed heavily commented code blocks (Python, YAML, Shell).
|
59 |
+
* Use emojis (like ✅) and **bolding** to guide the reader's eye.
|
60 |
+
* Explain the *why* for each key parameter or step.
|
61 |
+
|
62 |
+
5. **Pros, Cons, and Limitations (Bulleted List):** Honestly assess your solution. What are the trade-offs? Where is it still maturing?
|
63 |
+
|
64 |
+
6. **The Call to Action (CTA):** Invite readers to continue the conversation and try it themselves.
|
65 |
+
|
66 |
+
* Link to the full GitHub repo, further tutorials, or a relevant Slack/Discord channel.
|
67 |
+
* Ask for feedback, stories of their own failures, or pull requests.
|
68 |
+
|
69 |
+
-----
|
70 |
+
|
71 |
+
## 4\. Content & Style Cheat-Sheet
|
72 |
+
|
73 |
+
Use this final checklist to polish your post.
|
74 |
+
|
75 |
+
| ✅ DO | ❌ DON'T |
|
76 |
+
| :--- | :--- |
|
77 |
+
| **Provide working code** for specific versions (`Python 3.10+`, etc.). | **Oversell or use marketing hype.** |
|
78 |
+
| **Explain the "why"** behind each parameter and decision. | **Write long, fluffy introductions.** Get to the point. |
|
79 |
+
| **Share both successes and failures.** Be honest about limitations. | **Assume unlimited budgets or resources.** |
|
80 |
+
| **Reference real-world scenarios** and production pain points. | **Use region-locked or proprietary services** without an open-source alternative. |
|
81 |
+
| **Add personality** and humor sparingly. | **Over-explain programming basics.** |
|
82 |
+
| **Invite community feedback** and discussion. | **Write a "white-paper."** Keep it practical. |
|
main.py
ADDED
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Entry point for the command-line interface (CLI) of the todo-agent.
|
3 |
+
|
4 |
+
This script demonstrates a typical setup for a stateful, conversational agent:
|
5 |
+
- Loads environment variables for API keys and configuration.
|
6 |
+
- Initializes tracing and observability integrations (Phoenix, Weave).
|
7 |
+
- Manages conversation history by saving and loading it from a JSON file.
|
8 |
+
- Creates an agent with a file-based storage backend (`JsonTodoStorage`).
|
9 |
+
- Runs a loop to interact with the user via the command line.
|
10 |
+
"""
|
11 |
+
|
12 |
+
# Standard library imports
|
13 |
+
import os
|
14 |
+
import asyncio
|
15 |
+
import json
|
16 |
+
|
17 |
+
# Third-party imports
|
18 |
+
from dotenv import load_dotenv
|
19 |
+
from phoenix.otel import register
|
20 |
+
import weave
|
21 |
+
from agents import Runner, Agent
|
22 |
+
|
23 |
+
# Local application imports
|
24 |
+
from agent.todo_agent import create_agent
|
25 |
+
from agent.storage import JsonTodoStorage
|
26 |
+
|
27 |
+
# --- Initial Setup ---
|
28 |
+
# Load environment variables from a .env file. This is a best practice for
|
29 |
+
# managing secrets and configuration without hardcoding them in the source code.
|
30 |
+
load_dotenv()
|
31 |
+
|
32 |
+
# --- Tracing & Observation Setup ---
|
33 |
+
# Initialize integrations to observe and debug the agent's behavior.
|
34 |
+
# This is crucial for understanding the agent's decision-making process.
|
35 |
+
|
36 |
+
def initialize_tracing():
|
37 |
+
"""Initialize tracing with graceful error handling."""
|
38 |
+
os.environ["OPENAI_TRACING_ENABLED"] = "1"
|
39 |
+
os.environ["WEAVE_PRINT_CALL_LINK"] = "false"
|
40 |
+
|
41 |
+
# Phoenix: Add minimal custom resource attributes via environment variable
|
42 |
+
os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "app.name=todo-agent,tutorial.type=production,environment=production,interface=cli"
|
43 |
+
|
44 |
+
try:
|
45 |
+
register(project_name="todo-agent-cli", auto_instrument=True)
|
46 |
+
print("✅ Phoenix tracing initialized for: todo-agent-cli")
|
47 |
+
except Exception as e:
|
48 |
+
print(f"⚠️ Phoenix tracing failed: {e}")
|
49 |
+
|
50 |
+
if not weave.get_client():
|
51 |
+
try:
|
52 |
+
weave.init("todo-agent-cli")
|
53 |
+
print("✅ Weave tracing initialized for: todo-agent-cli")
|
54 |
+
except Exception as e:
|
55 |
+
print(f"⚠️ Weave tracing failed (continuing without Weave): {e}")
|
56 |
+
|
57 |
+
initialize_tracing()
|
58 |
+
|
59 |
+
# -----------------------------------------------------------------------------
|
60 |
+
# Session Management
|
61 |
+
#
|
62 |
+
# To create a stateful conversation, we save/load the message history
|
63 |
+
# to a JSON file, allowing the agent to "remember" past interactions.
|
64 |
+
# -----------------------------------------------------------------------------
|
65 |
+
SESSION_FILE = "data/session_default.json"
|
66 |
+
MAX_TURNS = 12 # Max *user* turns to keep in history to prevent token overflow.
|
67 |
+
|
68 |
+
def load_session() -> list:
|
69 |
+
"""Loads the message history from the session file."""
|
70 |
+
try:
|
71 |
+
with open(SESSION_FILE, "r") as f:
|
72 |
+
data = json.load(f)
|
73 |
+
# Return the history if it exists, otherwise an empty list.
|
74 |
+
return data.get("history", [])
|
75 |
+
except (FileNotFoundError, json.JSONDecodeError):
|
76 |
+
# If the file doesn't exist or is empty/corrupt, start a new session.
|
77 |
+
return []
|
78 |
+
|
79 |
+
def save_session(history: list):
|
80 |
+
"""Saves the message history to the session file."""
|
81 |
+
# Ensure the 'data' directory exists.
|
82 |
+
os.makedirs(os.path.dirname(SESSION_FILE), exist_ok=True)
|
83 |
+
with open(SESSION_FILE, "w") as f:
|
84 |
+
# Save the history in a structured format.
|
85 |
+
json.dump({"history": history}, f, indent=2)
|
86 |
+
|
87 |
+
async def main():
|
88 |
+
# Load the previous conversation history to maintain context.
|
89 |
+
history = load_session()
|
90 |
+
|
91 |
+
# Create the agent instance using the central factory,
|
92 |
+
# providing it with the file-based storage system.
|
93 |
+
agent = create_agent(
|
94 |
+
storage=JsonTodoStorage(),
|
95 |
+
agent_name="To-Do Agent (CLI)"
|
96 |
+
)
|
97 |
+
print("To-Do Agent (CLI) is ready. Tracing is enabled. Type 'exit' to quit.")
|
98 |
+
|
99 |
+
# Start the main interaction loop.
|
100 |
+
while True:
|
101 |
+
user_input = input("\nYou: ")
|
102 |
+
if user_input.strip().lower() in ("exit", "quit"):
|
103 |
+
print("Goodbye!")
|
104 |
+
break
|
105 |
+
|
106 |
+
# Add the new user message to the history.
|
107 |
+
history.append({"role": "user", "content": user_input})
|
108 |
+
|
109 |
+
# --- Context Window Management ---
|
110 |
+
# To prevent token overflow, we trim the history to the last `MAX_TURNS`.
|
111 |
+
user_message_indices = [i for i, msg in enumerate(history) if msg.get("role") == "user"]
|
112 |
+
if len(user_message_indices) > MAX_TURNS:
|
113 |
+
# Find the index of the oldest user message to keep.
|
114 |
+
start_index = user_message_indices[-MAX_TURNS]
|
115 |
+
print(f"(Trimming conversation history to the last {MAX_TURNS} turns...)")
|
116 |
+
history = history[start_index:]
|
117 |
+
|
118 |
+
# --- Agent Execution ---
|
119 |
+
# The Runner handles the conversation turn, calling tools and the LLM.
|
120 |
+
result = await Runner.run(
|
121 |
+
agent,
|
122 |
+
input=history,
|
123 |
+
)
|
124 |
+
print("----"*10)
|
125 |
+
print(f"Agent: {result.final_output}")
|
126 |
+
print("===="*10)
|
127 |
+
|
128 |
+
# The agent's result contains the full, updated history (user, assistant, tools).
|
129 |
+
# We replace our local history with this to prepare for the next turn.
|
130 |
+
history = result.to_input_list()
|
131 |
+
|
132 |
+
# Save the updated history to disk to maintain state for the next session.
|
133 |
+
save_session(history)
|
134 |
+
|
135 |
+
if __name__ == "__main__":
|
136 |
+
# Run the asynchronous main function.
|
137 |
+
asyncio.run(main())
|
manage.py
ADDED
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Main entry point for data management tasks.
|
2 |
+
|
3 |
+
"""
|
4 |
+
Management script for the todo-agent project.
|
5 |
+
|
6 |
+
Provides CLI commands for common administrative tasks like resetting data,
|
7 |
+
seeding the database, and running evaluations.
|
8 |
+
"""
|
9 |
+
|
10 |
+
import typer
|
11 |
+
import json
|
12 |
+
import os
|
13 |
+
|
14 |
+
app = typer.Typer(
|
15 |
+
help="A CLI for managing the to-do agent application.",
|
16 |
+
add_completion=False
|
17 |
+
)
|
18 |
+
|
19 |
+
TODOS_PATH = os.path.join("data", "todos.json")
|
20 |
+
SESSION_PATH = os.path.join("data", "session_default.json")
|
21 |
+
DEFAULT_SEED_PATH = os.path.join("data", "seed_todos.json")
|
22 |
+
|
23 |
+
@app.command()
|
24 |
+
def reset(
|
25 |
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt.")
|
26 |
+
):
|
27 |
+
"""
|
28 |
+
Resets the to-do list and session history to a clean state.
|
29 |
+
"""
|
30 |
+
if not yes:
|
31 |
+
confirm = typer.confirm("Are you sure you want to delete all to-dos and session history?")
|
32 |
+
if not confirm:
|
33 |
+
print("Aborting.")
|
34 |
+
raise typer.Abort()
|
35 |
+
|
36 |
+
# Reset todos.json to an empty list
|
37 |
+
with open(TODOS_PATH, "w") as f:
|
38 |
+
json.dump([], f)
|
39 |
+
|
40 |
+
# Reset session_default.json to an empty history
|
41 |
+
with open(SESSION_PATH, "w") as f:
|
42 |
+
json.dump({"history": []}, f)
|
43 |
+
|
44 |
+
print("✅ To-do list and session history have been reset.")
|
45 |
+
|
46 |
+
|
47 |
+
@app.command()
|
48 |
+
def seed(
|
49 |
+
file_path: str = typer.Argument(DEFAULT_SEED_PATH, help="Path to the seed JSON file.")
|
50 |
+
):
|
51 |
+
"""
|
52 |
+
Seeds the to-do list with data from a JSON file.
|
53 |
+
|
54 |
+
This command will overwrite the current to-do list.
|
55 |
+
"""
|
56 |
+
# If a filename is provided without a directory, assume it's in the data/ directory.
|
57 |
+
if not os.path.dirname(file_path):
|
58 |
+
file_path = os.path.join("data", file_path)
|
59 |
+
|
60 |
+
if not os.path.exists(file_path):
|
61 |
+
print(f"Error: Seed file not found at '{file_path}'")
|
62 |
+
raise typer.Exit(code=1)
|
63 |
+
|
64 |
+
with open(file_path, "r") as f:
|
65 |
+
seed_data = json.load(f)
|
66 |
+
|
67 |
+
with open(TODOS_PATH, "w") as f:
|
68 |
+
json.dump(seed_data, f, indent=2)
|
69 |
+
|
70 |
+
print(f"✅ To-do list has been seeded from '{file_path}'.")
|
71 |
+
|
72 |
+
|
73 |
+
if __name__ == "__main__":
|
74 |
+
app()
|
pyproject.toml
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "todo-agent"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Add your description here"
|
5 |
+
readme = "README.md"
|
6 |
+
requires-python = ">=3.12"
|
7 |
+
dependencies = [
|
8 |
+
"arize-phoenix-otel>=0.12.1",
|
9 |
+
"openai-agents>=0.1.0",
|
10 |
+
"openai>=1.93.0",
|
11 |
+
"pydantic>=2.11.7",
|
12 |
+
"python-dotenv>=1.1.1",
|
13 |
+
"weave>=0.51.54",
|
14 |
+
"arize-phoenix>=11.2.0",
|
15 |
+
"openinference-instrumentation-openai-agents>=0.1.14",
|
16 |
+
"typer[all]>=0.16.0",
|
17 |
+
"gradio>=5.35.0",
|
18 |
+
"pandas>=2.3.0",
|
19 |
+
]
|
requirements.txt
ADDED
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
aiofiles==24.1.0
|
2 |
+
aiohappyeyeballs==2.6.1
|
3 |
+
aiohttp==3.12.13
|
4 |
+
aioitertools==0.12.0
|
5 |
+
aiosignal==1.3.2
|
6 |
+
aiosqlite==0.21.0
|
7 |
+
alembic==1.16.2
|
8 |
+
annotated-types==0.7.0
|
9 |
+
anyio==4.9.0
|
10 |
+
arize-phoenix==11.2.0
|
11 |
+
arize-phoenix-client==1.11.0
|
12 |
+
arize-phoenix-evals==0.21.0
|
13 |
+
arize-phoenix-otel==0.12.1
|
14 |
+
attrs==25.3.0
|
15 |
+
authlib==1.6.0
|
16 |
+
backoff==2.2.1
|
17 |
+
cachetools==6.1.0
|
18 |
+
certifi==2025.6.15
|
19 |
+
cffi==1.17.1
|
20 |
+
charset-normalizer==3.4.2
|
21 |
+
click==8.2.1
|
22 |
+
colorama==0.4.6
|
23 |
+
cryptography==45.0.4
|
24 |
+
diskcache==5.6.3
|
25 |
+
distro==1.9.0
|
26 |
+
dnspython==2.7.0
|
27 |
+
email-validator==2.2.0
|
28 |
+
emoji==2.14.1
|
29 |
+
fastapi==0.115.14
|
30 |
+
ffmpy==0.6.0
|
31 |
+
filelock==3.18.0
|
32 |
+
frozenlist==1.7.0
|
33 |
+
fsspec==2025.5.1
|
34 |
+
gitdb==4.0.12
|
35 |
+
gitpython==3.1.44
|
36 |
+
googleapis-common-protos==1.70.0
|
37 |
+
gql==3.5.3
|
38 |
+
gradio==5.35.0
|
39 |
+
gradio-client==1.10.4
|
40 |
+
graphql-core==3.2.6
|
41 |
+
greenlet==3.2.3
|
42 |
+
griffe==1.7.3
|
43 |
+
groovy==0.1.2
|
44 |
+
grpc-interceptor==0.15.4
|
45 |
+
grpcio==1.73.1
|
46 |
+
h11==0.16.0
|
47 |
+
hf-xet==1.1.5
|
48 |
+
httpcore==1.0.9
|
49 |
+
httpx==0.28.1
|
50 |
+
httpx-sse==0.4.1
|
51 |
+
huggingface-hub==0.33.2
|
52 |
+
idna==3.10
|
53 |
+
importlib-metadata==8.7.0
|
54 |
+
jinja2==3.1.6
|
55 |
+
jiter==0.10.0
|
56 |
+
joblib==1.5.1
|
57 |
+
jsonschema==4.24.0
|
58 |
+
jsonschema-specifications==2025.4.1
|
59 |
+
mako==1.3.10
|
60 |
+
markdown-it-py==3.0.0
|
61 |
+
markupsafe==3.0.2
|
62 |
+
mcp==1.10.1
|
63 |
+
mdurl==0.1.2
|
64 |
+
multidict==6.6.3
|
65 |
+
nest-asyncio==1.6.0
|
66 |
+
numpy==2.3.1
|
67 |
+
openai==1.93.0
|
68 |
+
openai-agents==0.1.0
|
69 |
+
openinference-instrumentation==0.1.34
|
70 |
+
openinference-instrumentation-openai-agents==0.1.14
|
71 |
+
openinference-semantic-conventions==0.1.21
|
72 |
+
opentelemetry-api==1.34.1
|
73 |
+
opentelemetry-exporter-otlp==1.34.1
|
74 |
+
opentelemetry-exporter-otlp-proto-common==1.34.1
|
75 |
+
opentelemetry-exporter-otlp-proto-grpc==1.34.1
|
76 |
+
opentelemetry-exporter-otlp-proto-http==1.34.1
|
77 |
+
opentelemetry-instrumentation==0.55b1
|
78 |
+
opentelemetry-proto==1.34.1
|
79 |
+
opentelemetry-sdk==1.34.1
|
80 |
+
opentelemetry-semantic-conventions==0.55b1
|
81 |
+
orjson==3.10.18
|
82 |
+
packaging==25.0
|
83 |
+
pandas==2.3.0
|
84 |
+
pandas-stubs==2.3.0.250703
|
85 |
+
pillow==11.3.0
|
86 |
+
platformdirs==4.3.8
|
87 |
+
propcache==0.3.2
|
88 |
+
protobuf==5.29.5
|
89 |
+
psutil==7.0.0
|
90 |
+
pyarrow==20.0.0
|
91 |
+
pycparser==2.22
|
92 |
+
pydantic==2.11.7
|
93 |
+
pydantic-core==2.33.2
|
94 |
+
pydantic-settings==2.10.1
|
95 |
+
pydub==0.25.1
|
96 |
+
pygments==2.19.2
|
97 |
+
python-dateutil==2.9.0.post0
|
98 |
+
python-dotenv==1.1.1
|
99 |
+
python-multipart==0.0.20
|
100 |
+
pytz==2025.2
|
101 |
+
pyyaml==6.0.2
|
102 |
+
referencing==0.36.2
|
103 |
+
requests==2.32.4
|
104 |
+
requests-toolbelt==1.0.0
|
105 |
+
rich==14.0.0
|
106 |
+
rpds-py==0.25.1
|
107 |
+
ruff==0.12.2
|
108 |
+
safehttpx==0.1.6
|
109 |
+
scikit-learn==1.7.0
|
110 |
+
scipy==1.16.0
|
111 |
+
semantic-version==2.10.0
|
112 |
+
sentry-sdk==2.32.0
|
113 |
+
setproctitle==1.3.6
|
114 |
+
shellingham==1.5.4
|
115 |
+
six==1.17.0
|
116 |
+
smmap==5.0.2
|
117 |
+
sniffio==1.3.1
|
118 |
+
sqlalchemy==2.0.41
|
119 |
+
sqlean-py==3.49.1
|
120 |
+
sse-starlette==2.3.6
|
121 |
+
starlette==0.46.2
|
122 |
+
strawberry-graphql==0.270.1
|
123 |
+
tenacity==9.1.2
|
124 |
+
threadpoolctl==3.6.0
|
125 |
+
tomlkit==0.13.3
|
126 |
+
tqdm==4.67.1
|
127 |
+
typer==0.16.0
|
128 |
+
types-pytz==2025.2.0.20250516
|
129 |
+
types-requests==2.32.4.20250611
|
130 |
+
typing-extensions==4.14.0
|
131 |
+
typing-inspection==0.4.1
|
132 |
+
tzdata==2025.2
|
133 |
+
urllib3==2.5.0
|
134 |
+
uvicorn==0.35.0
|
135 |
+
wandb==0.20.1
|
136 |
+
weave==0.51.54
|
137 |
+
websockets==15.0.1
|
138 |
+
wrapt==1.17.2
|
139 |
+
yarl==1.20.1
|
140 |
+
zipp==3.23.0
|
tests/README.md
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Todo-Agent Tutorial Series
|
2 |
+
|
3 |
+
Progressive tutorial series for mastering AI agent workflows through realistic article planning.
|
4 |
+
|
5 |
+
## 🎓 Tutorial Series
|
6 |
+
|
7 |
+
| Tutorial | Description | What You Learn |
|
8 |
+
|----------|-------------|----------------|
|
9 |
+
| `test_basic_crud.py` | **Article Foundation Setup** | Essential CRUD operations while planning observability article structure |
|
10 |
+
| `test_web_search_brainstorming.py` | **Platform Research & Planning** | Web search integration for researching observability platforms |
|
11 |
+
| `test_natural_language.py` | **Project Completion Prep** | Natural language flexibility for finishing article tasks |
|
12 |
+
|
13 |
+
## 🚀 Running the Tutorials
|
14 |
+
|
15 |
+
### Complete Tutorial Series (Recommended)
|
16 |
+
```bash
|
17 |
+
uv run tests/run_demo_tests.py
|
18 |
+
```
|
19 |
+
|
20 |
+
### Individual Tutorials
|
21 |
+
```bash
|
22 |
+
# 1. Basic CRUD Tutorial - Learn essential operations
|
23 |
+
uv run tests/run_demo_tests.py basic
|
24 |
+
|
25 |
+
# 2. Platform Research Tutorial - Web search workflow
|
26 |
+
uv run tests/run_demo_tests.py research
|
27 |
+
|
28 |
+
# 3. Natural Language Tutorial - Casual conversation
|
29 |
+
uv run tests/run_demo_tests.py language
|
30 |
+
```
|
31 |
+
|
32 |
+
### Additional Options
|
33 |
+
```bash
|
34 |
+
# Generate tutorial report from existing logs
|
35 |
+
uv run tests/run_demo_tests.py --report
|
36 |
+
```
|
37 |
+
|
38 |
+
### Direct Tutorial Execution
|
39 |
+
```bash
|
40 |
+
uv run tests/test_basic_crud.py
|
41 |
+
uv run tests/test_web_search_brainstorming.py
|
42 |
+
uv run tests/test_natural_language.py
|
43 |
+
```
|
44 |
+
|
45 |
+
## 📚 Tutorial Learning Progression
|
46 |
+
|
47 |
+
### 1. Basic CRUD Tutorial (4 turns)
|
48 |
+
**Goal**: Learn essential todo operations while setting up article structure
|
49 |
+
- Create structured writing tasks with descriptions
|
50 |
+
- Organize tasks by project for better workflow
|
51 |
+
- Update task status and add progress notes
|
52 |
+
- Build article foundations systematically
|
53 |
+
|
54 |
+
**Focus**: Observability platforms comparison article planning
|
55 |
+
|
56 |
+
### 2. Platform Research Tutorial (4 turns)
|
57 |
+
**Goal**: Research workflow → structured task creation
|
58 |
+
- **Turn 1**: Search "Arize Phoenix Cloud main benefits" → 2 paragraph summary
|
59 |
+
- **Turn 2**: Search "Weights & Biases Weave main benefits" → 2 paragraph summary
|
60 |
+
- **Turn 3**: Search "OpenAI platform observability features" → 2 paragraph summary
|
61 |
+
- **Turn 4**: Convert research into structured writing tasks
|
62 |
+
|
63 |
+
**Focus**: Research stays in chat history, todos become actionable writing tasks
|
64 |
+
|
65 |
+
### 3. Natural Language Tutorial (4 turns)
|
66 |
+
**Goal**: Project completion with casual, natural conversation
|
67 |
+
- Handle typos gracefully ('everthing' → 'everything')
|
68 |
+
- Process casual language: 'hey', 'lemme see', 'gotta make sure'
|
69 |
+
- Context understanding: 'that proofreading task' references previous todo
|
70 |
+
- Natural conversation flow with task modifications
|
71 |
+
|
72 |
+
**Focus**: Finishing article with editing and publishing tasks
|
73 |
+
|
74 |
+
## 📊 Tutorial Logging & Reporting
|
75 |
+
|
76 |
+
Each tutorial automatically logs structured results with:
|
77 |
+
- Tutorial execution time and duration
|
78 |
+
- Turn-by-turn conversation tracking
|
79 |
+
- Learning objectives and outcomes
|
80 |
+
- Pass/fail status with detailed error reporting
|
81 |
+
- Automatic tutorial report generation
|
82 |
+
|
83 |
+
### Tutorial Logs Location
|
84 |
+
- `tests/logs/test_results.jsonl` - Individual tutorial results
|
85 |
+
- `tests/logs/test_suite_results.jsonl` - Tutorial series summaries
|
86 |
+
- `tests/logs/test_report.md` - Human-readable tutorial report
|
87 |
+
|
88 |
+
### Understanding Tutorial Results
|
89 |
+
Each tutorial includes minimal validation (basic sanity checks):
|
90 |
+
- All tutorials: Simply verify that some todos were created during the conversation
|
91 |
+
- **Real evaluation happens in your tracing dashboards** - use the observability tools to assess quality and performance
|
92 |
+
|
93 |
+
## 🔄 Data Management
|
94 |
+
|
95 |
+
### When Data Gets Reset
|
96 |
+
- All tutorials: Each tutorial automatically resets data before running for clean results
|
97 |
+
- Individual tutorials: Run with fresh data every time
|
98 |
+
- Tutorial series: Each tutorial in the series gets fresh data
|
99 |
+
|
100 |
+
### Data Persistence
|
101 |
+
- During tutorials: Data accumulates naturally through the tutorial conversation
|
102 |
+
- After tutorials: Data persists in `data/` directory for inspection
|
103 |
+
- Logs: Tutorial results and reports are preserved in `tests/logs/`
|
104 |
+
|
105 |
+
### Manual Data Control
|
106 |
+
```bash
|
107 |
+
uv run python -c "
|
108 |
+
import os, json
|
109 |
+
os.makedirs('data', exist_ok=True)
|
110 |
+
with open('data/todos.json', 'w') as f: json.dump([], f)
|
111 |
+
with open('data/session_default.json', 'w') as f: json.dump({'history': []}, f)
|
112 |
+
"
|
113 |
+
```
|
114 |
+
|
115 |
+
## 🔍 Observability & Tracing
|
116 |
+
|
117 |
+
Each tutorial uses separate tracing projects for clean observation:
|
118 |
+
- OpenAI Platform: Native tracing enabled
|
119 |
+
- Arize Phoenix Cloud: Projects `todo-agent-crud-tutorial`, `todo-agent-research-tutorial`, `todo-agent-language-tutorial`
|
120 |
+
- W&B Weave: Individual tracking for each tutorial
|
121 |
+
|
122 |
+
Check your tracing dashboards to see:
|
123 |
+
- Agent decision-making process and tool usage patterns
|
124 |
+
- Web search API calls and response processing
|
125 |
+
- Natural language interpretation and normalization
|
126 |
+
- Performance metrics across different conversation styles
|
127 |
+
|
128 |
+
## 🎯 Learning Objectives
|
129 |
+
|
130 |
+
This tutorial series teaches core AI engineering concepts through realistic workflows:
|
131 |
+
|
132 |
+
1. **Agent Architecture**: How to build conversational AI for content creation and project management
|
133 |
+
2. **Tool Design Patterns**: CRUD operations, web search integration, and schema validation
|
134 |
+
3. ****Observability First**: Use tracing dashboards to evaluate agent quality, not hardcoded validation
|
135 |
+
4. **Workflow Management**: Multi-project organization and realistic task progression patterns
|
136 |
+
5. **Natural Language Processing**: Handling casual input, typos, and collaborative interactions
|
137 |
+
|
138 |
+
## 💡 Key Takeaways
|
139 |
+
|
140 |
+
- **Progressive Learning**: Each tutorial builds on the previous, from basic operations to advanced workflows
|
141 |
+
- **Realistic Scenarios**: Actually plan an observability article while learning agent capabilities
|
142 |
+
- **Observability Over Validation**: Use tracing dashboards to evaluate agent quality, not rigid programmatic checks
|
143 |
+
- **Natural Language Robustness**: Agents handle casual input, typos, and informal language gracefully
|
144 |
+
- **Research Integration**: Web search becomes structured task planning, not information dumping
|
145 |
+
- **Educational Value**: Learn AI engineering concepts through practical, hands-on tutorials
|
146 |
+
|
tests/run_demo_tests.py
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Test Runner for Todo Agent Tutorials
|
3 |
+
Runs the progressive AI agent tutorial series with console-based reporting.
|
4 |
+
"""
|
5 |
+
|
6 |
+
import os
|
7 |
+
import json
|
8 |
+
import asyncio
|
9 |
+
import argparse
|
10 |
+
from datetime import datetime
|
11 |
+
from pathlib import Path
|
12 |
+
from test_basic_crud import run_basic_crud_test
|
13 |
+
from test_web_search_brainstorming import run_web_search_test
|
14 |
+
from test_natural_language import run_natural_language_test
|
15 |
+
from opentelemetry import trace
|
16 |
+
|
17 |
+
|
18 |
+
def reset_data():
|
19 |
+
"""Reset todos and session data for clean test runs."""
|
20 |
+
os.makedirs("data", exist_ok=True)
|
21 |
+
|
22 |
+
with open("data/todos.json", "w") as f:
|
23 |
+
json.dump([], f)
|
24 |
+
|
25 |
+
with open("data/session_default.json", "w") as f:
|
26 |
+
json.dump({"history": []}, f)
|
27 |
+
|
28 |
+
print("🔄 Data reset - starting with clean slate")
|
29 |
+
|
30 |
+
|
31 |
+
async def run_tutorial(tutorial_name):
|
32 |
+
"""Run a specific tutorial with timing."""
|
33 |
+
start_time = datetime.now()
|
34 |
+
|
35 |
+
print(f"\n🔄 Starting {tutorial_name.replace('_', ' ').title()}")
|
36 |
+
print("-" * 40)
|
37 |
+
|
38 |
+
try:
|
39 |
+
if tutorial_name == "basic":
|
40 |
+
success = await run_basic_crud_test()
|
41 |
+
elif tutorial_name == "research":
|
42 |
+
success = await run_web_search_test()
|
43 |
+
elif tutorial_name == "language":
|
44 |
+
success = await run_natural_language_test()
|
45 |
+
else:
|
46 |
+
print(f"❌ Unknown tutorial: {tutorial_name}")
|
47 |
+
print("Available tutorials: basic, research, language, all")
|
48 |
+
return False
|
49 |
+
|
50 |
+
end_time = datetime.now()
|
51 |
+
duration = (end_time - start_time).total_seconds()
|
52 |
+
|
53 |
+
status = "✅ PASSED" if success else "❌ FAILED"
|
54 |
+
print(f"{status} {tutorial_name.replace('_', ' ').title()} ({duration:.1f}s)")
|
55 |
+
|
56 |
+
return success
|
57 |
+
|
58 |
+
except Exception as e:
|
59 |
+
end_time = datetime.now()
|
60 |
+
duration = (end_time - start_time).total_seconds()
|
61 |
+
print(f"❌ {tutorial_name.replace('_', ' ').title()} failed with error: {e} ({duration:.1f}s)")
|
62 |
+
return False
|
63 |
+
|
64 |
+
|
65 |
+
async def run_all_tutorials():
|
66 |
+
"""Run all tutorials in sequence with timing."""
|
67 |
+
suite_start_time = datetime.now()
|
68 |
+
|
69 |
+
print("🚀 Running All Todo Agent Tutorials")
|
70 |
+
print("=" * 60)
|
71 |
+
print("🎓 Progressive tutorial series for AI agent mastery:")
|
72 |
+
print("• Writing Article Foundation: Essential CRUD operations")
|
73 |
+
print("• Observability Platform Research: Web search workflow")
|
74 |
+
print("• Finishing Article Project: Natural language conversation")
|
75 |
+
print("=" * 60)
|
76 |
+
|
77 |
+
tutorials = [
|
78 |
+
("basic", "Writing Article Foundation"),
|
79 |
+
("research", "Observability Platform Research"),
|
80 |
+
("language", "Finishing Article Project")
|
81 |
+
]
|
82 |
+
|
83 |
+
results = []
|
84 |
+
|
85 |
+
for tutorial_name, tutorial_description in tutorials:
|
86 |
+
try:
|
87 |
+
success = await run_tutorial(tutorial_name)
|
88 |
+
results.append({"name": tutorial_name, "description": tutorial_description, "success": success})
|
89 |
+
|
90 |
+
except Exception as e:
|
91 |
+
print(f"❌ {tutorial_description} failed with error: {e}")
|
92 |
+
results.append({"name": tutorial_name, "description": tutorial_description, "success": False})
|
93 |
+
|
94 |
+
# Shutdown tracer for re-initialization
|
95 |
+
trace.get_tracer_provider().shutdown()
|
96 |
+
await asyncio.sleep(1)
|
97 |
+
|
98 |
+
suite_end_time = datetime.now()
|
99 |
+
suite_duration = (suite_end_time - suite_start_time).total_seconds()
|
100 |
+
passed = sum(1 for r in results if r["success"])
|
101 |
+
total = len(results)
|
102 |
+
|
103 |
+
print("\n" + "=" * 60)
|
104 |
+
print("📊 Tutorial Series Results")
|
105 |
+
print("=" * 60)
|
106 |
+
|
107 |
+
for result in results:
|
108 |
+
status = "✅ PASSED" if result["success"] else "❌ FAILED"
|
109 |
+
print(f"{status} {result['description']}")
|
110 |
+
|
111 |
+
success_rate = (passed / total * 100) if total > 0 else 0
|
112 |
+
print(f"\n📈 Overall: {passed}/{total} tutorials completed successfully")
|
113 |
+
print(f"🎯 Success Rate: {success_rate:.1f}%")
|
114 |
+
print(f"⏱️ Total Duration: {suite_duration:.1f}s")
|
115 |
+
|
116 |
+
if passed == total:
|
117 |
+
print("\n🎉 Tutorial series complete! You've mastered the todo agent!")
|
118 |
+
else:
|
119 |
+
print("\n🔧 Some tutorials had issues - check the output above for details.")
|
120 |
+
|
121 |
+
return passed == total
|
122 |
+
|
123 |
+
|
124 |
+
async def main():
|
125 |
+
"""Main entry point."""
|
126 |
+
parser = argparse.ArgumentParser(description="Run todo-agent tutorial series")
|
127 |
+
parser.add_argument(
|
128 |
+
"tutorial",
|
129 |
+
nargs="?",
|
130 |
+
choices=["basic", "research", "language", "all"],
|
131 |
+
default="all",
|
132 |
+
help="Tutorial to run: basic, research, language, or all (default: all)"
|
133 |
+
)
|
134 |
+
|
135 |
+
args = parser.parse_args()
|
136 |
+
|
137 |
+
if args.tutorial == "all":
|
138 |
+
success = await run_all_tutorials()
|
139 |
+
else:
|
140 |
+
success = await run_tutorial(args.tutorial)
|
141 |
+
|
142 |
+
exit(0 if success else 1)
|
143 |
+
|
144 |
+
|
145 |
+
if __name__ == "__main__":
|
146 |
+
asyncio.run(main())
|
tests/test_basic_crud.py
ADDED
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Basic CRUD Operations Test
|
3 |
+
Tutorial: Learn core todo app operations while planning an observability article.
|
4 |
+
"""
|
5 |
+
|
6 |
+
import os
|
7 |
+
import sys
|
8 |
+
import asyncio
|
9 |
+
import json
|
10 |
+
from pathlib import Path
|
11 |
+
from datetime import datetime
|
12 |
+
from dotenv import load_dotenv
|
13 |
+
from phoenix.otel import register
|
14 |
+
import weave
|
15 |
+
from agents import Runner, Agent
|
16 |
+
|
17 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
18 |
+
from agent.todo_agent import create_agent
|
19 |
+
from agent.storage import JsonTodoStorage
|
20 |
+
|
21 |
+
|
22 |
+
def reset_test_data():
|
23 |
+
"""Reset todos and session data for clean test runs."""
|
24 |
+
os.makedirs("data", exist_ok=True)
|
25 |
+
|
26 |
+
with open("data/todos.json", "w") as f:
|
27 |
+
json.dump([], f)
|
28 |
+
|
29 |
+
with open("data/session_default.json", "w") as f:
|
30 |
+
json.dump({"history": []}, f)
|
31 |
+
|
32 |
+
print("🔄 Data reset - starting with clean slate")
|
33 |
+
|
34 |
+
|
35 |
+
|
36 |
+
def initialize_tracing(project_name: str):
|
37 |
+
"""Initialize tracing with graceful error handling."""
|
38 |
+
os.environ["OPENAI_TRACING_ENABLED"] = "1"
|
39 |
+
os.environ["WEAVE_PRINT_CALL_LINK"] = "false"
|
40 |
+
|
41 |
+
# Phoenix: Add minimal custom resource attributes via environment variable
|
42 |
+
os.environ["OTEL_RESOURCE_ATTRIBUTES"] = f"tutorial.name={project_name},tutorial.type=basic_crud,environment=test,app.name=todo-agent"
|
43 |
+
|
44 |
+
try:
|
45 |
+
register(project_name=project_name, auto_instrument=True)
|
46 |
+
print(f"✅ Phoenix tracing initialized for: {project_name}")
|
47 |
+
except Exception as e:
|
48 |
+
print(f"⚠️ Phoenix tracing failed: {e}")
|
49 |
+
|
50 |
+
if not weave.get_client():
|
51 |
+
try:
|
52 |
+
weave.init(project_name)
|
53 |
+
print(f"✅ Weave tracing initialized for: {project_name}")
|
54 |
+
except Exception as e:
|
55 |
+
print(f"⚠️ Weave tracing failed (continuing without Weave): {e}")
|
56 |
+
|
57 |
+
|
58 |
+
async def run_basic_crud_test():
|
59 |
+
"""Tutorial: Set up article structure while learning essential todo operations."""
|
60 |
+
start_time = datetime.now()
|
61 |
+
test_details = {
|
62 |
+
"turns": 0,
|
63 |
+
"validation_results": {},
|
64 |
+
"errors": []
|
65 |
+
}
|
66 |
+
|
67 |
+
try:
|
68 |
+
reset_test_data()
|
69 |
+
|
70 |
+
load_dotenv()
|
71 |
+
|
72 |
+
initialize_tracing("writing-article-foundation")
|
73 |
+
|
74 |
+
agent = create_agent(storage=JsonTodoStorage(), agent_name="To-Do Agent (Article Planning)")
|
75 |
+
|
76 |
+
print("🧪 Starting Basic CRUD Tutorial")
|
77 |
+
print("=" * 50)
|
78 |
+
print("🎯 Learn: Essential todo operations while planning an article")
|
79 |
+
print("📚 Foundation: Set up observability platforms comparison article")
|
80 |
+
|
81 |
+
test_messages = [
|
82 |
+
# === Article Structure Setup ===
|
83 |
+
"Add 'Write introduction to agent observability' to my Writing project with description 'Explain why observability matters for AI agents'",
|
84 |
+
|
85 |
+
# === Platform Sections ===
|
86 |
+
"Add these platform sections to Writing project: 'Create OpenAI platform overview', 'Write Arize Phoenix analysis', and 'Add Weights & Biases Weave section'",
|
87 |
+
|
88 |
+
# === Progress Check ===
|
89 |
+
"Show me my Writing project tasks",
|
90 |
+
|
91 |
+
# === Status Updates ===
|
92 |
+
"Mark 'Create OpenAI platform overview' as in progress since I'm starting research on that section",
|
93 |
+
|
94 |
+
# === Description Enhancement ===
|
95 |
+
"Update the description for 'Write Arize Phoenix analysis' to include 'Focus on cloud deployment benefits and trace visualization features'",
|
96 |
+
|
97 |
+
# === Final Completion ===
|
98 |
+
"Mark 'Write introduction to agent observability' as completed and add note 'Finished 300-word intro explaining the importance of observability'"
|
99 |
+
]
|
100 |
+
|
101 |
+
history = []
|
102 |
+
|
103 |
+
# Weave: Add minimal context attributes for this tutorial session
|
104 |
+
with weave.attributes({'tutorial_type': 'basic_crud', 'environment': 'test', 'app_name': 'todo-agent', 'tutorial_name': 'writing-article-foundation'}):
|
105 |
+
for i, message in enumerate(test_messages, 1):
|
106 |
+
print(f"\n--- Tutorial Step {i} ---")
|
107 |
+
print(f"User: {message}")
|
108 |
+
|
109 |
+
history.append({"role": "user", "content": message})
|
110 |
+
result = await Runner.run(agent, input=history)
|
111 |
+
|
112 |
+
print(f"Agent: {result.final_output}")
|
113 |
+
history = result.to_input_list()
|
114 |
+
|
115 |
+
await asyncio.sleep(0.5)
|
116 |
+
|
117 |
+
test_details["turns"] = len(test_messages)
|
118 |
+
|
119 |
+
print("\n" + "=" * 50)
|
120 |
+
print("🎓 Basic CRUD Tutorial Complete")
|
121 |
+
|
122 |
+
validation_success = True
|
123 |
+
|
124 |
+
try:
|
125 |
+
with open("data/todos.json", "r") as f:
|
126 |
+
todos = json.load(f)
|
127 |
+
|
128 |
+
total_todos = len(todos)
|
129 |
+
completed_todos = len([t for t in todos if t and t.get('status') == 'Completed'])
|
130 |
+
in_progress_todos = len([t for t in todos if t and t.get('status') == 'In Progress'])
|
131 |
+
test_details["validation_results"]["total_todos"] = total_todos
|
132 |
+
test_details["validation_results"]["completed_todos"] = completed_todos
|
133 |
+
test_details["validation_results"]["in_progress_todos"] = in_progress_todos
|
134 |
+
|
135 |
+
print(f"\n📊 Article Foundation: {total_todos} sections planned, {completed_todos} completed, {in_progress_todos} in progress")
|
136 |
+
|
137 |
+
for todo in todos:
|
138 |
+
if not todo or not isinstance(todo, dict):
|
139 |
+
continue
|
140 |
+
|
141 |
+
status = todo.get('status', 'Not Started')
|
142 |
+
name = todo.get('name', 'Unnamed Task')
|
143 |
+
|
144 |
+
if status == 'Completed':
|
145 |
+
status_emoji = "✅"
|
146 |
+
elif status == 'In Progress':
|
147 |
+
status_emoji = "🚧"
|
148 |
+
else:
|
149 |
+
status_emoji = "📝"
|
150 |
+
|
151 |
+
print(f" {status_emoji} {name}")
|
152 |
+
if todo.get('project'):
|
153 |
+
print(f" Project: {todo['project']}")
|
154 |
+
if todo.get('description'):
|
155 |
+
desc = todo['description']
|
156 |
+
print(f" Description: {desc[:60]}{'...' if len(desc) > 60 else ''}")
|
157 |
+
|
158 |
+
except FileNotFoundError:
|
159 |
+
validation_success = False
|
160 |
+
error_msg = "No todos.json file found"
|
161 |
+
test_details["errors"].append(error_msg)
|
162 |
+
print(f"❌ {error_msg}")
|
163 |
+
|
164 |
+
overall_success = validation_success and len(test_details["errors"]) == 0
|
165 |
+
|
166 |
+
print(f"\n🎓 What You Learned:")
|
167 |
+
print("• Create structured writing tasks with clear descriptions")
|
168 |
+
print("• Organize tasks by project for better workflow")
|
169 |
+
print("• Update task status (Not Started → In Progress → Completed)")
|
170 |
+
print("• Enhance descriptions and add progress notes")
|
171 |
+
print("• Comprehensive CRUD operations on all todo fields")
|
172 |
+
print("🚀 Next: Try the web search tutorial to research platform details!")
|
173 |
+
|
174 |
+
end_time = datetime.now()
|
175 |
+
duration = (end_time - start_time).total_seconds()
|
176 |
+
|
177 |
+
if overall_success:
|
178 |
+
print(f"\n✅ TUTORIAL PASSED: Article foundation ready! ({duration:.1f}s)")
|
179 |
+
else:
|
180 |
+
print(f"\n❌ TUTORIAL FAILED: Check setup and try again ({duration:.1f}s)")
|
181 |
+
|
182 |
+
return overall_success
|
183 |
+
|
184 |
+
except Exception as e:
|
185 |
+
end_time = datetime.now()
|
186 |
+
duration = (end_time - start_time).total_seconds()
|
187 |
+
print(f"\n❌ TUTORIAL FAILED: {str(e)} ({duration:.1f}s)")
|
188 |
+
return False
|
189 |
+
|
190 |
+
|
191 |
+
|
192 |
+
|
193 |
+
if __name__ == "__main__":
|
194 |
+
success = asyncio.run(run_basic_crud_test())
|
195 |
+
exit(0 if success else 1)
|
tests/test_natural_language.py
ADDED
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Natural Language Project Completion Test
|
3 |
+
Tutorial: Finish article project using natural language with typos and casual conversation.
|
4 |
+
"""
|
5 |
+
|
6 |
+
import os
|
7 |
+
import sys
|
8 |
+
import asyncio
|
9 |
+
import json
|
10 |
+
from pathlib import Path
|
11 |
+
from datetime import datetime
|
12 |
+
from dotenv import load_dotenv
|
13 |
+
from phoenix.otel import register
|
14 |
+
import weave
|
15 |
+
from agents import Runner, Agent
|
16 |
+
|
17 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
18 |
+
from agent.todo_agent import create_agent
|
19 |
+
from agent.storage import JsonTodoStorage
|
20 |
+
|
21 |
+
|
22 |
+
def reset_test_data():
|
23 |
+
"""Reset todos and session data for clean test runs."""
|
24 |
+
os.makedirs("data", exist_ok=True)
|
25 |
+
|
26 |
+
with open("data/todos.json", "w") as f:
|
27 |
+
json.dump([], f)
|
28 |
+
|
29 |
+
with open("data/session_default.json", "w") as f:
|
30 |
+
json.dump({"history": []}, f)
|
31 |
+
|
32 |
+
print("🔄 Data reset - starting with clean slate")
|
33 |
+
|
34 |
+
|
35 |
+
|
36 |
+
|
37 |
+
def initialize_tracing(project_name: str):
|
38 |
+
"""Initialize tracing with graceful error handling."""
|
39 |
+
os.environ["OPENAI_TRACING_ENABLED"] = "1"
|
40 |
+
os.environ["WEAVE_PRINT_CALL_LINK"] = "false"
|
41 |
+
|
42 |
+
# Phoenix: Add minimal custom resource attributes via environment variable
|
43 |
+
os.environ["OTEL_RESOURCE_ATTRIBUTES"] = f"tutorial.name={project_name},tutorial.type=natural_language,environment=test,app.name=todo-agent"
|
44 |
+
|
45 |
+
try:
|
46 |
+
register(project_name=project_name, auto_instrument=True)
|
47 |
+
print(f"✅ Phoenix tracing initialized for: {project_name}")
|
48 |
+
except Exception as e:
|
49 |
+
print(f"⚠️ Phoenix tracing failed: {e}")
|
50 |
+
|
51 |
+
if not weave.get_client():
|
52 |
+
try:
|
53 |
+
weave.init(project_name)
|
54 |
+
print(f"✅ Weave tracing initialized for: {project_name}")
|
55 |
+
except Exception as e:
|
56 |
+
print(f"⚠️ Weave tracing failed (continuing without Weave): {e}")
|
57 |
+
|
58 |
+
|
59 |
+
async def run_natural_language_test():
|
60 |
+
"""Tutorial: Complete article project using casual, natural language."""
|
61 |
+
start_time = datetime.now()
|
62 |
+
test_details = {
|
63 |
+
"turns": 0,
|
64 |
+
"validation_results": {},
|
65 |
+
"errors": []
|
66 |
+
}
|
67 |
+
|
68 |
+
try:
|
69 |
+
reset_test_data()
|
70 |
+
|
71 |
+
load_dotenv()
|
72 |
+
|
73 |
+
initialize_tracing("finishing-article-project")
|
74 |
+
|
75 |
+
agent = create_agent(storage=JsonTodoStorage(), agent_name="To-Do Agent (Article Completion)")
|
76 |
+
|
77 |
+
print("🧪 Starting Natural Language Project Completion Tutorial")
|
78 |
+
print("=" * 50)
|
79 |
+
print("🎯 Learn: Natural conversation with typos and casual language")
|
80 |
+
print("📚 Goal: Finish observability article with editing and publishing tasks")
|
81 |
+
|
82 |
+
test_messages = [
|
83 |
+
# === Casual task additions with typos ===
|
84 |
+
"hey, add 'write conclusion section' and 'proofread everthing' to my Writing project - getting close to finishing this article",
|
85 |
+
|
86 |
+
# === Natural editing and context ===
|
87 |
+
"actually change that proofreading task to 'final review and editing' - sounds more professional",
|
88 |
+
|
89 |
+
# === Publishing tasks with informal language ===
|
90 |
+
"also add 'create code examples' and 'format for publication' to my Publishing project - gotta make sure the examples actually work",
|
91 |
+
|
92 |
+
# === Check final status ===
|
93 |
+
"lemme see what we have for the Writing project now"
|
94 |
+
]
|
95 |
+
|
96 |
+
history = []
|
97 |
+
|
98 |
+
# Weave: Add minimal context attributes for this tutorial session
|
99 |
+
with weave.attributes({'tutorial_type': 'natural_language', 'environment': 'test', 'app_name': 'todo-agent', 'tutorial_name': 'language-completion-tutorial'}):
|
100 |
+
for i, message in enumerate(test_messages, 1):
|
101 |
+
print(f"\n--- Completion Step {i} ---")
|
102 |
+
print(f"User: {message}")
|
103 |
+
|
104 |
+
history.append({"role": "user", "content": message})
|
105 |
+
result = await Runner.run(agent, input=history)
|
106 |
+
|
107 |
+
print(f"Agent: {result.final_output}")
|
108 |
+
history = result.to_input_list()
|
109 |
+
|
110 |
+
await asyncio.sleep(0.5)
|
111 |
+
|
112 |
+
test_details["turns"] = len(test_messages)
|
113 |
+
|
114 |
+
print("\n" + "=" * 50)
|
115 |
+
print("🎓 Natural Language Project Completion Tutorial Complete")
|
116 |
+
|
117 |
+
validation_success = True
|
118 |
+
|
119 |
+
try:
|
120 |
+
with open("data/todos.json", "r") as f:
|
121 |
+
todos = json.load(f)
|
122 |
+
|
123 |
+
total_todos = len(todos)
|
124 |
+
test_details["validation_results"]["total_todos"] = total_todos
|
125 |
+
|
126 |
+
projects = set(t.get('project') for t in todos if t.get('project'))
|
127 |
+
test_details["validation_results"]["projects"] = sorted(list(projects))
|
128 |
+
|
129 |
+
print(f"\n📊 Article Completion: {total_todos} finishing tasks across {len(projects)} projects")
|
130 |
+
|
131 |
+
project_groups = {}
|
132 |
+
for todo in todos:
|
133 |
+
project = todo.get('project') or 'No Project'
|
134 |
+
if project not in project_groups:
|
135 |
+
project_groups[project] = []
|
136 |
+
project_groups[project].append(todo)
|
137 |
+
|
138 |
+
for project, project_todos in sorted(project_groups.items()):
|
139 |
+
print(f"\n📂 {project}:")
|
140 |
+
for todo in project_todos:
|
141 |
+
print(f" • {todo['name']}")
|
142 |
+
|
143 |
+
except FileNotFoundError:
|
144 |
+
validation_success = False
|
145 |
+
error_msg = "No todos.json file found"
|
146 |
+
test_details["errors"].append(error_msg)
|
147 |
+
print(f"❌ {error_msg}")
|
148 |
+
|
149 |
+
overall_success = validation_success and len(test_details["errors"]) == 0
|
150 |
+
|
151 |
+
print(f"\n🎓 What You Learned:")
|
152 |
+
print("• Agent handles typos gracefully ('everthing' → 'everything')")
|
153 |
+
print("• Natural conversation flow with task modifications")
|
154 |
+
print("• Casual language processing: 'hey', 'lemme see', 'gotta make sure'")
|
155 |
+
print("• Context understanding: 'that proofreading task' references previous todo")
|
156 |
+
print("🎉 Tutorial Series Complete: You've mastered todo agent workflows!")
|
157 |
+
|
158 |
+
end_time = datetime.now()
|
159 |
+
duration = (end_time - start_time).total_seconds()
|
160 |
+
|
161 |
+
if overall_success:
|
162 |
+
print(f"\n✅ TUTORIAL PASSED: Natural language mastery achieved! ({duration:.1f}s)")
|
163 |
+
else:
|
164 |
+
print(f"\n❌ TUTORIAL FAILED: Language processing needs work ({duration:.1f}s)")
|
165 |
+
|
166 |
+
return overall_success
|
167 |
+
|
168 |
+
except Exception as e:
|
169 |
+
end_time = datetime.now()
|
170 |
+
duration = (end_time - start_time).total_seconds()
|
171 |
+
print(f"\n❌ TUTORIAL FAILED: {str(e)} ({duration:.1f}s)")
|
172 |
+
return False
|
173 |
+
|
174 |
+
|
175 |
+
if __name__ == "__main__":
|
176 |
+
success = asyncio.run(run_natural_language_test())
|
177 |
+
exit(0 if success else 1)
|
tests/test_web_search_brainstorming.py
ADDED
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Web Search Platform Research Test
|
3 |
+
Tutorial: Research observability platforms and convert findings into writing tasks.
|
4 |
+
"""
|
5 |
+
|
6 |
+
import os
|
7 |
+
import sys
|
8 |
+
import asyncio
|
9 |
+
import json
|
10 |
+
from pathlib import Path
|
11 |
+
from datetime import datetime
|
12 |
+
from dotenv import load_dotenv
|
13 |
+
from phoenix.otel import register
|
14 |
+
import weave
|
15 |
+
from agents import Runner, Agent
|
16 |
+
|
17 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
18 |
+
from agent.todo_agent import create_agent
|
19 |
+
from agent.storage import JsonTodoStorage
|
20 |
+
|
21 |
+
|
22 |
+
def reset_test_data():
|
23 |
+
"""Reset todos and session data for clean test runs."""
|
24 |
+
os.makedirs("data", exist_ok=True)
|
25 |
+
|
26 |
+
with open("data/todos.json", "w") as f:
|
27 |
+
json.dump([], f)
|
28 |
+
|
29 |
+
with open("data/session_default.json", "w") as f:
|
30 |
+
json.dump({"history": []}, f)
|
31 |
+
|
32 |
+
print("🔄 Data reset - starting with clean slate")
|
33 |
+
|
34 |
+
|
35 |
+
|
36 |
+
|
37 |
+
def initialize_tracing(project_name: str):
|
38 |
+
"""Initialize tracing with graceful error handling."""
|
39 |
+
os.environ["OPENAI_TRACING_ENABLED"] = "1"
|
40 |
+
os.environ["WEAVE_PRINT_CALL_LINK"] = "false"
|
41 |
+
|
42 |
+
# Phoenix: Add minimal custom resource attributes via environment variable
|
43 |
+
os.environ["OTEL_RESOURCE_ATTRIBUTES"] = f"tutorial.name={project_name},tutorial.type=web_search,environment=test,app.name=todo-agent"
|
44 |
+
|
45 |
+
try:
|
46 |
+
register(project_name=project_name, auto_instrument=True)
|
47 |
+
print(f"✅ Phoenix tracing initialized for: {project_name}")
|
48 |
+
except Exception as e:
|
49 |
+
print(f"⚠️ Phoenix tracing failed: {e}")
|
50 |
+
|
51 |
+
if not weave.get_client():
|
52 |
+
try:
|
53 |
+
weave.init(project_name)
|
54 |
+
print(f"✅ Weave tracing initialized for: {project_name}")
|
55 |
+
except Exception as e:
|
56 |
+
print(f"⚠️ Weave tracing failed (continuing without Weave): {e}")
|
57 |
+
|
58 |
+
|
59 |
+
async def run_web_search_test():
|
60 |
+
"""Tutorial: Research platforms and create structured writing tasks."""
|
61 |
+
start_time = datetime.now()
|
62 |
+
test_details = {
|
63 |
+
"turns": 0,
|
64 |
+
"validation_results": {},
|
65 |
+
"errors": []
|
66 |
+
}
|
67 |
+
|
68 |
+
try:
|
69 |
+
reset_test_data()
|
70 |
+
|
71 |
+
load_dotenv()
|
72 |
+
|
73 |
+
initialize_tracing("observability-platform-research")
|
74 |
+
|
75 |
+
agent = create_agent(storage=JsonTodoStorage(), agent_name="To-Do Agent (Platform Research)")
|
76 |
+
|
77 |
+
print("🧪 Starting Web Search Platform Research Tutorial")
|
78 |
+
print("=" * 50)
|
79 |
+
print("🎯 Learn: Research workflow → structured task creation")
|
80 |
+
print("📚 Goal: Compare observability platforms for AI agents")
|
81 |
+
|
82 |
+
test_messages = [
|
83 |
+
# === Platform Research (3 searches with guided responses) ===
|
84 |
+
"Search for 'Arize Phoenix Cloud main benefits agent observability' and give me a brief 2 paragraph summary",
|
85 |
+
|
86 |
+
"Search for 'Weights & Biases Weave main benefits agent tracing' and give me a brief 2 paragraph summary",
|
87 |
+
|
88 |
+
"Search for 'OpenAI platform observability features benefits' and give me a brief 2 paragraph summary",
|
89 |
+
|
90 |
+
# === Convert Research to Tasks ===
|
91 |
+
"Based on this research, please add writing tasks to my 'Platform Comparison' project for comparing these platforms - I need specific tasks I can work on"
|
92 |
+
]
|
93 |
+
|
94 |
+
history = []
|
95 |
+
|
96 |
+
# Weave: Add minimal context attributes for this tutorial session
|
97 |
+
with weave.attributes({'tutorial_type': 'web_search', 'environment': 'test', 'app_name': 'todo-agent', 'tutorial_name': 'platform-research-tutorial'}):
|
98 |
+
for i, message in enumerate(test_messages, 1):
|
99 |
+
print(f"\n--- Research Step {i} ---")
|
100 |
+
print(f"User: {message}")
|
101 |
+
|
102 |
+
history.append({"role": "user", "content": message})
|
103 |
+
result = await Runner.run(agent, input=history)
|
104 |
+
|
105 |
+
print(f"Agent: {result.final_output}")
|
106 |
+
history = result.to_input_list()
|
107 |
+
|
108 |
+
await asyncio.sleep(0.5)
|
109 |
+
|
110 |
+
test_details["turns"] = len(test_messages)
|
111 |
+
|
112 |
+
print("\n" + "=" * 50)
|
113 |
+
print("🎓 Platform Research Tutorial Complete")
|
114 |
+
|
115 |
+
validation_success = True
|
116 |
+
|
117 |
+
try:
|
118 |
+
with open("data/todos.json", "r") as f:
|
119 |
+
todos = json.load(f)
|
120 |
+
|
121 |
+
total_todos = len(todos)
|
122 |
+
test_details["validation_results"]["total_todos"] = total_todos
|
123 |
+
|
124 |
+
# Research tutorial should create at least 3 writing tasks
|
125 |
+
if total_todos < 3:
|
126 |
+
validation_success = False
|
127 |
+
error_msg = f"Expected at least 3 writing tasks from research, got {total_todos}"
|
128 |
+
test_details["errors"].append(error_msg)
|
129 |
+
print(f"❌ {error_msg}")
|
130 |
+
|
131 |
+
print(f"\n📊 Research Results: {total_todos} writing tasks created from platform research")
|
132 |
+
|
133 |
+
for i, todo in enumerate(todos, 1):
|
134 |
+
if not todo or not isinstance(todo, dict):
|
135 |
+
continue
|
136 |
+
name = todo.get('name', 'Unnamed Task')
|
137 |
+
print(f"{i}. {name}")
|
138 |
+
if todo.get('description'):
|
139 |
+
print(f" Description: {todo['description']}")
|
140 |
+
if todo.get('project'):
|
141 |
+
print(f" Project: {todo['project']}")
|
142 |
+
|
143 |
+
except FileNotFoundError:
|
144 |
+
validation_success = False
|
145 |
+
error_msg = "No todos.json file found"
|
146 |
+
test_details["errors"].append(error_msg)
|
147 |
+
print(f"❌ {error_msg}")
|
148 |
+
|
149 |
+
overall_success = validation_success and len(test_details["errors"]) == 0
|
150 |
+
|
151 |
+
print(f"\n🎓 What You Learned:")
|
152 |
+
print("• Web search integration for research workflows")
|
153 |
+
print("• Converting research findings into structured writing tasks")
|
154 |
+
print("• Multi-platform comparison methodology")
|
155 |
+
print("• Research stays in chat history, todos are actionable tasks")
|
156 |
+
print("🚀 Next: Try the natural language tutorial for project finishing touches!")
|
157 |
+
|
158 |
+
end_time = datetime.now()
|
159 |
+
duration = (end_time - start_time).total_seconds()
|
160 |
+
|
161 |
+
if overall_success:
|
162 |
+
print(f"\n✅ TUTORIAL PASSED: Platform research complete! ({duration:.1f}s)")
|
163 |
+
else:
|
164 |
+
print(f"\n❌ TUTORIAL FAILED: Research workflow needs attention ({duration:.1f}s)")
|
165 |
+
|
166 |
+
return overall_success
|
167 |
+
|
168 |
+
except Exception as e:
|
169 |
+
end_time = datetime.now()
|
170 |
+
duration = (end_time - start_time).total_seconds()
|
171 |
+
print(f"\n❌ TUTORIAL FAILED: {str(e)} ({duration:.1f}s)")
|
172 |
+
return False
|
173 |
+
|
174 |
+
|
175 |
+
if __name__ == "__main__":
|
176 |
+
success = asyncio.run(run_web_search_test())
|
177 |
+
exit(0 if success else 1)
|
todo_gradio/gradio_app.py
ADDED
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import pandas as pd
|
3 |
+
from typing import List, Optional, Any, Dict
|
4 |
+
from datetime import datetime, timezone
|
5 |
+
import gradio as gr
|
6 |
+
from agents import Agent, function_tool, RunContextWrapper, WebSearchTool, Runner
|
7 |
+
from phoenix.otel import register
|
8 |
+
import weave
|
9 |
+
|
10 |
+
# Add parent directory to path for local imports
|
11 |
+
import sys
|
12 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
13 |
+
from agent.todo_agent import create_agent
|
14 |
+
from agent.storage import InMemoryTodoStorage, TodoStatus
|
15 |
+
|
16 |
+
def initialize_tracing():
|
17 |
+
"""Initializes Phoenix and Weave tracing for the application."""
|
18 |
+
project_name = "todo-agent-gradio"
|
19 |
+
|
20 |
+
os.environ["OPENAI_TRACING_ENABLED"] = "1"
|
21 |
+
os.environ["WEAVE_PRINT_CALL_LINK"] = "false"
|
22 |
+
|
23 |
+
# Phoenix: Add minimal custom resource attributes via environment variable
|
24 |
+
os.environ["OTEL_RESOURCE_ATTRIBUTES"] = f"app.name=todo-agent,tutorial.type=production,environment=production,interface=gradio"
|
25 |
+
|
26 |
+
# Prevent re-initialization on hot-reload
|
27 |
+
if not weave.get_client():
|
28 |
+
try:
|
29 |
+
register(project_name=project_name, auto_instrument=True)
|
30 |
+
weave.init(project_name=project_name)
|
31 |
+
print(f"Tracing initialized for project: '{project_name}'")
|
32 |
+
except Exception as e:
|
33 |
+
print(
|
34 |
+
f"Warning: Tracing initialization failed. The app will work, but traces will not be captured. Error: {e}"
|
35 |
+
)
|
36 |
+
|
37 |
+
initialize_tracing()
|
38 |
+
|
39 |
+
def format_todos_for_display(todos: list) -> pd.DataFrame:
|
40 |
+
"""
|
41 |
+
Formats the to-do list for display in the Gradio DataFrame.
|
42 |
+
This is a "ViewModel" transformation, adapting the data model for the UI.
|
43 |
+
"""
|
44 |
+
if not todos:
|
45 |
+
return pd.DataFrame(columns=["ID", "Status", "Task", "Details", "Project", "Created"])
|
46 |
+
|
47 |
+
df = pd.DataFrame([t.model_dump() for t in todos])
|
48 |
+
|
49 |
+
# Rename for user-friendly headers
|
50 |
+
df.rename(columns={
|
51 |
+
'id': 'ID',
|
52 |
+
'name': 'Task',
|
53 |
+
'description': 'Details',
|
54 |
+
'project': 'Project',
|
55 |
+
'status': 'Status',
|
56 |
+
'created_at': 'Created'
|
57 |
+
}, inplace=True)
|
58 |
+
|
59 |
+
df['Created'] = pd.to_datetime(df['Created']).dt.strftime('%Y-%m-%d %H:%M')
|
60 |
+
df['Project'] = df['Project'].fillna('')
|
61 |
+
|
62 |
+
display_df = df[['ID', 'Status', 'Task', 'Details', 'Project', 'Created']]
|
63 |
+
|
64 |
+
return display_df
|
65 |
+
|
66 |
+
async def agent_chat(user_input: str, chat_history: list, storage_instance: InMemoryTodoStorage):
|
67 |
+
"""Handles chat interaction between user and agent."""
|
68 |
+
chat_history.append({"role": "user", "content": user_input})
|
69 |
+
|
70 |
+
agent = create_agent(
|
71 |
+
storage=storage_instance,
|
72 |
+
agent_name="To-Do Agent (Gradio)"
|
73 |
+
)
|
74 |
+
|
75 |
+
result = await Runner.run(agent, input=chat_history)
|
76 |
+
full_history = result.to_input_list()
|
77 |
+
|
78 |
+
# Hide raw tool calls in display
|
79 |
+
display_history = []
|
80 |
+
for msg in full_history:
|
81 |
+
role = msg.get("role")
|
82 |
+
content = msg.get("content")
|
83 |
+
|
84 |
+
if role == "user":
|
85 |
+
display_history.append(msg)
|
86 |
+
elif role == "assistant":
|
87 |
+
if content:
|
88 |
+
display_content = ""
|
89 |
+
# Handle streaming response chunks
|
90 |
+
if isinstance(content, list):
|
91 |
+
display_content = "".join(chunk.get('text', '') for chunk in content if isinstance(chunk, dict))
|
92 |
+
elif isinstance(content, dict) and 'text' in content:
|
93 |
+
display_content = content['text']
|
94 |
+
else:
|
95 |
+
display_content = str(content)
|
96 |
+
|
97 |
+
if display_content:
|
98 |
+
display_history.append({"role": "assistant", "content": display_content})
|
99 |
+
elif msg.get("tool_calls"):
|
100 |
+
display_history.append({"role": "assistant", "content": "🛠️ Thinking..."})
|
101 |
+
|
102 |
+
todos = storage_instance.read_all()
|
103 |
+
df = format_todos_for_display(todos)
|
104 |
+
|
105 |
+
return "", display_history, full_history, storage_instance, df
|
106 |
+
|
107 |
+
async def refresh_todos_df(storage_instance: InMemoryTodoStorage):
|
108 |
+
"""Callback to manually refresh the to-do list display."""
|
109 |
+
todos = storage_instance.read_all()
|
110 |
+
return format_todos_for_display(todos)
|
111 |
+
|
112 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="To-Do Agent") as demo:
|
113 |
+
gr.Markdown("# To-Do Agent")
|
114 |
+
gr.Markdown("Manage your to-do list with an AI assistant. The agent can create, read, update, and delete tasks. It can also use web search to help you flesh out your ideas.")
|
115 |
+
|
116 |
+
storage_state = gr.State(InMemoryTodoStorage)
|
117 |
+
chat_history_state = gr.State([])
|
118 |
+
|
119 |
+
with gr.Row():
|
120 |
+
with gr.Column(scale=2):
|
121 |
+
gr.Markdown("### To-Do List")
|
122 |
+
todo_df = gr.DataFrame(
|
123 |
+
interactive=False,
|
124 |
+
wrap=True,
|
125 |
+
column_widths=["5%", "15%", "25%", "30%", "10%", "15%"]
|
126 |
+
)
|
127 |
+
refresh_button = gr.Button("Refresh List")
|
128 |
+
|
129 |
+
with gr.Column(scale=1):
|
130 |
+
gr.Markdown("### Chat")
|
131 |
+
chatbot = gr.Chatbot(label="To-Do Agent Chat", type="messages", height=500)
|
132 |
+
with gr.Row():
|
133 |
+
user_input_box = gr.Textbox(placeholder="Type your message here...", show_label=False, scale=4)
|
134 |
+
send_button = gr.Button("Send", variant="primary", scale=1)
|
135 |
+
|
136 |
+
send_button.click(
|
137 |
+
agent_chat,
|
138 |
+
inputs=[user_input_box, chat_history_state, storage_state],
|
139 |
+
outputs=[user_input_box, chatbot, chat_history_state, storage_state, todo_df]
|
140 |
+
)
|
141 |
+
user_input_box.submit(
|
142 |
+
agent_chat,
|
143 |
+
inputs=[user_input_box, chat_history_state, storage_state],
|
144 |
+
outputs=[user_input_box, chatbot, chat_history_state, storage_state, todo_df]
|
145 |
+
)
|
146 |
+
refresh_button.click(
|
147 |
+
refresh_todos_df,
|
148 |
+
inputs=[storage_state],
|
149 |
+
outputs=[todo_df]
|
150 |
+
)
|
151 |
+
|
152 |
+
def initial_load():
|
153 |
+
"""Returns the initial state for the UI components."""
|
154 |
+
return format_todos_for_display([]), [], InMemoryTodoStorage()
|
155 |
+
|
156 |
+
demo.load(initial_load, None, [todo_df, chatbot, storage_state])
|
157 |
+
|
158 |
+
|
159 |
+
if __name__ == "__main__":
|
160 |
+
demo.launch()
|
uv.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|