Update UI and prompts

This commit is contained in:
2026-05-26 23:25:53 -07:00
parent 679eca3fef
commit b83d9b5e6a
5 changed files with 165 additions and 78 deletions
+15
View File
@@ -54,6 +54,16 @@ class ContextUpdate(BaseModel):
)
class FilterResult(BaseModel):
contextual_info: str = Field(
...,
description="Information interesting to the user but not useful for structured extraction",
)
filtered_text: str = Field(
..., description="Cleaned transcript used for structured data extraction"
)
class ExtractionResult(BaseModel):
lore_updates: List[LoreUpdate] = Field(
default_factory=list, description="List of discovered lore facts", alias="lore"
@@ -68,6 +78,11 @@ class ExtractionResult(BaseModel):
description="List of significant plot points or events",
alias="events",
)
context_updates: List[ContextUpdate] = Field(
default_factory=list,
description="List of context updates",
alias="context",
)
class Config:
populate_by_name = True
+30 -25
View File
@@ -1,11 +1,13 @@
import logging
import os
from posix import system
from this import s
from typing import Any, Dict, Optional
from openai import OpenAI
from pydantic import ValidationError
from .models import ExtractionResult
from .models import ExtractionResult, FilterResult
from .prompts import EXTRACTION_SYSTEM_PROMPT, NOISE_FILTER_SYSTEM_PROMPT
logger = logging.getLogger(__name__)
@@ -90,13 +92,28 @@ class LLMProcessor:
logger.error(f"LLM Error: {e}")
return ""
def filter_transcript(self, text: str, context: Optional[str] = None) -> str:
def filter_transcript(
self, text: str, context: Optional[str] = None
) -> FilterResult:
"""
Stage 1: Raw Transcript -> Filtered Text.
"""
result = self._call_llm(NOISE_FILTER_SYSTEM_PROMPT, text, context=context)
result = self._call_llm(
NOISE_FILTER_SYSTEM_PROMPT,
text,
context=context,
response_format={"type": "json_object"},
)
logger.info(f"LLM Processor (Filter): {text} -> {result}")
return result
import json
try:
data = json.loads(result)
return FilterResult(**data)
except (json.JSONDecodeError, ValidationError) as e:
logger.error(f"Filter Parsing Error: {e}")
return FilterResult(contextual_info="", filtered_text=result)
def extract_structured_data(
self, filtered_text: str, context: Optional[str] = None
@@ -109,18 +126,18 @@ class LLMProcessor:
# Using standard chat.completions.create with JSON mode for better compatibility with vLLM
logger.info("LLM Processor (Extract): Sending request to backend...")
messages = [
{"role": "system", "content": EXTRACTION_SYSTEM_PROMPT},
]
system_prompt = EXTRACTION_SYSTEM_PROMPT
if context:
messages.append(
{
"role": "system",
"content": f"Context from previous conversation:\n{context}",
}
)
system_prompt += f"\n{context}"
messages = [
{"role": "system", "content": system_prompt},
]
messages.append({"role": "user", "content": filtered_text})
for message in messages:
logger.info(f"LLM Processor (Extract): Message: {message}")
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
@@ -142,15 +159,3 @@ class LLMProcessor:
logger.error(f"Extraction Error: {e}")
# Return an empty ExtractionResult if parsing fails
return ExtractionResult()
def process_pipeline(
self, raw_text: str, context: Optional[str] = None
) -> ExtractionResult:
"""
Executes the two-stage pipeline: Raw Transcript -> Filtered Text -> Structured Data.
"""
filtered_text = self.filter_transcript(raw_text, context=context)
if not filtered_text:
return ExtractionResult()
return self.extract_structured_data(filtered_text, context=context)
+16 -2
View File
@@ -2,7 +2,11 @@
NOISE_FILTER_SYSTEM_PROMPT = """
You are a D&D Game Master's assistant. Given a transcript, remove all out-of-character (OOC) chatter, logistical discussions (e.g., 'Where is my d20?'), and non-relevant noise.
Output only the in-character dialogue and game-relevant events.
You must output your response as a JSON object with the following keys:
- "contextual_info": Information that is interesting or relevant to the story/session but doesn't fit into lore, character state, or significant events (e.g., flavor text, atmospheric descriptions, player questions, or player commentary that adds context).
- "filtered_text": The cleaned transcript used for structured data extraction.
Keep the original speakers' names if they are present in the transcript.
Do not add any commentary or summaries. Just filter the text.
"""
@@ -26,6 +30,7 @@ Return a JSON object with exactly these keys:
- "category": (string) 'NPC', 'Location', 'WorldBuilding', or 'Plot'
- "entity_name": (string) The name of the NPC, Location, or entity
- "content": (string) The actual lore fact or description
- "context": (string, optional) Helpful information for the DM (e.g., descriptions of characters, spell details, game mechanics) discovered via the knowledge base or the transcript.
2. "character_state": A list of objects. Each object MUST have:
- "character_name": (string) Name of the character
- "hp_change": (integer, optional) Change in HP
@@ -33,6 +38,9 @@ Return a JSON object with exactly these keys:
- "status_effects_removed": (list of strings)
- "inventory_changes": (list of objects with "item", "quantity", "action")
3. "events": A list of strings. Each string should be a concise description of a significant plot development.
4. "context": A list of objects. Each object MUST have:
- "contextual_info": (string) The contextual information for the event
- "source": (string) The source of the context (e.g., "players handbook, page 68")
Example Output:
{
@@ -40,7 +48,7 @@ Example Output:
{
"category": "NPC",
"entity_name": "Thorne",
"content": "A gruff dwarf who runs the local tavern."
"content": "A gruff dwarf who runs the local tavern.",
}
],
"character_state": [
@@ -54,6 +62,12 @@ Example Output:
],
"events": [
"The party discovered the secret entrance to the crypt."
],
"context": [
{
"contextual_info": "fireball does 1d6 damage, with an area of effect of 10 feet circle",
"source": "players handbook, page 68"
}
]
}
+57 -13
View File
@@ -131,32 +131,76 @@ class PipelineOrchestrator:
context_text = full_history_text
# 2. Prepare Context (Wiki / Database of Knowledge)
wiki_context = self._get_wiki_context()
# wiki_context = self._get_wiki_context()
# Combine both
combined_context = f"Conversation History:\n{context_text}\n\nWiki Knowledge:\n{wiki_context}"
combined_context = f"Conversation History:\n{context_text}\n\n"
# Process via LLM (Filter -> Extract)
# Run in a separate thread to avoid blocking the event loop
result = await asyncio.to_thread(
self.processor.process_pipeline, raw_text, context=combined_context
# --- New RAG Flow ---
# a. Filter transcript first to get cleaned text
filter_result = await asyncio.to_thread(
self.processor.filter_transcript, raw_text, context=combined_context
)
# b. Use filtered text to retrieve relevant snippets from RAG
rag_snippets = []
if filter_result.filtered_text:
try:
snippets = await asyncio.to_thread(
self.rag_manager.retrieve, filter_result.filtered_text
)
rag_snippets = snippets
except Exception as e:
logger.error(f"RAG Retrieval Error in llm_worker: {e}")
# c. Combine RAG snippets with existing combined_context
logger.info(f"LLM Processor (Extract): rag_snippets: {rag_snippets}")
rag_context_text = "\n".join([s.snippet for s in rag_snippets])
augmented_context = combined_context
if rag_context_text:
augmented_context += (
f"\n\nRelevant RAG Context:\n{rag_context_text}"
)
# d. Extract structured data using the augmented context
extraction_result = await asyncio.to_thread(
self.processor.extract_structured_data,
filter_result.filtered_text if filter_result.filtered_text else "",
context=augmented_context,
)
if (
result.lore_updates
or result.character_updates
or result.significant_events
extraction_result.lore_updates
or extraction_result.character_updates
or extraction_result.significant_events
):
logger.info(
f"LLM Worker: Proposal generated. Putting into proposal queue. (Lore: {len(result.lore_updates)}, Char: {len(result.character_updates)})"
f"LLM Worker: Proposal generated. Putting into proposal queue. (Lore: {len(extraction_result.lore_updates)}, Char: {len(extraction_result.character_updates)})"
)
await self.proposal_queue.put(result)
await self.proposal_queue.put(extraction_result)
# Trigger RAG query based on extracted entities
await self._trigger_rag_queries(result)
# Trigger RAG query based on extracted entities (for TUI updates)
await self._trigger_rag_queries(extraction_result)
else:
logger.info("LLM Worker: No relevant game data extracted.")
# e. If the filter found contextual info, push it to the context queue
if filter_result.contextual_info:
logger.info(
f"LLM Worker: Contextual info found: {filter_result.contextual_info}"
)
await self.context_queue.put(
ContextUpdate(
query="Filter",
snippet=filter_result.contextual_info,
source="Transcript",
)
)
# f. Also push the RAG snippets used for extraction to the context queue
for snippet in rag_snippets:
await self.context_queue.put(snippet)
except Exception as e:
logger.error(f"LLM Worker error: {e}")
+25 -16
View File
@@ -3,7 +3,7 @@ from typing import List, Optional, Union
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.widgets import Button, DataTable, Footer, Input, Label, Static
from textual.widgets import Button, DataTable, Footer, Input, Label, ListView, Static
from src.llm.models import CharacterStateUpdate, ExtractionResult, LoreUpdate
from src.persistence.characters import update_character_state
@@ -12,18 +12,16 @@ from src.persistence.lore import update_lore
class ConfirmationApp(App):
CSS = """
Screen {
layout: horizontal;
#main-container {
height: 100%;
}
#top-pane {
height: 70%;
}
#left-pane {
width: 30%;
border: solid;
padding: 1;
}
#middle-pane {
width: 30%;
width: 60%;
border: solid;
padding: 1;
}
@@ -35,6 +33,12 @@ class ConfirmationApp(App):
layout: vertical;
}
#middle-pane {
height: 30%;
border: solid;
padding: 1;
}
#details-container {
height: auto;
margin-bottom: 1;
@@ -84,15 +88,12 @@ class ConfirmationApp(App):
def compose(self) -> ComposeResult:
yield Container(
Vertical(
Horizontal(
Vertical(
DataTable(id="update-table"),
id="left-pane",
),
Vertical(
Static("No context available", id="context-pane"),
id="middle-pane",
),
Vertical(
Vertical(
Label("Details:", id="details-label"),
@@ -114,6 +115,12 @@ class ConfirmationApp(App):
id="right-pane",
),
),
Horizontal(
ListView(id="context-pane"),
id="middle-pane",
),
id="main-container",
),
Footer(),
)
@@ -169,11 +176,13 @@ class ConfirmationApp(App):
while True:
try:
update = await self.context_queue.get()
context_pane = self.query_one("#context-pane", Static)
context_pane = self.query_one("#context-pane", ListView)
# Format the update for display
display_text = f"Query: {update.query}\nSource: {update.source}\n\n{update.snippet}"
context_pane.update(display_text)
# Add a new Static widget to the ListView
context_pane.mount(Static(display_text))
if hasattr(self.context_queue, "task_done"):
self.context_queue.task_done()