An agentic prototype that turns unstructured incoming work (an email, meeting notes, a founder instruction, a bug report) into a structured, reviewable, partially automated workflow. Submit raw text and the system interprets it into a strict schema, plans by routing each action to a tool, runs the safe actions automatically, holds anything that would leave the building behind a human approval gate, logs every step, and persists all of it.
output.mp4
- Intake. A web interface accepts a block of unstructured text.
- Interpret. An LLM extracts a fixed structured schema (not free-form prose): task title, summary, action items, priority, detected deadline, missing information, what could be automated, and what needs human confirmation. The output is validated by Pydantic.
- Plan. A second LLM step uses function calling to route each action into one of four buckets:
execute_auto,human_review,cannot_execute, orneeds_clarification, each with a reason. - Execute. Safe tools run immediately. The one tool that produces external-facing communication is drafted but held for approval. Missing-info and unsupported actions are recorded, never faked.
- Approve. Any drafted communication needs an explicit Approve / Reject / Edit before it counts as done. No real email is ever sent.
- Persist + trace. Everything lands in SQLite with timestamps, and a plain-language activity trace is visible in the UI.
- Review + manage. Past requests are listed, searchable, and re-openable; each can be deleted individually, or all cleared at once, from the UI.
flowchart TD
U["User: unstructured text"] --> API["FastAPI /api/requests"]
API --> INT["Interpret<br/>Groq JSON mode + Pydantic validation"]
INT -->|valid| PLAN["Plan<br/>Groq function calling"]
INT -->|invalid x2| FAIL["status: interpretation_failed<br/>(clear failure, no fake result)"]
PLAN --> EXE["Execute<br/>route by boundary policy"]
EXE -->|execute_auto| AUTO["run tool now"]
EXE -->|human_review| GATE["draft, then await approval"]
EXE -->|needs_clarification| CLAR["record the question"]
EXE -->|cannot_execute| CANT["record the boundary"]
GATE --> HUMAN["Approve / Reject / Edit"]
AUTO --> DB[("SQLite")]
CLAR --> DB
CANT --> DB
HUMAN --> DB
DB --> TRACE["activity trace in UI"]
The agent boundary lives in one place: ROUTE_POLICY in app/tools.py. Anything that produces a
communication is forced through human_review; the planner cannot override that.
intake → interpretation → planning → tools → approval → persistence → completion
- intake
POST /api/requestscreates a request row. - interpretation
agent.interpret()calls Groq in JSON mode, validates with theInterpretationPydantic model, retries once on invalid output, then fails clearly. - planning
agent.plan()hands the model a tool catalog and reads back itstool_calls. - tools
agent.execute()dispatches each call throughROUTE_POLICYto a real function inapp/tools.py. - approval
agent.approve() / reject() / edit()transition a gated action. - persistence everything is written in
app/db.py(requests, action_items, activity_log). - completion request status is recomputed after each change.
Six real tools (assignment requires at least three):
| tool | route | what it does |
|---|---|---|
create_task_record |
auto | persists a structured task |
generate_markdown_brief |
auto | builds a markdown brief from the interpretation |
draft_communication |
human review | drafts an email/message; never sends; gated by approval |
bounded_website_check |
auto | one real HTTP GET, 8s timeout, http/https only; reports actual status, timing, size, title, on-page signals |
simulate_reminder |
auto | records a simulated reminder (no real calendar) |
search_stored_work |
auto | searches previously stored work |
Plus two control verdicts the planner can emit: request_clarification (→ needs clarification) and
flag_cannot_execute (→ cannot execute).
Requirements: Python 3.10+ and a Groq API key (free tier works). Any OpenAI-compatible provider can
be swapped in by editing app/llm.py.
# 1. clone
git clone https://github.com/DevAshish9090/Work_Agent.git
cd Work_Agent
# 2. create a virtual environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 3. install
pip install -r requirements.txt
# 4. configure
cp .env.example .env
# open .env and paste your GROQ_API_KEY
# 5. run (the app auto-loads .env, so no manual export is needed)
uvicorn app.main:app --reload --port 8000
# 6. open
# http://localhost:8000Health check and quick test:
curl http://localhost:8000/api/health
curl -X POST http://localhost:8000/api/requests \
-H "Content-Type: application/json" \
-d '{"text":"Please take care of the documentation and send it to everyone before the meeting."}'Generate the three sample scenario outputs:
python -m scenarios.run_scenarios
# writes scenarios/scenario_1_routine.md, scenario_2_website.md, scenario_3_ambiguous.md# with the Railway CLI, from the repo root:
railway init
railway up
# then in the Railway dashboard:
# - add a variable GROQ_API_KEY
# - (optional) attach a volume mounted at /data and set DATABASE_PATH=/data/agent.db
# so the SQLite file survives redeploysThe included Procfile and railway.json set the start command
(uvicorn app.main:app --host 0.0.0.0 --port $PORT --workers 1). One worker is intentional: state
is in-process SQLite.
Placeholders only. Never commit a real .env.
| variable | required | default | purpose |
|---|---|---|---|
GROQ_API_KEY |
yes | none | Groq API key |
GROQ_MODEL |
no | llama-3.3-70b-versatile |
model id |
DATABASE_PATH |
no | data/agent.db |
SQLite location; point at a volume in prod |
- Two LLM steps, then deterministic execution. Interpretation and planning use the model; execution does not. The model proposes, code disposes. This keeps behaviour explainable and keeps the agent boundary in code rather than in a prompt.
- JSON mode + Pydantic for interpretation, function calling for planning. These are the two distinct capabilities the brief asks to demonstrate, so each stage uses the right one.
- The boundary is a table, not a vibe.
ROUTE_POLICYdecides what may auto-run. The planner can suggestdraft_communication, but policy pins it to human review regardless. - Fail loud. A failed interpretation sets
interpretation_failed; an unreachable site raises and is logged as a failure. The UI shows a red banner. The system never reports success it did not achieve. - FastAPI + vanilla JS + SQLite. A small stack I can fully explain under questioning beats a heavier one I would be guessing about. Single-origin in production, so no CORS layer.
- Planning quality depends on the model; a weaker model may under-route or over-route actions.
- The website check is deliberately shallow (one GET, a few signals). It is not a crawler or a full accessibility/SEO audit.
- Search is a substring match, not semantic search.
- SQLite with one worker is right for a prototype, not for concurrent production load.
- "Reminders" and "task records" are internal simulations; there is no real calendar or ticketing integration by design (the brief asks not to send real invites or email).
- Semantic search over stored work using embeddings.
- Real integrations behind the same approval gate (calendar, ticketing) with a dry-run mode.
- A re-plan loop: once a clarification is answered, feed it back and re-route automatically.
- Per-action retries with backoff and a proper dead-letter state.
- Auth and multi-user workspaces so each user sees only their own intake.
- Tools used: Claude for scaffolding and code review; Groq (Llama 3.3 70B) is the runtime LLM inside the app itself.
- What for: drafting the FastAPI + SQLite structure, the tool schemas, and the frontend, then reviewing my routing logic and the failure paths. I kept the architecture small on purpose so I could read and defend every file.
- One AI mistake I caught and fixed: an early version created the database tables only inside
FastAPI's
startupevent. That works underuvicorn, but when I ran the app a different way the tables did not exist yet and a request failed withno such table: requests. I caught it by running the endpoint directly instead of trusting it, then fixed it by also callingdb.init_db()at import time inapp/main.py, which is idempotent and safe under any launcher. The lesson I took from it: AI-suggested lifecycle hooks are easy to accept without checking when they actually fire, so I now test the boot path explicitly. - How I validated AI output generally: I ran the tools against real inputs (the website check hits a live URL and reports only what it observed), forced the no-key path to confirm it fails loudly, and read every routing branch rather than assuming the generated logic was correct.