From 727f4403e3ce2ca2cbcb8f4bb09b76d9b66ec607 Mon Sep 17 00:00:00 2001 From: anadoris007 Date: Mon, 20 Apr 2026 22:02:16 +0530 Subject: [PATCH] feat(narrative): add narrative API blueprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five endpoints under /api/narrative/*: - GET /story/ — full story so far - GET /story//round/ — single round - POST /translate — translate a round on demand - GET /characters/ — roster with emotional state - POST /characters//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) --- backend/app/__init__.py | 3 +- backend/app/api/__init__.py | 2 + backend/app/api/narrative.py | 82 ++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 backend/app/api/narrative.py diff --git a/backend/app/__init__.py b/backend/app/__init__.py index aba624bb..ccfc8c60 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -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') diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py index ffda743a..bcd6a278 100644 --- a/backend/app/api/__init__.py +++ b/backend/app/api/__init__.py @@ -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 diff --git a/backend/app/api/narrative.py b/backend/app/api/narrative.py new file mode 100644 index 00000000..cf316231 --- /dev/null +++ b/backend/app/api/narrative.py @@ -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/', 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//round/', 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/', 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//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})