Add LLM input logging and UI log pane

- Add log_queue to PipelineOrchestrator and log LLM inputs to UI
- Use entity_name for lore update logs instead of topic
- Pass log_queue into ConfirmationApp to display logs in UI
- Introduce a log pane and left/right pane layout in the UI
- Poll and render log messages via a new poll_log_updates worker
- Run log polling with Textual workers to avoid GC issues
- Fix ListView insertion by wrapping ListItem in a list
- Relax RAG similarity threshold from 0.7 to 0.5
This commit is contained in:
2026-05-27 23:09:11 -07:00
parent 1098bdb2f9
commit 1cfba3a0ae
3 changed files with 74 additions and 10 deletions
+6 -1
View File
@@ -53,6 +53,7 @@ class PipelineOrchestrator:
self.ui_to_llm_queue = asyncio.Queue() self.ui_to_llm_queue = asyncio.Queue()
self.clean_to_llm_queue = asyncio.Queue() self.clean_to_llm_queue = asyncio.Queue()
self.llm_to_ui_queue = asyncio.Queue() self.llm_to_ui_queue = asyncio.Queue()
self.log_queue = asyncio.Queue()
self.is_running = False self.is_running = False
@@ -202,6 +203,9 @@ class PipelineOrchestrator:
# RAG Retrieval for context # RAG Retrieval for context
context = await asyncio.to_thread(self.rag_manager.retrieve, text) context = await asyncio.to_thread(self.rag_manager.retrieve, text)
# Log the text sent to the LLM for UI affordance
await self.log_queue.put(f"[{speaker}] {text}")
# Structured extraction using the processor # Structured extraction using the processor
extraction_result = await asyncio.to_thread( extraction_result = await asyncio.to_thread(
self.processor.extract_structured_data, self.processor.extract_structured_data,
@@ -212,7 +216,7 @@ class PipelineOrchestrator:
# Persistence: Lore Updates # Persistence: Lore Updates
for lore_update in extraction_result.lore_updates: for lore_update in extraction_result.lore_updates:
await asyncio.to_thread(update_lore, lore_update) await asyncio.to_thread(update_lore, lore_update)
logger.info(f"LLM Worker: Lore updated: {lore_update.topic}") logger.info(f"LLM Worker: Lore updated: {lore_update.entity_name}")
# Persistence: Character State Updates # Persistence: Character State Updates
for char_update in extraction_result.character_updates: for char_update in extraction_result.character_updates:
@@ -271,6 +275,7 @@ class PipelineOrchestrator:
app = ConfirmationApp( app = ConfirmationApp(
ui_to_llm_queue=self.ui_to_llm_queue, ui_to_llm_queue=self.ui_to_llm_queue,
llm_to_ui_queue=self.llm_to_ui_queue, llm_to_ui_queue=self.llm_to_ui_queue,
log_queue=self.log_queue,
) )
await app.run_async() await app.run_async()
self.stop() self.stop()
+1 -1
View File
@@ -162,7 +162,7 @@ class RAGManager:
nodes = retriever.retrieve(query) nodes = retriever.retrieve(query)
# Filter nodes by similarity score (threshold > 0.7) # Filter nodes by similarity score (threshold > 0.7)
nodes = [node for node in nodes if node.score >= 0.7] nodes = [node for node in nodes if node.score >= 0.5]
if summarize: if summarize:
return self.summarize_results(query, nodes) return self.summarize_results(query, nodes)
+67 -8
View File
@@ -51,6 +51,22 @@ class ConfirmationApp(App):
height: 100%; height: 100%;
} }
#content-wrapper {
layout: horizontal;
height: 100%;
}
#left-pane {
width: 70%;
layout: vertical;
}
#right-pane {
width: 30%;
layout: vertical;
border: solid white;
}
#pending-facts-table { #pending-facts-table {
height: 40%; height: 40%;
border: solid white; border: solid white;
@@ -67,6 +83,17 @@ class ConfirmationApp(App):
border: solid white; border: solid white;
} }
#log-pane {
height: 30%;
border: solid white;
background: #111;
}
#log-footer {
height: 70%;
border: solid white;
}
#modal-container { #modal-container {
width: 60%; width: 60%;
height: auto; height: auto;
@@ -110,11 +137,13 @@ class ConfirmationApp(App):
result: Optional[ExtractionResult] = None, result: Optional[ExtractionResult] = None,
ui_to_llm_queue: Optional[asyncio.Queue] = None, ui_to_llm_queue: Optional[asyncio.Queue] = None,
llm_to_ui_queue: Optional[asyncio.Queue] = None, llm_to_ui_queue: Optional[asyncio.Queue] = None,
log_queue: Optional[asyncio.Queue] = None,
): ):
super().__init__() super().__init__()
self.result = result self.result = result
self.ui_to_llm_queue = ui_to_llm_queue self.ui_to_llm_queue = ui_to_llm_queue
self.llm_to_ui_queue = llm_to_ui_queue self.llm_to_ui_queue = llm_to_ui_queue
self.log_queue = log_queue
self.pending_updates: List[Union[LoreUpdate, CharacterStateUpdate]] = [] self.pending_updates: List[Union[LoreUpdate, CharacterStateUpdate]] = []
if result: if result:
@@ -123,12 +152,23 @@ class ConfirmationApp(App):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Vertical( yield Vertical(
DataTable(id="pending-facts-table"), Horizontal(
Vertical( Vertical(
Input(placeholder="Message LLM...", id="llm-input"), DataTable(id="pending-facts-table"),
id="llm-input-container", Vertical(
Input(placeholder="Message LLM...", id="llm-input"),
id="llm-input-container",
),
ListView(id="context-pane"),
id="left-pane",
),
Vertical(
ListView(id="log-pane"),
Static("LATEST LLM INPUTS", id="log-footer"),
id="right-pane",
),
id="content-wrapper",
), ),
ListView(id="context-pane"),
id="main-container", id="main-container",
) )
yield Footer() yield Footer()
@@ -145,7 +185,11 @@ class ConfirmationApp(App):
# We don't need a poller for this, just the action_send # We don't need a poller for this, just the action_send
pass pass
if self.llm_to_ui_queue: if self.llm_to_ui_queue:
self.run_worker(self.poll_llm_updates, thread=False) # Use Textual workers so the task isn't garbage-collected and
# exceptions are surfaced via the worker manager.
self.run_worker(self.poll_llm_updates(), exclusive=False)
if self.log_queue:
self.run_worker(self.poll_log_updates(), exclusive=False)
self.query_one("#llm-input", Input).focus() self.query_one("#llm-input", Input).focus()
@@ -171,13 +215,28 @@ class ConfirmationApp(App):
update = await self.llm_to_ui_queue.get() update = await self.llm_to_ui_queue.get()
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_list = self.query_one("#context-pane", ListView) context_list = self.query_one("#context-pane", ListView)
# Insert at the top to show most recent first # ListView.insert takes an *iterable* of ListItems; passing a
context_list.insert(0, ListItem(Static(display_text))) # bare ListItem raises TypeError because ListItem is not iterable.
# Insert at the top to show most recent first.
await context_list.insert(0, [ListItem(Static(display_text))])
if hasattr(self.llm_to_ui_queue, "task_done"): if hasattr(self.llm_to_ui_queue, "task_done"):
self.llm_to_ui_queue.task_done() self.llm_to_ui_queue.task_done()
except Exception as e: except Exception as e:
self.log(f"Error polling LLM updates: {e}") self.log(f"Error polling LLM updates: {e}")
async def poll_log_updates(self) -> None:
while True:
try:
log_text = await self.log_queue.get()
log_list = self.query_one("#log-pane", ListView)
# See poll_llm_updates: wrap the ListItem in a list.
# Insert at the top to show most recent first.
await log_list.insert(0, [ListItem(Static(log_text))])
if hasattr(self.log_queue, "task_done"):
self.log_queue.task_done()
except Exception as e:
self.log(f"Error polling log updates: {e}")
def handle_proposal_result(self, result: ExtractionResult) -> None: def handle_proposal_result(self, result: ExtractionResult) -> None:
table = self.query_one("#pending-facts-table", DataTable) table = self.query_one("#pending-facts-table", DataTable)
for update in result.lore_updates + result.character_updates: for update in result.lore_updates + result.character_updates: