Spaces:
Running
on
Zero
Running
on
Zero
| # NPC Chat API Integration Guide | |
| ## New FastAPI Endpoints for NPC Chat | |
| Add these routes to your existing `service.py`: | |
| ````python | |
| # === NPC CHAT ENDPOINTS === | |
| class NPCInitializeRequest(BaseModel): | |
| """Request to initialize a new NPC.""" | |
| npc_id: str | |
| name: str | |
| biography: str | |
| realm: str = "dialogue" | |
| alignment: str = "neutral" | |
| class NPCChatRequest(BaseModel): | |
| """Request to chat with an NPC.""" | |
| npc_id: str | |
| player_id: str = "anonymous" | |
| message: str | |
| class NPCChatResponse(BaseModel): | |
| """Response from NPC chat.""" | |
| conversation_id: str | |
| npc_id: str | |
| player_id: str | |
| player_message: str | |
| npc_response: str | |
| emotion: str | |
| intent: str | |
| coherence_score: float | |
| timestamp: str | |
| turn_number: int | |
| @app.post("/npc/initialize", response_model=Dict[str, Any]) | |
| async def initialize_npc(request: NPCInitializeRequest) -> Dict[str, Any]: | |
| """Initialize a new NPC character.""" | |
| global npc_chat_service | |
| if npc_chat_service is None: | |
| npc_chat_service = NPCChatService( | |
| retrieval_api=apiinstance, | |
| embedding_provider=EmbeddingProviderFactory.get_default_provider(), | |
| summarization_ladder=SummarizationLadder(), | |
| semantic_anchors=SemanticAnchorGraph(), | |
| llm_provider=llm_provider, | |
| config={"enable_self_consumption": True}, | |
| ) | |
| profile = npc_chat_service.initialize_npc( | |
| npc_id=request.npc_id, | |
| name=request.name, | |
| biography=request.biography, | |
| realm=request.realm, | |
| alignment=request.alignment, | |
| ) | |
| return { | |
| "status": "initialized", | |
| "npc_id": profile.npc_id, | |
| "name": profile.name, | |
| "biography": profile.biography[:100], | |
| "realm": profile.realm, | |
| "alignment": profile.alignment, | |
| "timestamp": datetime.now().isoformat(), | |
| } | |
| @app.post("/npc/chat", response_model=NPCChatResponse) | |
| async def chat_with_npc(request: NPCChatRequest) -> NPCChatResponse: | |
| """Send message to NPC, get response with self-consumption.""" | |
| global npc_chat_service | |
| if npc_chat_service is None: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="NPC Chat Service not initialized. Call /npc/initialize first.", | |
| ) | |
| try: | |
| result = npc_chat_service.chat_with_npc( | |
| npc_id=request.npc_id, | |
| player_id=request.player_id, | |
| player_message=request.message, | |
| ) | |
| return NPCChatResponse( | |
| conversation_id=result["conversation_id"], | |
| npc_id=result["npc_id"], | |
| player_id=result["player_id"], | |
| player_message=result["player_message"], | |
| npc_response=result["npc_response"], | |
| emotion=result["emotion"], | |
| intent=result["intent"], | |
| coherence_score=result["coherence_score"], | |
| timestamp=result["timestamp"], | |
| turn_number=result["turn_number"], | |
| ) | |
| except Exception as e: | |
| logger.error(f"Error in NPC chat: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| @app.get("/npc/{npc_id}/profile") | |
| async def get_npc_profile(npc_id: str) -> Dict[str, Any]: | |
| """Get NPC profile with conversation statistics.""" | |
| global npc_chat_service | |
| if npc_chat_service is None: | |
| raise HTTPException(status_code=503, detail="NPC Chat Service not initialized") | |
| profile = npc_chat_service.get_npc_profile(npc_id) | |
| if not profile: | |
| raise HTTPException(status_code=404, detail=f"NPC {npc_id} not found") | |
| return profile | |
| @app.get("/conversation/{conversation_id}") | |
| async def get_conversation(conversation_id: str) -> Dict[str, Any]: | |
| """Retrieve full conversation history.""" | |
| global npc_chat_service | |
| if npc_chat_service is None: | |
| raise HTTPException(status_code=503, detail="NPC Chat Service not initialized") | |
| history = npc_chat_service.get_conversation_history(conversation_id) | |
| if not history: | |
| raise HTTPException(status_code=404, detail="Conversation not found") | |
| return history | |
| @app.get("/npc/metrics/self-consumption") | |
| async def get_self_consumption_metrics() -> Dict[str, Any]: | |
| """Get learning loop performance metrics.""" | |
| global npc_chat_service | |
| if npc_chat_service is None: | |
| return { | |
| "status": "uninitialized", | |
| "message": "NPC Chat Service not yet started", | |
| } | |
| return npc_chat_service.get_self_consumption_metrics() | |
| # Add to global state in lifespan | |
| npc_chat_service: Optional[NPCChatService] = None | |
| llm_provider: Optional[Any] = None # Initialize your LLM provider here | |
| @asynccontextmanager | |
| async def lifespan(app: FastAPI): | |
| """Application lifespan with NPC Chat initialization.""" | |
| initapi() | |
| autoloadpacks() | |
| global llm_provider | |
| try: | |
| # Initialize your LLM provider here | |
| # Options: HuggingFace local, OpenAI API, etc. | |
| from sentence_transformers import SentenceTransformer | |
| llm_provider = SentenceTransformer("all-MiniLM-L6-v2") | |
| except Exception as e: | |
| logger.warning(f"Could not initialize LLM provider: {e}") | |
| yield | |
| # Cleanup | |
| logger.info("NPC Chat Service shutting down") | |
| ```` | |
| --- | |
| ## New CLI Commands for NPC Chat | |
| Add these commands to your `cli.py`: | |
| ````python | |
| # === NPC CHAT COMMANDS === | |
| @cli.group() | |
| @click.pass_context | |
| def npc(ctx): | |
| """NPC chat commands - initialize and converse with characters.""" | |
| pass | |
| @npc.command() | |
| @click.option("--npc-id", required=True, help="Unique NPC identifier") | |
| @click.option("--name", required=True, help="NPC character name") | |
| @click.option("--biography", required=True, help="NPC character biography") | |
| @click.option("--realm", default="dialogue", help="NPC realm/domain") | |
| @click.option("--alignment", default="neutral", help="NPC alignment (neutral, harmonic, chaotic)") | |
| @click.pass_context | |
| def init(ctx, npc_id, name, biography, realm, alignment): | |
| """Initialize a new NPC character.""" | |
| client = ctx.obj["client"] | |
| baseurl = ctx.obj["api_url"] | |
| try: | |
| response = requests.post( | |
| f"{baseurl}/npc/initialize", | |
| json={ | |
| "npc_id": npc_id, | |
| "name": name, | |
| "biography": biography, | |
| "realm": realm, | |
| "alignment": alignment, | |
| }, | |
| timeout=30, | |
| ) | |
| response.raise_for_status() | |
| result = response.json() | |
| click.secho(f"✓ NPC Initialized", fg="green") | |
| click.echo(f" ID: {result['npc_id']}") | |
| click.echo(f" Name: {result['name']}") | |
| click.echo(f" Realm: {result['realm']}") | |
| click.echo(f" Status: Ready for chat") | |
| except Exception as e: | |
| click.secho(f"✗ Error: {str(e)}", fg="red") | |
| @npc.command() | |
| @click.option("--npc-id", required=True, help="NPC to chat with") | |
| @click.option("--message", required=True, help="Message to send") | |
| @click.option("--player-id", default="player1", help="Your player ID") | |
| @click.option("--json-output", is_flag=True, help="Output as JSON") | |
| @click.pass_context | |
| def chat(ctx, npc_id, message, player_id, json_output): | |
| """Chat with an NPC and get response with self-consumption.""" | |
| client = ctx.obj["client"] | |
| baseurl = ctx.obj["api_url"] | |
| try: | |
| response = requests.post( | |
| f"{baseurl}/npc/chat", | |
| json={ | |
| "npc_id": npc_id, | |
| "player_id": player_id, | |
| "message": message, | |
| }, | |
| timeout=30, | |
| ) | |
| response.raise_for_status() | |
| result = response.json() | |
| if json_output: | |
| click.echo(json.dumps(result, indent=2)) | |
| else: | |
| click.echo("\n" + "="*60) | |
| click.secho(f"{result['npc_id']} says:", fg="cyan", bold=True) | |
| click.echo(f"\n{result['npc_response']}\n") | |
| click.echo("="*60) | |
| # Show metrics | |
| click.echo(f"Turn: {result['turn_number']} | Coherence: {result['coherence_score']:.2f}") | |
| click.echo(f"Emotion: {result['emotion']} | Intent: {result['intent']}") | |
| click.echo(f"Conversation ID: {result['conversation_id']}") | |
| except Exception as e: | |
| click.secho(f"✗ Error: {str(e)}", fg="red") | |
| @npc.command() | |
| @click.option("--npc-id", required=True, help="NPC to query") | |
| @click.option("--json-output", is_flag=True, help="Output as JSON") | |
| @click.pass_context | |
| def profile(ctx, npc_id, json_output): | |
| """Show NPC profile and statistics.""" | |
| client = ctx.obj["client"] | |
| baseurl = ctx.obj["api_url"] | |
| try: | |
| response = requests.get(f"{baseurl}/npc/{npc_id}/profile", timeout=30) | |
| response.raise_for_status() | |
| profile_data = response.json() | |
| if json_output: | |
| click.echo(json.dumps(profile_data, indent=2)) | |
| else: | |
| click.secho(f"NPC Profile: {profile_data['name']}", bold=True) | |
| click.echo(f"ID: {profile_data['npc_id']}") | |
| click.echo(f"Realm: {profile_data['realm']}") | |
| click.echo(f"Alignment: {profile_data['alignment']}") | |
| click.echo(f"Total Conversations: {profile_data['total_conversations']}") | |
| click.echo(f"Average Coherence: {profile_data['average_coherence']:.2f}") | |
| click.echo(f"Learned Traits: {profile_data['personality_anchor_count']}") | |
| except Exception as e: | |
| click.secho(f"✗ Error: {str(e)}", fg="red") | |
| @npc.command() | |
| @click.option("--conversation-id", required=True, help="Conversation ID to retrieve") | |
| @click.option("--json-output", is_flag=True, help="Output as JSON") | |
| @click.pass_context | |
| def history(ctx, conversation_id, json_output): | |
| """Show conversation history.""" | |
| client = ctx.obj["client"] | |
| baseurl = ctx.obj["api_url"] | |
| try: | |
| response = requests.get(f"{baseurl}/conversation/{conversation_id}", timeout=30) | |
| response.raise_for_status() | |
| history_data = response.json() | |
| if json_output: | |
| click.echo(json.dumps(history_data, indent=2)) | |
| else: | |
| click.secho(f"Conversation {history_data['conversation_id']}", bold=True) | |
| click.echo(f"NPC: {history_data['npc_id']} | Player: {history_data['player_id']}") | |
| click.echo(f"Messages: {history_data['message_count']} | Depth: {history_data['conversation_depth']}") | |
| click.echo(f"Coherence: {history_data['coherence_score']:.2f}\n") | |
| click.echo("Recent Messages:") | |
| for msg in history_data["messages"]: | |
| speaker = "You" if msg["speaker"] == "player" else history_data["npc_id"] | |
| click.echo(f" {speaker}: {msg['text']}") | |
| except Exception as e: | |
| click.secho(f"✗ Error: {str(e)}", fg="red") | |
| @npc.command() | |
| @click.option("--json-output", is_flag=True, help="Output as JSON") | |
| @click.pass_context | |
| def metrics(ctx, json_output): | |
| """Show self-consumption learning metrics.""" | |
| client = ctx.obj["client"] | |
| baseurl = ctx.obj["api_url"] | |
| try: | |
| response = requests.get(f"{baseurl}/npc/metrics/self-consumption", timeout=30) | |
| response.raise_for_status() | |
| metrics_data = response.json() | |
| if json_output: | |
| click.echo(json.dumps(metrics_data, indent=2)) | |
| else: | |
| click.secho("Self-Consumption Metrics", bold=True) | |
| click.echo(f"Conversations: {metrics_data['conversations_processed']}") | |
| click.echo(f"Anchors Created: {metrics_data['anchors_created']}") | |
| click.echo(f"Micro-Summaries: {metrics_data['micro_summaries_distilled']}") | |
| click.echo(f"Macro Distillations: {metrics_data['macro_distillations_created']}") | |
| click.echo(f"Total Conversations Stored: {metrics_data['total_conversations']}") | |
| click.echo(f"Total NPCs: {metrics_data['total_npcs']}") | |
| click.echo(f"Timestamp: {metrics_data['timestamp']}") | |
| except Exception as e: | |
| click.secho(f"✗ Error: {str(e)}", fg="red") | |
| @npc.command() | |
| @click.option("--npc-id", required=True, help="NPC to chat with") | |
| @click.option("--player-id", default="player1", help="Your player ID") | |
| @click.pass_context | |
| def interactive(ctx, npc_id, player_id): | |
| """Start interactive conversation with an NPC.""" | |
| baseurl = ctx.obj["api_url"] | |
| click.secho(f"Starting conversation with {npc_id}...", fg="green") | |
| click.echo("Type 'quit' to exit\n") | |
| while True: | |
| try: | |
| user_input = click.prompt(f"You").strip() | |
| if user_input.lower() == "quit": | |
| click.echo("Goodbye!") | |
| break | |
| if not user_input: | |
| continue | |
| response = requests.post( | |
| f"{baseurl}/npc/chat", | |
| json={ | |
| "npc_id": npc_id, | |
| "player_id": player_id, | |
| "message": user_input, | |
| }, | |
| timeout=30, | |
| ) | |
| response.raise_for_status() | |
| result = response.json() | |
| click.secho(f"{npc_id}: {result['npc_response']}\n", fg="cyan") | |
| except KeyboardInterrupt: | |
| click.echo("\nGoodbye!") | |
| break | |
| except Exception as e: | |
| click.secho(f"Error: {str(e)}", fg="red") | |
| ```` | |
| --- | |
| ## Example Usage Workflow | |
| ````bash | |
| # Initialize an NPC | |
| $ python -m warbler_cda.cli npc init \ | |
| --npc-id "gandalf-01" \ | |
| --name "Gandalf" \ | |
| --biography "A wise wizard with deep knowledge of ancient lore and magic. Known for cryptic riddles and patient guidance." | |
| # Chat with the NPC | |
| $ python -m warbler_cda.cli npc chat \ | |
| --npc-id "gandalf-01" \ | |
| --player-id "player-frodo" \ | |
| --message "What lies ahead on our journey?" | |
| # Start interactive conversation | |
| $ python -m warbler_cda.cli npc interactive \ | |
| --npc-id "gandalf-01" \ | |
| --player-id "player-frodo" | |
| # View NPC profile | |
| $ python -m warbler_cda.cli npc profile --npc-id "gandalf-01" | |
| # Check self-consumption metrics | |
| $ python -m warbler_cda.cli npc metrics | |
| # Retrieve conversation history | |
| $ python -m warbler_cda.cli npc history \ | |
| --conversation-id "conv-gandalf-01-player-frodo-1733754000" | |
| ```` | |
| --- | |
| ## API HTTP Examples | |
| Using curl or httpie: | |
| ````bash | |
| # Initialize NPC | |
| curl -X POST http://localhost:8000/npc/initialize \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "npc_id": "gandalf-01", | |
| "name": "Gandalf", | |
| "biography": "A wise wizard...", | |
| "realm": "dialogue", | |
| "alignment": "neutral" | |
| }' | |
| # Chat | |
| curl -X POST http://localhost:8000/npc/chat \ | |
| -H "Content-Type: application/json" \ | |
| -d '{ | |
| "npc_id": "gandalf-01", | |
| "player_id": "player-frodo", | |
| "message": "What lies ahead?" | |
| }' | |
| # Get profile | |
| curl http://localhost:8000/npc/gandalf-01/profile | |
| # Get metrics | |
| curl http://localhost:8000/npc/metrics/self-consumption | |
| ```` | |
| ```` | |