|
| 1 | +"""Indicator workspace endpoints for external AI agents. |
| 2 | +
|
| 3 | +Read (R): contract, list, get, validate |
| 4 | +Write (W): save / update private indicators in ``qd_indicator_codes`` |
| 5 | +""" |
| 6 | +from __future__ import annotations |
| 7 | + |
| 8 | +from app.services.indicator_workspace import ( |
| 9 | + get_indicator_authoring_contract, |
| 10 | + get_user_indicator, |
| 11 | + link_indicator_config, |
| 12 | + list_user_indicators, |
| 13 | + save_user_indicator, |
| 14 | + validate_indicator_code, |
| 15 | +) |
| 16 | +from app.utils.agent_auth import SCOPE_R, SCOPE_W, agent_required, current_user_id |
| 17 | +from app.utils.logger import get_logger |
| 18 | +from flask import request |
| 19 | + |
| 20 | +from ._security import assert_indicator_code_size |
| 21 | +from . import agent_v1_bp |
| 22 | +from ._helpers import clip_int, envelope, error, get_json_or_400 |
| 23 | + |
| 24 | +logger = get_logger(__name__) |
| 25 | + |
| 26 | + |
| 27 | +@agent_v1_bp.route("/indicators/authoring-contract", methods=["GET"]) |
| 28 | +@agent_required(SCOPE_R) |
| 29 | +def indicator_authoring_contract(): |
| 30 | + """Return starter template + required I/O contract for AI code generation.""" |
| 31 | + return envelope(get_indicator_authoring_contract()) |
| 32 | + |
| 33 | + |
| 34 | +@agent_v1_bp.route("/indicators", methods=["GET"]) |
| 35 | +@agent_required(SCOPE_R) |
| 36 | +def list_indicators(): |
| 37 | + """List tenant indicators (compact; no code body).""" |
| 38 | + limit = clip_int(request.args.get("limit"), default=50, lo=1, hi=200) |
| 39 | + rows = list_user_indicators(current_user_id(), limit=limit) |
| 40 | + return envelope(rows) |
| 41 | + |
| 42 | + |
| 43 | +@agent_v1_bp.route("/indicators/<int:indicator_id>", methods=["GET"]) |
| 44 | +@agent_required(SCOPE_R) |
| 45 | +def get_indicator(indicator_id: int): |
| 46 | + """Fetch one indicator including ``code``.""" |
| 47 | + row = get_user_indicator(current_user_id(), indicator_id) |
| 48 | + if not row: |
| 49 | + return error(404, "Indicator not found", http=404) |
| 50 | + return envelope(row) |
| 51 | + |
| 52 | + |
| 53 | +@agent_v1_bp.route("/indicators/validate", methods=["POST"]) |
| 54 | +@agent_required(SCOPE_R) |
| 55 | +def validate_indicator(): |
| 56 | + """Sandbox-run indicator code without persisting.""" |
| 57 | + body, err = get_json_or_400() |
| 58 | + if err: |
| 59 | + return err |
| 60 | + code = (body.get("code") or body.get("indicator_code") or "").strip() |
| 61 | + if not code: |
| 62 | + return error(400, "code is required") |
| 63 | + try: |
| 64 | + assert_indicator_code_size(code) |
| 65 | + except ValueError as ve: |
| 66 | + return error(400, str(ve)) |
| 67 | + params = body.get("indicator_params") or body.get("params") or {} |
| 68 | + result = validate_indicator_code(code, params) |
| 69 | + return envelope(result, message="validated" if result.get("success") else "validation_failed") |
| 70 | + |
| 71 | + |
| 72 | +@agent_v1_bp.route("/indicators", methods=["POST"]) |
| 73 | +@agent_required(SCOPE_W) |
| 74 | +def save_indicator(): |
| 75 | + """Save indicator into ``qd_indicator_codes`` (private; not community publish).""" |
| 76 | + body, err = get_json_or_400() |
| 77 | + if err: |
| 78 | + return err |
| 79 | + code = (body.get("code") or body.get("indicator_code") or "").strip() |
| 80 | + if not code: |
| 81 | + return error(400, "code is required") |
| 82 | + try: |
| 83 | + assert_indicator_code_size(code) |
| 84 | + except ValueError as ve: |
| 85 | + return error(400, str(ve)) |
| 86 | + |
| 87 | + validate_first = body.get("validate", True) |
| 88 | + if validate_first is not False and str(validate_first).lower() not in ("0", "false", "no"): |
| 89 | + validation = validate_indicator_code( |
| 90 | + code, |
| 91 | + body.get("indicator_params") or body.get("params") or {}, |
| 92 | + ) |
| 93 | + if not validation.get("success"): |
| 94 | + return error( |
| 95 | + 400, |
| 96 | + validation.get("msg") or "Indicator validation failed", |
| 97 | + details=validation, |
| 98 | + http=400, |
| 99 | + ) |
| 100 | + |
| 101 | + try: |
| 102 | + indicator_id = int(body.get("id") or body.get("indicator_id") or 0) |
| 103 | + except (TypeError, ValueError): |
| 104 | + indicator_id = 0 |
| 105 | + |
| 106 | + try: |
| 107 | + new_id = save_user_indicator( |
| 108 | + user_id=current_user_id(), |
| 109 | + code=code, |
| 110 | + name=body.get("name") or body.get("indicator_name"), |
| 111 | + description=body.get("description") or body.get("indicator_description"), |
| 112 | + indicator_id=indicator_id, |
| 113 | + ) |
| 114 | + except ValueError as ve: |
| 115 | + return error(400, str(ve)) |
| 116 | + except Exception as exc: |
| 117 | + logger.error(f"agent_v1/indicators save failed: {exc}", exc_info=True) |
| 118 | + return error(500, "save_indicator failed", details=str(exc), http=500) |
| 119 | + |
| 120 | + row = get_user_indicator(current_user_id(), new_id) |
| 121 | + return envelope( |
| 122 | + {"indicator_id": new_id, "indicator": row}, |
| 123 | + message="saved", |
| 124 | + ) |
| 125 | + |
| 126 | + |
| 127 | +@agent_v1_bp.route("/indicators/link-config", methods=["POST"]) |
| 128 | +@agent_required(SCOPE_W) |
| 129 | +def link_indicator(): |
| 130 | + """Normalize ``indicator_config`` dict: auto-save embedded code + set indicator_id.""" |
| 131 | + body, err = get_json_or_400() |
| 132 | + if err: |
| 133 | + return err |
| 134 | + ic = body.get("indicator_config") or body |
| 135 | + linked = link_indicator_config(current_user_id(), ic, auto_save=True) |
| 136 | + return envelope(linked, message="linked") |
0 commit comments