diff --git a/.gitignore b/.gitignore index 86eb1c0..b824b69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ artifacts/ -__pycache__ +**/__pycache__/ diff --git a/src/__pycache__/__init__.cpython-314.pyc b/src/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index ba30e24..0000000 Binary files a/src/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/src/llm/__pycache__/__init__.cpython-314.pyc b/src/llm/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 3c08e5d..0000000 Binary files a/src/llm/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/src/llm/__pycache__/models.cpython-314.pyc b/src/llm/__pycache__/models.cpython-314.pyc deleted file mode 100644 index 2dab234..0000000 Binary files a/src/llm/__pycache__/models.cpython-314.pyc and /dev/null differ diff --git a/src/llm/__pycache__/processor.cpython-314.pyc b/src/llm/__pycache__/processor.cpython-314.pyc deleted file mode 100644 index e142040..0000000 Binary files a/src/llm/__pycache__/processor.cpython-314.pyc and /dev/null differ diff --git a/src/llm/__pycache__/prompts.cpython-314.pyc b/src/llm/__pycache__/prompts.cpython-314.pyc deleted file mode 100644 index 8d7af04..0000000 Binary files a/src/llm/__pycache__/prompts.cpython-314.pyc and /dev/null differ diff --git a/src/llm/processor.py b/src/llm/processor.py index ee241d2..d973614 100644 --- a/src/llm/processor.py +++ b/src/llm/processor.py @@ -1,3 +1,4 @@ +import logging import os from typing import Any, Dict, Optional @@ -7,6 +8,8 @@ from pydantic import ValidationError from .models import ExtractionResult from .prompts import EXTRACTION_SYSTEM_PROMPT, NOISE_FILTER_SYSTEM_PROMPT +logger = logging.getLogger(__name__) + class LLMProcessor: def __init__( @@ -47,7 +50,7 @@ class LLMProcessor: # but we can ensure the client is initialized. pass except Exception as e: - print(f"Error initializing LLM client for backend {backend}: {e}") + logger.error(f"Error initializing LLM client for backend {backend}: {e}") raise self.model = model or os.environ.get("LLM_MODEL", "gpt-4o") @@ -56,73 +59,98 @@ class LLMProcessor: self, system_prompt: str, user_prompt: str, + context: Optional[str] = None, response_format: Optional[Any] = None, ) -> str: """ Generic method to call the LLM. """ + messages = [ + {"role": "system", "content": system_prompt}, + ] + if context: + messages.append( + { + "role": "system", + "content": f"Context from previous conversation:\n{context}", + } + ) + + messages.append({"role": "user", "content": user_prompt}) + try: response = self.client.chat.completions.create( model=self.model, - messages=[ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ], + messages=messages, response_format=response_format, extra_body={"include_reasoning": False}, ) return response.choices[0].message.content except Exception as e: - print(f"LLM Error: {e}") + logger.error(f"LLM Error: {e}") return "" - def filter_transcript(self, text: str) -> str: + def filter_transcript(self, text: str, context: Optional[str] = None) -> str: """ Stage 1: Raw Transcript -> Filtered Text. """ - result = self._call_llm(NOISE_FILTER_SYSTEM_PROMPT, text) - print(f"LLM Processor (Filter): {text} -> {result}") + result = self._call_llm(NOISE_FILTER_SYSTEM_PROMPT, text, context=context) + logger.info(f"LLM Processor (Filter): {text} -> {result}") return result - def extract_structured_data(self, filtered_text: str) -> ExtractionResult: + def extract_structured_data( + self, filtered_text: str, context: Optional[str] = None + ) -> ExtractionResult: """ Stage 2: Filtered Text -> Structured Data. """ - print(f"LLM Processor (Extract): Calling extraction for: {filtered_text}") + logger.info(f"LLM Processor (Extract): Calling extraction for: {filtered_text}") try: # Using standard chat.completions.create with JSON mode for better compatibility with vLLM - print("LLM Processor (Extract): Sending request to backend...") + logger.info("LLM Processor (Extract): Sending request to backend...") + + messages = [ + {"role": "system", "content": EXTRACTION_SYSTEM_PROMPT}, + ] + if context: + messages.append( + { + "role": "system", + "content": f"Context from previous conversation:\n{context}", + } + ) + messages.append({"role": "user", "content": filtered_text}) + response = self.client.chat.completions.create( model=self.model, - messages=[ - {"role": "system", "content": EXTRACTION_SYSTEM_PROMPT}, - {"role": "user", "content": filtered_text}, - ], + messages=messages, response_format={"type": "json_object"}, extra_body={"include_reasoning": False}, ) - print("LLM Processor (Extract): Response received from backend.") + logger.info("LLM Processor (Extract): Response received from backend.") import json content = response.choices[0].message.content - print(f"LLM Processor (Extract): Raw JSON response: {content}") + logger.info(f"LLM Processor (Extract): Raw JSON response: {content}") data = json.loads(content) # Map the JSON data to the Pydantic model return ExtractionResult(**data) except Exception as e: - print(f"Extraction Error: {e}") + logger.error(f"Extraction Error: {e}") # Return an empty ExtractionResult if parsing fails return ExtractionResult() - def process_pipeline(self, raw_text: str) -> 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) + filtered_text = self.filter_transcript(raw_text, context=context) if not filtered_text: return ExtractionResult() - return self.extract_structured_data(filtered_text) + return self.extract_structured_data(filtered_text, context=context) diff --git a/src/persistence/__pycache__/__init__.cpython-314.pyc b/src/persistence/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 4c10d38..0000000 Binary files a/src/persistence/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/src/persistence/__pycache__/characters.cpython-314.pyc b/src/persistence/__pycache__/characters.cpython-314.pyc deleted file mode 100644 index c5d01df..0000000 Binary files a/src/persistence/__pycache__/characters.cpython-314.pyc and /dev/null differ diff --git a/src/persistence/__pycache__/lore.cpython-314.pyc b/src/persistence/__pycache__/lore.cpython-314.pyc deleted file mode 100644 index 2c6d68c..0000000 Binary files a/src/persistence/__pycache__/lore.cpython-314.pyc and /dev/null differ diff --git a/src/pipeline/__pycache__/orchestrator.cpython-314.pyc b/src/pipeline/__pycache__/orchestrator.cpython-314.pyc deleted file mode 100644 index 377ff5d..0000000 Binary files a/src/pipeline/__pycache__/orchestrator.cpython-314.pyc and /dev/null differ diff --git a/src/pipeline/orchestrator.py b/src/pipeline/orchestrator.py index aea3f2a..b9ddd09 100644 --- a/src/pipeline/orchestrator.py +++ b/src/pipeline/orchestrator.py @@ -1,5 +1,8 @@ import asyncio import logging +import os +from pathlib import Path +from typing import List, Optional from src.llm.models import ExtractionResult from src.llm.processor import LLMProcessor @@ -7,7 +10,14 @@ from src.stt.listener import AudioListener from src.stt.transcriber import Transcriber from src.ui.tui import ConfirmationApp -logging.basicConfig(level=logging.INFO) +# Configure logging to write to a file instead of stdout +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + handlers=[ + logging.FileHandler("pipeline.log"), + ], +) logger = logging.getLogger(__name__) @@ -17,7 +27,7 @@ class PipelineOrchestrator: # Modules self.listener = AudioListener(loop=self.loop) - self.transcriber = Transcriber() + self.transcriber = Transcriber(model_size="small") self.processor = LLMProcessor() # Queues @@ -26,6 +36,10 @@ class PipelineOrchestrator: self.is_running = False + # Conversation history for context + self.history = [] # List of strings (transcripts) + self.history_max_words = 1000 + async def stt_worker(self): """ Worker that handles STT: Audio -> Text. @@ -61,9 +75,29 @@ class PipelineOrchestrator: logger.info(f"LLM Worker: Processing text: {raw_text}") + # 1. Prepare Context (Conversation History) + # Maintain history and truncate to max words + self.history.append(raw_text) + full_history_text = " ".join(self.history) + words = full_history_text.split() + if len(words) > self.history_max_words: + # Keep the last N words + kept_words = words[-self.history_max_words :] + context_text = " ".join(kept_words) + else: + context_text = full_history_text + + # 2. Prepare Context (Wiki / Database of Knowledge) + wiki_context = self._get_wiki_context() + + # Combine both + combined_context = f"Conversation History:\n{context_text}\n\nWiki Knowledge:\n{wiki_context}" + # Process via LLM (Filter -> Extract) - # Note: this is currently a synchronous call, which blocks the loop. - result = self.processor.process_pipeline(raw_text) + # 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 + ) if ( result.lore_updates @@ -83,6 +117,30 @@ class PipelineOrchestrator: # Small sleep await asyncio.sleep(0.1) + def _get_wiki_context(self) -> str: + """ + Reads all files in the lore directory and returns them as a single context string. + """ + from src.persistence.lore import DATA_LORE_DIR + + wiki_contents = [] + # Recursively find all .md files in the lore directory + for path in DATA_LORE_DIR.rglob("*.md"): + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + wiki_contents.append( + f"File: {path.relative_to(DATA_LORE_DIR)}\nContent:\n{content}" + ) + except Exception as e: + logger.error(f"Error reading wiki file {path}: {e}") + + return ( + "\n\n".join(wiki_contents) + if wiki_contents + else "No wiki knowledge available." + ) + async def tui_worker(self): """ Worker that handles TUI: Proposal -> Persistence. @@ -93,8 +151,11 @@ class PipelineOrchestrator: # Pass the proposal queue to the app. app = ConfirmationApp(proposal_queue=self.proposal_queue) await app.run_async() + # Once the TUI exits, stop the entire pipeline + self.stop() except Exception as e: logger.error(f"TUI Worker error: {e}") + self.stop() async def run(self): """ diff --git a/src/stt/__pycache__/__init__.cpython-314.pyc b/src/stt/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index ffdeb67..0000000 Binary files a/src/stt/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/src/stt/__pycache__/listener.cpython-314.pyc b/src/stt/__pycache__/listener.cpython-314.pyc deleted file mode 100644 index 393e9fd..0000000 Binary files a/src/stt/__pycache__/listener.cpython-314.pyc and /dev/null differ diff --git a/src/stt/__pycache__/transcriber.cpython-314.pyc b/src/stt/__pycache__/transcriber.cpython-314.pyc deleted file mode 100644 index 8936e12..0000000 Binary files a/src/stt/__pycache__/transcriber.cpython-314.pyc and /dev/null differ diff --git a/src/stt/listener.py b/src/stt/listener.py index 73defdc..959311d 100644 --- a/src/stt/listener.py +++ b/src/stt/listener.py @@ -5,7 +5,7 @@ import numpy as np import sounddevice as sd import torch -logging.basicConfig(level=logging.INFO) +# Do not call basicConfig here, as it's called in the orchestrator logger = logging.getLogger(__name__) diff --git a/src/stt/transcriber.py b/src/stt/transcriber.py index 188bbe8..18a276b 100644 --- a/src/stt/transcriber.py +++ b/src/stt/transcriber.py @@ -2,7 +2,7 @@ import logging from faster_whisper import WhisperModel -logging.basicConfig(level=logging.INFO) +# Do not call basicConfig here, as it's called in the orchestrator logger = logging.getLogger(__name__) diff --git a/src/ui/__pycache__/__init__.cpython-314.pyc b/src/ui/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 7db42bd..0000000 Binary files a/src/ui/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/src/ui/__pycache__/cli.cpython-314.pyc b/src/ui/__pycache__/cli.cpython-314.pyc deleted file mode 100644 index 164d568..0000000 Binary files a/src/ui/__pycache__/cli.cpython-314.pyc and /dev/null differ diff --git a/src/ui/__pycache__/tui.cpython-314.pyc b/src/ui/__pycache__/tui.cpython-314.pyc deleted file mode 100644 index 1064f1c..0000000 Binary files a/src/ui/__pycache__/tui.cpython-314.pyc and /dev/null differ diff --git a/src/ui/tui.py b/src/ui/tui.py index 8e914bb..9cb5454 100644 --- a/src/ui/tui.py +++ b/src/ui/tui.py @@ -140,13 +140,12 @@ class ConfirmationApp(App): try: result = await self.proposal_queue.get() self.add_result(result) - except Exception as e: - # Log error but keep the worker running - self.log(f"Error polling proposal queue: {e}") - finally: # Signal that the item has been processed if hasattr(self.proposal_queue, "task_done"): self.proposal_queue.task_done() + except Exception as e: + # Log error but keep the worker running + self.log(f"Error polling proposal queue: {e}") def add_result(self, result: ExtractionResult) -> None: """