1.0 Release

This commit is contained in:
2026-03-05 20:07:35 +00:00
parent e4a2dd7bf2
commit 58f20856fd
15 changed files with 1208 additions and 1002 deletions
-38
View File
@@ -1,38 +0,0 @@
# --- Connection Settings ---
api:
base_url: "http://framework.tawny-bellatrix.ts.net:1234"
api_version: "/v1/"
# --- Model Settings ---
models:
enrich: "lm_studio/qwen-"
embedding: "text-embedding-qwen3-embedding-8b"
retrieval: "lm_studio/qwen/qwen3-30b-a3b-2507"
# --- Ingestion Settings ---
ingestion:
data_dir: "/home/cosmic/DnD"
db_path: "./data/dmv.db"
active_llms: 5
parallel_requests_per_llm: 2
chunk_size: 800
chunk_overlap: 100
embedding_batch_size: 32
time_file_location: "./data/time_file.txt"
# ---- Agent Settings ----
ingestion_agent:
ingestion_signature: |
You are an expert Dungeon Master's assistant.
Analyze the provided notes and extract a concise synopsis and relevant metadata.
synopsis = A one-sentence summary of the document.
tags = Relevant tags (NPCs, Locations, Items, Plot Points).
entities = a list of Key names of people, places, or factions.
"note -> synopsis:str, tags: list[str], entities: list[str]"
retrieval_agent:
retrieval_signature: |
You are an expert Dungeon Master's assistant.
Given the context and the question, answer the question.
Do not make things up, base all of your answers on the context.
Always site your sources
+1 -1
View File
@@ -1,7 +1,7 @@
import yaml
def load_config(config_path="src/config.yaml"):
def load_config(config_path="config.yaml"):
with open(config_path) as f:
return yaml.safe_load(f)
+2 -3
View File
@@ -6,10 +6,9 @@ CFG = load_config()
API_BASE = CFG["api"]["base_url"]
API_VERSION = CFG["api"]["api_version"]
class LocalLMEmbeddings(Embeddings):
def __init__(
self, model: str, base_url: str = API_BASE, batch_size: int = 32
):
def __init__(self, model: str, base_url: str = API_BASE, batch_size: int = 32):
self.url = f"{base_url}/{API_VERSION}embeddings"
self.model = model
self.batch_size = batch_size
+6 -2
View File
@@ -5,11 +5,15 @@ from config_loader import load_config
CFG = load_config()
INGESTION_CONFIG = CFG["ingestion_agent"]
class IngestionSignature(dspy.Signature):
f"{INGESTION_CONFIG["ingestion_signature"]}"
f"{INGESTION_CONFIG['ingestion_signature']}"
note: str = dspy.InputField(desc="The DM notes or session recap content.")
answer: dict[str,str|List] = dspy.OutputField(desc="the metadata dictionary with the keys; synopsis, tags, entities")
answer: dict[str, str | List] = dspy.OutputField(
desc="the metadata dictionary with the keys; synopsis, tags, entities"
)
class IngestionAgent(dspy.Module):
def __init__(self):
+13 -17
View File
@@ -28,13 +28,12 @@ def retrieve_from_turso(embedded_question, k=5):
rows = cur.fetchall()
return rows
# --- DSPy Signature ---
class DnDContextQA(dspy.Signature):
f"{RETRIEVAL_CONFIG["retrieval_signature"]}"
f"{RETRIEVAL_CONFIG['retrieval_signature']}"
context = dspy.InputField(
desc="Relevant chunks and metadata from the campaign notes."
)
context = dspy.InputField(desc="Relevant chunks and metadata from the campaign notes.")
question = dspy.InputField()
answer = dspy.OutputField(desc="A detailed answer based on the notes, citing the source file.")
@@ -45,16 +44,14 @@ class DnDRAG(dspy.Module):
self.embeddings_model = LocalLMEmbeddings(
model=EMBEDDING_MODEL,
base_url=API_BASE,
batch_size=1, # we only send 1 question at a time.
)
# Tools exposed to the ReAct loop
self.tools = [
self.load_file
]
self.generate_answer = dspy.ReAct(signature=DnDContextQA,tools=self.tools)
batch_size=1, # we only send 1 question at a time.
)
# Tools exposed to the ReAct loop
self.tools = [self.load_file]
self.generate_answer = dspy.ReAct(signature=DnDContextQA, tools=self.tools)
def forward(self, question):
# TODO: Add step here to LLM Expand
# TODO: Add step here to LLM Expand
# given the current question, generate 3-5 distinct search queries.
# embed all the questions
embedded_question = self.embeddings_model._post_request(question)
@@ -66,13 +63,12 @@ class DnDRAG(dspy.Module):
for i, row in enumerate(results):
source = row[0] # file_path
synopsis = row[1] # synopsis
tags = row[2] # tags
tags = row[2] # tags
entities = row[3] # entities
content = row[4] # chunk_data
context_parts.append(f"""
--- Chunk {i+1} from {source} ---
--- Chunk {i + 1} from {source} ---
synopsis: {synopsis},
tags: {tags},
entities: {entities}
@@ -82,7 +78,7 @@ entities: {entities}
# print('Closest embedding hits')
# for part in context_parts:
# print(part)
context = "\n\n".join(context_parts)
prediction = self.generate_answer(context=context, question=question)
@@ -97,4 +93,4 @@ entities: {entities}
except Exception:
return None
else:
return None
return None
+6 -7
View File
@@ -13,6 +13,7 @@ RETRIEVE_MODEL = CFG["models"]["retrieval"]
API_BASE = CFG["api"]["base_url"]
API_VERSION = CFG["api"]["api_version"]
class CallbackHandler(BaseCallback):
"""Custom callback class for logging agent interactions."""
@@ -47,6 +48,7 @@ class CallbackHandler(BaseCallback):
def _is_reasoning_output(self, outputs):
return any(k.startswith("Thought") for k in outputs)
def setup_logging():
"""Set up logging configuration for Merlin."""
# Create a custom logger
@@ -60,15 +62,11 @@ def setup_logging():
console_handler.setLevel(logging.INFO)
# Create a file handler with rotation every 5MB
file_handler = RotatingFileHandler(
"dmv.log", maxBytes=5 * 1024 * 1024, backupCount=3
)
file_handler = RotatingFileHandler("data/dmv.log", maxBytes=5 * 1024 * 1024, backupCount=3)
file_handler.setLevel(logging.DEBUG)
# Create a formatter
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
# Set the formatter for the handler
console_handler.setFormatter(formatter)
@@ -129,5 +127,6 @@ def main():
except Exception as e:
print(f"\n⚠️ An error occurred: {e}")
if __name__ == "__main__":
main()
main()