Add AI task composition for routines (bot + web client)

Users can now describe a goal and have AI auto-generate 4-7 ADHD-friendly
steps, which they can review and modify before saving.

- ai/ai_config.json: Add step_generator prompt and ai_compose examples
  to command_parser so bot recognises vague task descriptions
- api/routes/ai.py: New POST /api/ai/generate-steps endpoint — calls
  LLM via ai_parser, validates and sanitises returned steps
- api/main.py: Register new ai_routes module
- bot/commands/routines.py: Add ai_compose action — generates steps,
  shows numbered list with durations, uses existing yes/no confirm flow
- synculous-client/src/lib/api.ts: Add api.ai.generateSteps(goal)
- synculous-client/src/app/dashboard/routines/new/page.tsx: Add
  Generate with AI panel with collapsible textarea, loading spinner,
  and inline error; generated steps slot into existing editable list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 13:56:54 -06:00
parent 9fb56edf74
commit 95ebae6766
6 changed files with 260 additions and 17 deletions

View File

@@ -23,6 +23,7 @@ import api.routes.rewards as rewards_routes
import api.routes.victories as victories_routes
import api.routes.adaptive_meds as adaptive_meds_routes
import api.routes.snitch as snitch_routes
import api.routes.ai as ai_routes
app = flask.Flask(__name__)
CORS(app)
@@ -41,6 +42,7 @@ ROUTE_MODULES = [
victories_routes,
adaptive_meds_routes,
snitch_routes,
ai_routes,
]

76
api/routes/ai.py Normal file
View File

@@ -0,0 +1,76 @@
"""
api/routes/ai.py - AI-powered generation endpoints
"""
import asyncio
import flask
import jwt
import os
import ai.parser as ai_parser
JWT_SECRET = os.getenv("JWT_SECRET")
def _get_user_uuid(request):
"""Extract and validate user UUID from JWT token."""
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return None
token = auth_header[7:]
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
return payload.get("sub")
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
return None
def register(app):
@app.route("/api/ai/generate-steps", methods=["POST"])
def api_generate_steps():
"""
Generate ADHD-friendly routine steps from a goal description.
Body: {"goal": string}
Returns: {"steps": [{"name": string, "duration_minutes": int}]}
"""
user_uuid = _get_user_uuid(flask.request)
if not user_uuid:
return flask.jsonify({"error": "unauthorized"}), 401
data = flask.request.get_json()
if not data:
return flask.jsonify({"error": "missing body"}), 400
goal = data.get("goal", "").strip()
if not goal:
return flask.jsonify({"error": "missing required field: goal"}), 400
if len(goal) > 500:
return flask.jsonify({"error": "goal too long (max 500 characters)"}), 400
try:
result = asyncio.run(ai_parser.parse(goal, "step_generator"))
except Exception as e:
return flask.jsonify({"error": f"AI service error: {str(e)}"}), 500
if "error" in result:
return flask.jsonify({"error": result["error"]}), 500
steps = result.get("steps", [])
validated = []
for s in steps:
if not isinstance(s, dict):
continue
name = str(s.get("name", "")).strip()
if not name:
continue
try:
dur = max(1, min(60, int(s.get("duration_minutes", 5))))
except (ValueError, TypeError):
dur = 5
validated.append({"name": name, "duration_minutes": dur})
if len(validated) < 2:
return flask.jsonify({"error": "AI failed to generate valid steps"}), 500
return flask.jsonify({"steps": validated}), 200