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): class ExtractionResult(BaseModel):
lore_updates: List[LoreUpdate] = Field( lore_updates: List[LoreUpdate] = Field(
default_factory=list, description="List of discovered lore facts", alias="lore" 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", description="List of significant plot points or events",
alias="events", alias="events",
) )
context_updates: List[ContextUpdate] = Field(
default_factory=list,
description="List of context updates",
alias="context",
)
class Config: class Config:
populate_by_name = True populate_by_name = True
+30 -25
View File
@@ -1,11 +1,13 @@
import logging import logging
import os import os
from posix import system
from this import s
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from openai import OpenAI from openai import OpenAI
from pydantic import ValidationError from pydantic import ValidationError
from .models import ExtractionResult from .models import ExtractionResult, FilterResult
from .prompts import EXTRACTION_SYSTEM_PROMPT, NOISE_FILTER_SYSTEM_PROMPT from .prompts import EXTRACTION_SYSTEM_PROMPT, NOISE_FILTER_SYSTEM_PROMPT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -90,13 +92,28 @@ class LLMProcessor:
logger.error(f"LLM Error: {e}") logger.error(f"LLM Error: {e}")
return "" 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. 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}") 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( def extract_structured_data(
self, filtered_text: str, context: Optional[str] = None 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 # Using standard chat.completions.create with JSON mode for better compatibility with vLLM
logger.info("LLM Processor (Extract): Sending request to backend...") logger.info("LLM Processor (Extract): Sending request to backend...")
messages = [ system_prompt = EXTRACTION_SYSTEM_PROMPT
{"role": "system", "content": EXTRACTION_SYSTEM_PROMPT},
]
if context: if context:
messages.append( system_prompt += f"\n{context}"
{
"role": "system", messages = [
"content": f"Context from previous conversation:\n{context}", {"role": "system", "content": system_prompt},
} ]
)
messages.append({"role": "user", "content": filtered_text}) 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( response = self.client.chat.completions.create(
model=self.model, model=self.model,
messages=messages, messages=messages,
@@ -142,15 +159,3 @@ class LLMProcessor:
logger.error(f"Extraction Error: {e}") logger.error(f"Extraction Error: {e}")
# Return an empty ExtractionResult if parsing fails # Return an empty ExtractionResult if parsing fails
return ExtractionResult() 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 = """ 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. 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. Keep the original speakers' names if they are present in the transcript.
Do not add any commentary or summaries. Just filter the text. 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' - "category": (string) 'NPC', 'Location', 'WorldBuilding', or 'Plot'
- "entity_name": (string) The name of the NPC, Location, or entity - "entity_name": (string) The name of the NPC, Location, or entity
- "content": (string) The actual lore fact or description - "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: 2. "character_state": A list of objects. Each object MUST have:
- "character_name": (string) Name of the character - "character_name": (string) Name of the character
- "hp_change": (integer, optional) Change in HP - "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) - "status_effects_removed": (list of strings)
- "inventory_changes": (list of objects with "item", "quantity", "action") - "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. 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: Example Output:
{ {
@@ -40,7 +48,7 @@ Example Output:
{ {
"category": "NPC", "category": "NPC",
"entity_name": "Thorne", "entity_name": "Thorne",
"content": "A gruff dwarf who runs the local tavern." "content": "A gruff dwarf who runs the local tavern.",
} }
], ],
"character_state": [ "character_state": [
@@ -54,6 +62,12 @@ Example Output:
], ],
"events": [ "events": [
"The party discovered the secret entrance to the crypt." "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 context_text = full_history_text
# 2. Prepare Context (Wiki / Database of Knowledge) # 2. Prepare Context (Wiki / Database of Knowledge)
wiki_context = self._get_wiki_context() # wiki_context = self._get_wiki_context()
# Combine both # 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) # --- New RAG Flow ---
# Run in a separate thread to avoid blocking the event loop # a. Filter transcript first to get cleaned text
result = await asyncio.to_thread( filter_result = await asyncio.to_thread(
self.processor.process_pipeline, raw_text, context=combined_context 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 ( if (
result.lore_updates extraction_result.lore_updates
or result.character_updates or extraction_result.character_updates
or result.significant_events or extraction_result.significant_events
): ):
logger.info( 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 # Trigger RAG query based on extracted entities (for TUI updates)
await self._trigger_rag_queries(result) await self._trigger_rag_queries(extraction_result)
else: else:
logger.info("LLM Worker: No relevant game data extracted.") 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: except Exception as e:
logger.error(f"LLM Worker error: {e}") logger.error(f"LLM Worker error: {e}")
+47 -38
View File
@@ -3,7 +3,7 @@ from typing import List, Optional, Union
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical 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.llm.models import CharacterStateUpdate, ExtractionResult, LoreUpdate
from src.persistence.characters import update_character_state from src.persistence.characters import update_character_state
@@ -12,18 +12,16 @@ from src.persistence.lore import update_lore
class ConfirmationApp(App): class ConfirmationApp(App):
CSS = """ CSS = """
Screen { #main-container {
layout: horizontal; height: 100%;
}
#top-pane {
height: 70%;
} }
#left-pane { #left-pane {
width: 30%; width: 60%;
border: solid;
padding: 1;
}
#middle-pane {
width: 30%;
border: solid; border: solid;
padding: 1; padding: 1;
} }
@@ -35,6 +33,12 @@ class ConfirmationApp(App):
layout: vertical; layout: vertical;
} }
#middle-pane {
height: 30%;
border: solid;
padding: 1;
}
#details-container { #details-container {
height: auto; height: auto;
margin-bottom: 1; margin-bottom: 1;
@@ -84,35 +88,38 @@ class ConfirmationApp(App):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Container( yield Container(
Horizontal( Vertical(
Vertical( Horizontal(
DataTable(id="update-table"), Vertical(
id="left-pane", DataTable(id="update-table"),
id="left-pane",
),
Vertical(
Vertical(
Label("Details:", id="details-label"),
Static("No update selected", id="details-text"),
id="details-container",
),
Vertical(
Label("Edit Value:"),
Input(id="edit-input"),
Button("Save Edit", id="save-edit"),
id="edit-container",
),
Horizontal(
Button("Accept", id="btn-accept"),
Button("Reject", id="btn-reject"),
Button("Edit", id="btn-edit"),
id="actions-container",
),
id="right-pane",
),
), ),
Vertical( Horizontal(
Static("No context available", id="context-pane"), ListView(id="context-pane"),
id="middle-pane", id="middle-pane",
), ),
Vertical( id="main-container",
Vertical(
Label("Details:", id="details-label"),
Static("No update selected", id="details-text"),
id="details-container",
),
Vertical(
Label("Edit Value:"),
Input(id="edit-input"),
Button("Save Edit", id="save-edit"),
id="edit-container",
),
Horizontal(
Button("Accept", id="btn-accept"),
Button("Reject", id="btn-reject"),
Button("Edit", id="btn-edit"),
id="actions-container",
),
id="right-pane",
),
), ),
Footer(), Footer(),
) )
@@ -169,11 +176,13 @@ class ConfirmationApp(App):
while True: while True:
try: try:
update = await self.context_queue.get() 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 # Format the update for display
display_text = f"Query: {update.query}\nSource: {update.source}\n\n{update.snippet}" 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"): if hasattr(self.context_queue, "task_done"):
self.context_queue.task_done() self.context_queue.task_done()