Elevate MiroFish/CrowdSight from single-container dev to a SaaS foundation: - Local memory backend (Zep-compatible): memory services/models, local graph builder + updater, AgentActivity seam, import-boundary isolation; Zep stays default, local is opt-in behind MEMORY_BACKEND. Semantic parity not yet proven. - Durable product persistence: projects/simulations/reports schema (migration 0007) + tenant/owner-scoped ProductRepository + dual-write + scoped_project read-first + ArtifactStore abstraction; durable JobQueue + worker.py. - SaaS hardening: durable RateLimiter (wired to login), UsageService (LLM accounting), redacted AuditService, idempotency, CORS allowlist, safe API errors, single-use PasswordResetService + endpoints (covers invite-pending). - Exactly 3 roles (super_admin/admin/user) with tenant authz policy. - Admin UI: GET/POST/PATCH /api/admin/users + GET/PUT /api/admin/settings (super-admin only, encrypted/masked); AdminView.vue + SettingsView.vue with admin/super-admin route guards, th/en i18n. - Production deploy topology: multi-stage Dockerfile (frontend build + gunicorn wsgi + nginx SPA-proxy + supervisord worker), backend/wsgi.py, gunicorn dep. Backend 197 passed; frontend 10 tests + build green. ruff unavailable (gap). No commit of credentials; secrets handled via env/.env.example. Deferred: Zep semantic A/B parity, object storage cutover, mobile QA, EasyPanel container build of deploy topology.
155 lines
4.9 KiB
Python
155 lines
4.9 KiB
Python
"""
|
|
Template Auto-Selection API
|
|
Analyze seed data and recommend the best simulation template
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from flask import Blueprint, request, jsonify
|
|
from ..utils.llm_client import LLMClient
|
|
from ..utils.locale import t, get_locale, get_language_instruction
|
|
from ..utils.logger import get_logger
|
|
from ..utils.api_errors import internal_error_payload
|
|
from ..security.auth import require_auth
|
|
from ..services.idempotency import idempotent
|
|
|
|
logger = get_logger('crowdsight.template')
|
|
|
|
template_bp = Blueprint('template', __name__)
|
|
|
|
# Load templates
|
|
_templates_path = os.path.join(os.path.dirname(__file__), '..', 'templates.json')
|
|
with open(_templates_path, 'r', encoding='utf-8') as f:
|
|
_templates_data = json.load(f)
|
|
_templates = _templates_data['templates']
|
|
|
|
|
|
@template_bp.route('/list', methods=['GET'])
|
|
@require_auth
|
|
def list_templates():
|
|
"""Return all available templates"""
|
|
locale = get_locale()
|
|
lang_key = f'prompt_{locale}' if locale in ('th', 'en', 'zh') else 'prompt_en'
|
|
|
|
result = []
|
|
for tmpl in _templates:
|
|
result.append({
|
|
'id': tmpl['id'],
|
|
'icon': tmpl['icon'],
|
|
'category': tmpl['category'],
|
|
'prompt': tmpl.get(lang_key, tmpl['prompt_en']),
|
|
'placeholders': tmpl['placeholders'],
|
|
'name': t(f'templates.{tmpl["id"]}'),
|
|
})
|
|
|
|
return jsonify({'success': True, 'templates': result})
|
|
|
|
|
|
@template_bp.route('/auto-select', methods=['POST'])
|
|
@require_auth
|
|
@idempotent
|
|
def auto_select_template():
|
|
"""
|
|
Analyze seed data and recommend the best template + pre-fill prompt.
|
|
|
|
Request JSON:
|
|
{
|
|
"text": "extracted text from uploaded documents",
|
|
"simulation_requirement": "optional user requirement"
|
|
}
|
|
|
|
Response JSON:
|
|
{
|
|
"success": true,
|
|
"template_id": "news_event",
|
|
"prompt": "pre-filled prompt with actual values",
|
|
"confidence": 0.85,
|
|
"reasoning": "why this template was selected"
|
|
}
|
|
"""
|
|
try:
|
|
data = request.get_json() or {}
|
|
text = data.get('text', '')[:5000] # Limit to 5000 chars
|
|
simulation_requirement = data.get('simulation_requirement', '')
|
|
|
|
if not text:
|
|
return jsonify({'success': False, 'error': 'No text provided'}), 400
|
|
|
|
locale = get_locale()
|
|
lang_instruction = get_language_instruction()
|
|
|
|
# Build template descriptions for the LLM
|
|
template_desc = []
|
|
lang_key = f'prompt_{locale}' if locale in ('th', 'en', 'zh') else 'prompt_en'
|
|
for tmpl in _templates:
|
|
template_desc.append(f"- {tmpl['id']}: {tmpl.get(lang_key, tmpl['prompt_en'])}")
|
|
|
|
templates_list = '\n'.join(template_desc)
|
|
|
|
system_prompt = f"""You are a smart assistant that analyzes document content and recommends the best simulation template.
|
|
|
|
Available templates:
|
|
{templates_list}
|
|
|
|
Your task:
|
|
1. Analyze the document content
|
|
2. Select the MOST appropriate template
|
|
3. Fill in the template placeholders with actual values from the document
|
|
4. Return the result in JSON format
|
|
|
|
{lang_instruction}"""
|
|
|
|
user_prompt = f"""Document content:
|
|
{text[:3000]}
|
|
|
|
{f'User requirement: {simulation_requirement}' if simulation_requirement else ''}
|
|
|
|
Return JSON:
|
|
{{
|
|
"template_id": "best_template_id",
|
|
"prompt": "the template with placeholders filled in with actual values from the document",
|
|
"confidence": 0.0-1.0,
|
|
"reasoning": "brief explanation of why this template was chosen"
|
|
}}"""
|
|
|
|
llm = LLMClient()
|
|
result = llm.chat_json(
|
|
messages=[
|
|
{"role": "system", "content": system_prompt},
|
|
{"role": "user", "content": user_prompt}
|
|
],
|
|
temperature=0.3
|
|
)
|
|
|
|
# Validate template_id
|
|
valid_ids = [t['id'] for t in _templates]
|
|
if result.get('template_id') not in valid_ids:
|
|
result['template_id'] = 'news_event' # Default fallback
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'template_id': result.get('template_id', 'news_event'),
|
|
'prompt': result.get('prompt', ''),
|
|
'confidence': result.get('confidence', 0.5),
|
|
'reasoning': result.get('reasoning', ''),
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error("Template auto-select failed: error_type=%s", type(e).__name__)
|
|
return jsonify(internal_error_payload(t)), 500
|
|
|
|
|
|
@template_bp.route('/<template_id>/filter-rules', methods=['GET'])
|
|
@require_auth
|
|
def get_filter_rules(template_id):
|
|
"""Return entity filter rules for a template (used by Step 1)"""
|
|
for tmpl in _templates:
|
|
if tmpl['id'] == template_id:
|
|
return jsonify({
|
|
'success': True,
|
|
'template_id': template_id,
|
|
'filter_rules': tmpl.get('entity_filter', {})
|
|
})
|
|
|
|
return jsonify({'success': False, 'error': 'Template not found'}), 404
|