warbler-cda / NPC_CHAT_API_INTEGRATION.md
Bellok
feat(docs, refactor): add NPC Chat API integration guide and update data ingestion
ec2d906
|
raw
history blame
15 kB

NPC Chat API Integration Guide

New FastAPI Endpoints for NPC Chat

Add these routes to your existing service.py:

# === 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:

# === 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

# 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:

# 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