feat(narrative): add narrative API blueprint
Five endpoints under /api/narrative/*: - GET /story/<sim_id> — full story so far - GET /story/<sim_id>/round/<num> — single round - POST /translate — translate a round on demand - GET /characters/<sim_id> — roster with emotional state - POST /characters/<sim_id>/init — bootstrap from OASIS profiles Blueprint registered alongside existing graph/simulation/report blueprints following the established pattern. Smoke-tested via Flask test client. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
caec8b5c77
commit
727f4403e3
|
|
@ -63,10 +63,11 @@ def create_app(config_class=Config):
|
|||
return response
|
||||
|
||||
# 注册蓝图
|
||||
from .api import graph_bp, simulation_bp, report_bp
|
||||
from .api import graph_bp, simulation_bp, report_bp, narrative_bp
|
||||
app.register_blueprint(graph_bp, url_prefix='/api/graph')
|
||||
app.register_blueprint(simulation_bp, url_prefix='/api/simulation')
|
||||
app.register_blueprint(report_bp, url_prefix='/api/report')
|
||||
app.register_blueprint(narrative_bp, url_prefix='/api/narrative')
|
||||
|
||||
# 健康检查
|
||||
@app.route('/health')
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ from flask import Blueprint
|
|||
graph_bp = Blueprint('graph', __name__)
|
||||
simulation_bp = Blueprint('simulation', __name__)
|
||||
report_bp = Blueprint('report', __name__)
|
||||
narrative_bp = Blueprint('narrative', __name__)
|
||||
|
||||
from . import graph # noqa: E402, F401
|
||||
from . import simulation # noqa: E402, F401
|
||||
from . import report # noqa: E402, F401
|
||||
from . import narrative # noqa: E402, F401
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
"""Narrative Layer API endpoints.
|
||||
|
||||
Routes are attached to the narrative_bp blueprint defined in api/__init__.py.
|
||||
All endpoints are prefixed with /api/narrative (see app/__init__.py).
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
from flask import jsonify, request
|
||||
|
||||
from . import narrative_bp
|
||||
from ..config import Config
|
||||
from ..services.narrative.story_store import StoryStore
|
||||
from ..services.narrative.narrative_translator import translate_round
|
||||
from ..services.narrative.character_engine import CharacterEngine
|
||||
|
||||
|
||||
def _sim_dir(sim_id: str) -> str:
|
||||
"""Resolve a simulation ID to its on-disk directory."""
|
||||
return os.path.join(Config.OASIS_SIMULATION_DATA_DIR, sim_id)
|
||||
|
||||
|
||||
@narrative_bp.route('/story/<sim_id>', methods=['GET'])
|
||||
def get_full_story(sim_id):
|
||||
"""Return every story beat generated so far for this simulation."""
|
||||
store = StoryStore(_sim_dir(sim_id))
|
||||
return jsonify({"sim_id": sim_id, "beats": store.get_all_beats()})
|
||||
|
||||
|
||||
@narrative_bp.route('/story/<sim_id>/round/<int:round_num>', methods=['GET'])
|
||||
def get_round_story(sim_id, round_num):
|
||||
"""Return a single round's story beat, or 404 if not yet translated."""
|
||||
store = StoryStore(_sim_dir(sim_id))
|
||||
beat = store.get_beat_by_round(round_num)
|
||||
if not beat:
|
||||
return jsonify({"error": "Round not translated yet"}), 404
|
||||
return jsonify(beat)
|
||||
|
||||
|
||||
@narrative_bp.route('/translate', methods=['POST'])
|
||||
def translate():
|
||||
"""Translate a specific round on demand.
|
||||
|
||||
Body: {"sim_id": str, "round": int, "platform": str = "twitter", "tone": str = "neutral"}
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
sim_id = data.get('sim_id')
|
||||
round_num = data.get('round')
|
||||
platform = data.get('platform', 'twitter')
|
||||
tone = data.get('tone', 'neutral')
|
||||
|
||||
if not sim_id or round_num is None:
|
||||
return jsonify({"error": "sim_id and round are required"}), 400
|
||||
|
||||
try:
|
||||
beat = translate_round(_sim_dir(sim_id), platform, int(round_num), tone)
|
||||
return jsonify(beat)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@narrative_bp.route('/characters/<sim_id>', methods=['GET'])
|
||||
def get_characters(sim_id):
|
||||
"""Return the extended character roster (with emotional state)."""
|
||||
store = StoryStore(_sim_dir(sim_id))
|
||||
return jsonify({"characters": store.load_characters()})
|
||||
|
||||
|
||||
@narrative_bp.route('/characters/<sim_id>/init', methods=['POST'])
|
||||
def initialize_characters(sim_id):
|
||||
"""Bootstrap narrative character profiles from the simulation's OASIS profiles."""
|
||||
sim_dir = _sim_dir(sim_id)
|
||||
profiles_path = os.path.join(sim_dir, 'profiles.json')
|
||||
if not os.path.exists(profiles_path):
|
||||
return jsonify({"error": "profiles.json not found for this simulation"}), 404
|
||||
|
||||
with open(profiles_path, 'r', encoding='utf-8') as f:
|
||||
profiles = json.load(f)
|
||||
|
||||
store = StoryStore(sim_dir)
|
||||
engine = CharacterEngine(store)
|
||||
characters = engine.initialize_from_profiles(profiles)
|
||||
return jsonify({"count": len(characters), "characters": characters})
|
||||
Loading…
Reference in New Issue