A structured email-writing assistant built with the reflection agentic design pattern:
- Writer creates an email from the user's context.
- Critic checks the draft for accuracy, tone, clarity, and completeness.
- Rewriter applies actionable feedback.
- The loop repeats until the email is approved or the iteration limit is reached.
The project uses Pydantic structured outputs and works with OpenAI or an OpenAI-compatible API.
.
├── app.py # Streamlit UI
├── src/email_assistant/
│ ├── client.py # Environment-based API client
│ ├── critic.py # Draft evaluation
│ ├── models.py # Pydantic request/response models
│ ├── prompts.py # Agent instructions
│ ├── reflection.py # Writer → critic → rewriter loop
│ └── writer.py # Initial draft generation
└── tests/
├── test_writer.py
├── test_critic.py
└── test_reflection.py
Python 3.11+ is required. With uv:
uv sync
cp .env.example .envSet OPENAI_API_KEY and MODEL_NAME in .env. For an OpenAI-compatible provider, set OPENAI_BASE_URL too. The legacy names API_KEY and BASE_URL are also accepted.
With standard pip:
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
cp .env.example .envuv run streamlit run app.py
# or: streamlit run app.pyEnter the sender and recipient context, choose a tone, optionally provide an existing draft, and click Write email. The UI displays the final email, provides a local Copy email button for the subject and body, and includes expandable critic/reflection details.
The interface supports Persian input and mixed Persian/English text using the configured Unicode direction styles.
The app logs workflow steps to the terminal running Streamlit. For example:
INFO | email_assistant.reflection | Reflection: iteration 1/3
INFO | email_assistant.writer | Writer: draft received in 2.14s
INFO | email_assistant.critic | Critic: completed in 1.08s (approved=False, issues=2)
INFO | email_assistant.reflection | Reflection: draft not approved; moving to rewriter
Logs include stage names, model name, timings, iteration counts, and approval state, but not API keys or full email content. Set level=logging.DEBUG in app.py if more verbose diagnostics are needed.
Tests use fake structured responses and do not call an API:
uv run pytest
# or: pytest
