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:
76
api/routes/ai.py
Normal file
76
api/routes/ai.py
Normal 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
|
||||
Reference in New Issue
Block a user