Update UI and prompts
This commit is contained in:
@@ -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
@@ -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
@@ -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"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user