Commit e31efbb9 authored by Kantz's avatar Kantz
Browse files

Merge branch 'dev' into 'main'

Dev

See merge request kantz/tutor_react!4
parents 43e305bb 5e93567b
...@@ -85,4 +85,3 @@ def decode_references(text: str, sources: Iterable[Source]) -> Tuple[str, int]: ...@@ -85,4 +85,3 @@ def decode_references(text: str, sources: Iterable[Source]) -> Tuple[str, int]:
return f"[{ref}]({href})" return f"[{ref}]({href})"
return _BRACKET_RE.sub(replace, text), replacements return _BRACKET_RE.sub(replace, text), replacements
# app/deterministic_services/retrieval_service.py
from __future__ import annotations
from typing import List
from app.deterministic_services import Source
from app import config
from app.deterministic_services.embeddings import EmbeddingFactory
from app.deterministic_services import vector_store
def retrieve_context(query_text: str, pg_url: str | None = None) -> List[Source]:
embedder = EmbeddingFactory.create(config.get_embedding_settings())
url = pg_url or config.get_postgres_url()
sources = vector_store.retrieve(
pg_url=url,
embedder=embedder,
query=query_text,
k=8,
expand_links=True,
)
return sources
\ No newline at end of file
from __future__ import annotations
from typing import List
import app.config as config
from app.deterministic_services import vector_store, vector_store_subsection
from app.deterministic_services.vector_store import EmbeddingLike, Source
def _use_subsection_retrieval() -> bool:
return config.get_retrieval_impl() == "subsection"
def retrieve(
pg_url: str,
embedder: EmbeddingLike,
query: str,
k: int = 4,
section_index: int | None = None,
subsection_index: int | None = None,
source_type_filter: list[str] | None = None,
expand_links: bool = True,
neighbor_expand: int = 0,
) -> List[Source]:
if _use_subsection_retrieval():
return vector_store_subsection.retrieve(
pg_url=pg_url,
embedder=embedder,
query=query,
k=k,
section_index=section_index,
subsection_index=subsection_index,
source_type_filter=source_type_filter,
expand_links=expand_links,
neighbor_expand=neighbor_expand,
)
return vector_store.retrieve(
pg_url=pg_url,
embedder=embedder,
query=query,
k=k,
section_index=section_index,
subsection_index=subsection_index,
source_type_filter=source_type_filter,
expand_links=expand_links,
neighbor_expand=neighbor_expand,
)
...@@ -49,7 +49,7 @@ def archive_chat(messages: list[dict[str, Any]], draft: str | None = None) -> st ...@@ -49,7 +49,7 @@ def archive_chat(messages: list[dict[str, Any]], draft: str | None = None) -> st
def _summarize_record(record: dict[str, Any]) -> dict[str, Any]: def _summarize_record(record: dict[str, Any]) -> dict[str, Any]:
history = record.get("history", []) history = record.get("history", [])
preview = "" preview = ""
for entry in reversed(history): for entry in history:
if entry.get("role") == "user" and entry.get("content"): if entry.get("role") == "user" and entry.get("content"):
preview = entry["content"][:120] preview = entry["content"][:120]
break break
...@@ -100,7 +100,8 @@ def load_archive(chat_id: str) -> dict[str, Any] | None: ...@@ -100,7 +100,8 @@ def load_archive(chat_id: str) -> dict[str, Any] | None:
continue continue
if record.get("chat_id") == chat_id: if record.get("chat_id") == chat_id:
history = [ history = [
{"role": entry.get("role", "user"), "text": entry.get("content", "")} {"role": entry.get("role", "user"),
"text": entry.get("content", "")}
for entry in record.get("history", []) for entry in record.get("history", [])
] ]
return { return {
......
...@@ -5,7 +5,7 @@ from datetime import datetime ...@@ -5,7 +5,7 @@ from datetime import datetime
def _format_log_timestamp(created_at: str | None) -> str: def _format_log_timestamp(created_at: str | None) -> str:
if not created_at: if not created_at:
return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") return datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
try: try:
parsed = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%SZ") parsed = datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%SZ")
return parsed.strftime("%Y-%m-%dT%H:%M:%SZ") return parsed.strftime("%Y-%m-%dT%H:%M:%SZ")
...@@ -37,7 +37,8 @@ def write_tool_log(entries: list[dict], created_at: str | None = None, chat_id: ...@@ -37,7 +37,8 @@ def write_tool_log(entries: list[dict], created_at: str | None = None, chat_id:
existing = json.load(f) existing = json.load(f)
if isinstance(existing, dict) and isinstance(existing.get("entries"), list): if isinstance(existing, dict) and isinstance(existing.get("entries"), list):
payload["entries"] = existing["entries"] + payload["entries"] payload["entries"] = existing["entries"] + payload["entries"]
payload["timestamp"] = existing.get("timestamp", payload["timestamp"]) payload["timestamp"] = existing.get(
"timestamp", payload["timestamp"])
except (OSError, json.JSONDecodeError): except (OSError, json.JSONDecodeError):
pass pass
with open(path, "w", encoding="utf-8") as f: with open(path, "w", encoding="utf-8") as f:
......
...@@ -12,6 +12,14 @@ from pgvector.psycopg import register_vector ...@@ -12,6 +12,14 @@ from pgvector.psycopg import register_vector
import yaml import yaml
from pydantic import BaseModel from pydantic import BaseModel
import app.config
embedding_dim = app.config.get_embedding_settings().target_dim
# --------------------------------------------------------------------------------------------------------------------
# Einlesen der Dokumente
# --------------------------------------------------------------------------------------------------------------------
@dataclass @dataclass
class DocRecord: class DocRecord:
...@@ -107,17 +115,12 @@ def load_docs(base_dir: Path) -> List[DocRecord]: ...@@ -107,17 +115,12 @@ def load_docs(base_dir: Path) -> List[DocRecord]:
return docs return docs
# --------------------------------------------------------------------------------------------------------------------
# Init der Databse
# --------------------------------------------------------------------------------------------------------------------
def embed_passages(embedder: EmbeddingLike, texts: List[str]) -> List[List[float]]:
prefixed = [f"passage: {t}" for t in texts]
return embedder.embed_documents(prefixed)
def embed_query(embedder: EmbeddingLike, text: str) -> List[float]:
return embedder.embed_query(f"query: {text}")
DATABASE_CREATION_SQL = f"""
DDL = """
CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS docs ( CREATE TABLE IF NOT EXISTS docs (
...@@ -135,7 +138,7 @@ CREATE TABLE IF NOT EXISTS docs ( ...@@ -135,7 +138,7 @@ CREATE TABLE IF NOT EXISTS docs (
path TEXT NOT NULL, path TEXT NOT NULL,
markdown TEXT NOT NULL, markdown TEXT NOT NULL,
embedding VECTOR(1024) NOT NULL embedding VECTOR({embedding_dim}) NOT NULL
); );
CREATE INDEX IF NOT EXISTS docs_embedding_cos_idx CREATE INDEX IF NOT EXISTS docs_embedding_cos_idx
...@@ -147,6 +150,19 @@ CREATE INDEX IF NOT EXISTS docs_section_idx ON docs(section_index); ...@@ -147,6 +150,19 @@ CREATE INDEX IF NOT EXISTS docs_section_idx ON docs(section_index);
CREATE INDEX IF NOT EXISTS docs_subsection_idx ON docs(section_index, subsection_index); CREATE INDEX IF NOT EXISTS docs_subsection_idx ON docs(section_index, subsection_index);
""" """
def init_db(pg_url: str) -> None:
with psycopg.connect(pg_url) as conn:
with conn.cursor() as cur:
cur.execute(DATABASE_CREATION_SQL)
register_vector(conn)
conn.commit()
# --------------------------------------------------------------------------------------------------------------------
# Einfügen der Dokumente und Embeddings
# --------------------------------------------------------------------------------------------------------------------
UPSERT_SQL = """ UPSERT_SQL = """
INSERT INTO docs ( INSERT INTO docs (
uid, doc_type, uid, doc_type,
...@@ -175,14 +191,6 @@ ON CONFLICT (uid) DO UPDATE SET ...@@ -175,14 +191,6 @@ ON CONFLICT (uid) DO UPDATE SET
""" """
def init_db(pg_url: str) -> None:
with psycopg.connect(pg_url) as conn:
with conn.cursor() as cur:
cur.execute(DDL)
register_vector(conn)
conn.commit()
def _meta_int(meta: Dict[str, Any], key: str) -> Optional[int]: def _meta_int(meta: Dict[str, Any], key: str) -> Optional[int]:
value = meta.get(key) value = meta.get(key)
if value is None or value == "": if value is None or value == "":
...@@ -232,6 +240,18 @@ def clear_docs(pg_url: str) -> None: ...@@ -232,6 +240,18 @@ def clear_docs(pg_url: str) -> None:
conn.commit() conn.commit()
def embed_documents(embedder: EmbeddingLike, texts: List[str]) -> List[List[float]]:
return embedder.embed_documents(texts)
def embed_query(embedder: EmbeddingLike, text: str) -> List[float]:
return embedder.embed_query(text)
# --------------------------------------------------------------------------------------------------------------------
# Retrival der Dokumente
# --------------------------------------------------------------------------------------------------------------------
@dataclass @dataclass
class Retrieved: class Retrieved:
uid: str uid: str
...@@ -249,57 +269,6 @@ class Retrieved: ...@@ -249,57 +269,6 @@ class Retrieved:
"markdown": self.markdown, "markdown": self.markdown,
} }
class Source(BaseModel):
source_id: SourceID
retrieved_as: str
source_type: str
score: float
markdown: str
def to_dict(self) -> Dict[str, Any]:
return {
"source_id": self.source_id.to_dict(),
"retrieved_as": self.retrieved_as,
"source_type": self.source_type,
"score": self.score,
"markdown": self.markdown,
}
def to_string(self) -> str:
return (
f"Source(source_id={self.source_id.to_string()},\n"
f" retrieved_as={self.retrieved_as},\n"
f" source_type={self.source_type},\n"
f" score={self.score},\n"
f" markdown={self.markdown}"
)
class SourceID(BaseModel):
chapter_title: Optional[str] = None
section_title: Optional[str] = None
subsection_title: Optional[str] = None
title: str
doc_type: str
def to_dict(self) -> Dict[str, Any]:
return {
"chapter_title": self.chapter_title,
"section_title": self.section_title,
"subsection_title": self.subsection_title,
"title": self.title,
"doc_type": self.doc_type,
}
def to_string(self) -> str:
string_rep = self.title
if self.subsection_title:
string_rep = f"{self.subsection_title}|{string_rep}"
if self.section_title:
string_rep = f"{self.section_title}|{string_rep}"
if self.chapter_title:
string_rep = f"{self.chapter_title}|{string_rep}"
return f"[{string_rep}|{self.doc_type}]"
def _row_to_retrieved(row: Dict[str, Any], source_type: Optional[str] = None) -> Retrieved: def _row_to_retrieved(row: Dict[str, Any], source_type: Optional[str] = None) -> Retrieved:
meta = { meta = {
...@@ -323,26 +292,6 @@ def _row_to_retrieved(row: Dict[str, Any], source_type: Optional[str] = None) -> ...@@ -323,26 +292,6 @@ def _row_to_retrieved(row: Dict[str, Any], source_type: Optional[str] = None) ->
) )
def _retrivla_to_sources(retrievd : Dict[str, List[Retrieved]]) -> List[Source]:
sources = []
for name, retrieved_grouep in retrievd.items():
for retrival in retrieved_grouep:
sources.append(Source(
source_id=SourceID(
chapter_title = "none",
section_title=retrival.metadata.get("section_title"),
subsection_title=retrival.metadata.get("subsection_title"),
title=retrival.metadata.get("title"),
doc_type=retrival.doc_type
),
retrieved_as=name,
source_type=retrival.metadata.get("source_type") or "unknown",
score=retrival.score,
markdown=retrival.markdown
))
return sources
def retrieve( def retrieve(
pg_url: str, pg_url: str,
embedder: EmbeddingLike, embedder: EmbeddingLike,
...@@ -411,7 +360,8 @@ def retrieve( ...@@ -411,7 +360,8 @@ def retrieve(
sec_sub_counts[key] = sec_sub_counts.get(key, 0) + 1 sec_sub_counts[key] = sec_sub_counts.get(key, 0) + 1
if sec_sub_counts: if sec_sub_counts:
most_common_sec_sub = max(sec_sub_counts.items(), key=lambda x: x[1])[0] most_common_sec_sub = max(
sec_sub_counts.items(), key=lambda x: x[1])[0]
most_common_sec, most_common_sub = most_common_sec_sub most_common_sec, most_common_sub = most_common_sec_sub
cur.execute( cur.execute(
...@@ -427,9 +377,11 @@ def retrieve( ...@@ -427,9 +377,11 @@ def retrieve(
AND d.section_index = %(sec)s AND d.section_index = %(sec)s
AND d.subsection_index = %(sub)s AND d.subsection_index = %(sub)s
""", """,
{"sec": most_common_sec, "sub": most_common_sub, "sub_doc_types": ["subsection", "chapter"]}, {"sec": most_common_sec, "sub": most_common_sub,
"sub_doc_types": ["subsection", "chapter"]},
) )
subsections = [_row_to_retrieved(row, source_type="subsection") for row in cur.fetchall()] subsections = [_row_to_retrieved(
row, source_type="subsection") for row in cur.fetchall()]
cur.execute( cur.execute(
""" """
...@@ -443,9 +395,11 @@ def retrieve( ...@@ -443,9 +395,11 @@ def retrieve(
WHERE doc_type = ANY(%(sec_doc_types)s) WHERE doc_type = ANY(%(sec_doc_types)s)
AND section_index = %(sec)s AND section_index = %(sec)s
""", """,
{"sec": most_common_sec, "sec_doc_types": ["section", "oberchapter"]}, {"sec": most_common_sec, "sec_doc_types": [
"section", "oberchapter"]},
) )
sections_docs = [_row_to_retrieved(row, source_type="section") for row in cur.fetchall()] sections_docs = [_row_to_retrieved(
row, source_type="section") for row in cur.fetchall()]
cur.execute( cur.execute(
""" """
...@@ -462,12 +416,15 @@ def retrieve( ...@@ -462,12 +416,15 @@ def retrieve(
ORDER BY d.embedding <=> %(qvec)s ORDER BY d.embedding <=> %(qvec)s
LIMIT 5; LIMIT 5;
""", """,
{"qvec": qvec, "sec": most_common_sec, "sub": most_common_sub}, {"qvec": qvec, "sec": most_common_sec,
"sub": most_common_sub},
) )
additional_children = [_row_to_retrieved(row) for row in cur.fetchall()] additional_children = [_row_to_retrieved(
row) for row in cur.fetchall()]
existing_uids = {child.uid for child in children} existing_uids = {child.uid for child in children}
new_children = [child for child in additional_children if child.uid not in existing_uids] new_children = [
child for child in additional_children if child.uid not in existing_uids]
children.extend(new_children) children.extend(new_children)
children_expanded.extend(new_children) children_expanded.extend(new_children)
...@@ -503,12 +460,15 @@ def retrieve( ...@@ -503,12 +460,15 @@ def retrieve(
ON d.section_index = u.sec AND d.subsection_index = u.sub AND d.child_index = u.child ON d.section_index = u.sec AND d.subsection_index = u.sub AND d.child_index = u.child
WHERE d.doc_type = 'child' WHERE d.doc_type = 'child'
""", """,
{"sec_arr": sec_arr, "sub_arr": sub_arr, "child_arr": child_arr}, {"sec_arr": sec_arr, "sub_arr": sub_arr,
"child_arr": child_arr},
) )
neighbors = [_row_to_retrieved(row) for row in cur.fetchall()] neighbors = [_row_to_retrieved(
row) for row in cur.fetchall()]
child_uids = {child.uid for child in children} child_uids = {child.uid for child in children}
neighbors = [neighbor for neighbor in neighbors if neighbor.uid not in child_uids] neighbors = [
neighbor for neighbor in neighbors if neighbor.uid not in child_uids]
retrivla_dict = { retrivla_dict = {
"children_direct": children_direct, "children_direct": children_direct,
"children_expanded": children_expanded, "children_expanded": children_expanded,
...@@ -519,6 +479,87 @@ def retrieve( ...@@ -519,6 +479,87 @@ def retrieve(
sources = _retrivla_to_sources(retrivla_dict) sources = _retrivla_to_sources(retrivla_dict)
return sources return sources
# --------------------------------------------------------------------------------------------------------------------
# Retrival in Sources umwandeln
# --------------------------------------------------------------------------------------------------------------------
class Source(BaseModel):
source_id: SourceID
retrieved_as: str
source_type: str
score: float
markdown: str
def to_dict(self) -> Dict[str, Any]:
return {
"source_id": self.source_id.to_dict(),
"retrieved_as": self.retrieved_as,
"source_type": self.source_type,
"score": self.score,
"markdown": self.markdown,
}
def to_string(self) -> str:
return (
f"Source(source_id={self.source_id.to_string()},\n"
f" retrieved_as={self.retrieved_as},\n"
f" source_type={self.source_type},\n"
f" score={self.score},\n"
f" markdown={self.markdown}"
)
class SourceID(BaseModel):
chapter_title: Optional[str] = None
section_title: Optional[str] = None
subsection_title: Optional[str] = None
title: str
doc_type: str
def to_dict(self) -> Dict[str, Any]:
return {
"chapter_title": self.chapter_title,
"section_title": self.section_title,
"subsection_title": self.subsection_title,
"title": self.title,
"doc_type": self.doc_type,
}
def to_string(self) -> str:
string_rep = self.title
if self.subsection_title:
string_rep = f"{self.subsection_title}|{string_rep}"
if self.section_title:
string_rep = f"{self.section_title}|{string_rep}"
if self.chapter_title:
string_rep = f"{self.chapter_title}|{string_rep}"
return f"[{string_rep}|{self.doc_type}]"
def _retrivla_to_sources(retrievd: Dict[str, List[Retrieved]]) -> List[Source]:
sources = []
for name, retrieved_grouep in retrievd.items():
for retrival in retrieved_grouep:
sources.append(Source(
source_id=SourceID(
chapter_title="none",
section_title=retrival.metadata.get("section_title"),
subsection_title=retrival.metadata.get("subsection_title"),
title=retrival.metadata.get("title"),
doc_type=retrival.doc_type
),
retrieved_as=name,
source_type=retrival.metadata.get("source_type") or "unknown",
score=retrival.score,
markdown=retrival.markdown
))
return sources
# --------------------------------------------------------------------------------------------------------------------
# Listen für Filterung
# --------------------------------------------------------------------------------------------------------------------
def list_sections(pg_url: str) -> List[Dict[str, Any]]: def list_sections(pg_url: str) -> List[Dict[str, Any]]:
sql = """ sql = """
......
from __future__ import annotations
from typing import Any, Dict, List, Optional
import psycopg
from pgvector import Vector
from pgvector.psycopg import register_vector
from psycopg.rows import dict_row
from app.deterministic_services.vector_store import (
EmbeddingLike,
Source,
_retrivla_to_sources,
_row_to_retrieved,
embed_query,
)
def retrieve(
pg_url: str,
embedder: EmbeddingLike,
query: str,
k: int = 4,
section_index: Optional[int] = None,
subsection_index: Optional[int] = None,
source_type_filter: Optional[List[str]] = None,
expand_links: bool = False,
neighbor_expand: int = 0,
) -> List[Source]:
# Parameters kept for drop-in compatibility with child-level retrieve.
_ = expand_links
_ = neighbor_expand
qvec = Vector(embed_query(embedder, query))
where = ["doc_type = ANY(%(sub_doc_types)s)"]
params: Dict[str, Any] = {
"qvec": qvec,
"k": k,
"sub_doc_types": ["subsection", "chapter"],
}
if section_index is not None:
where.append("section_index = %(section_index)s")
params["section_index"] = section_index
if subsection_index is not None:
where.append("subsection_index = %(subsection_index)s")
params["subsection_index"] = subsection_index
if source_type_filter:
where.append("source_type = ANY(%(source_type_filter)s)")
params["source_type_filter"] = source_type_filter
where_sql = " AND ".join(where)
sql = f"""
SELECT
uid, doc_type,
section_index, subsection_index, child_index,
section_title, subsection_title, title, source_type,
path, markdown,
1 - (embedding <=> %(qvec)s) AS score
FROM docs
WHERE {where_sql}
ORDER BY embedding <=> %(qvec)s
LIMIT %(k)s;
"""
with psycopg.connect(pg_url, row_factory=dict_row) as conn:
register_vector(conn)
with conn.cursor() as cur:
cur.execute(sql, params)
rows = cur.fetchall()
subsections = [_row_to_retrieved(
row, source_type="subsection") for row in rows]
return _retrivla_to_sources({"subsections_direct": subsections})
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import logging
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from app.api import canvas, chat, health, retrieval, context from app.api import canvas, chat, health, context
from app.config import get_frontend_url from app.config import get_frontend_url
from app.deterministic_services import embedding_provider
logger = logging.getLogger(__name__)
@asynccontextmanager @asynccontextmanager
async def lifespan(_app: FastAPI): async def lifespan(_app: FastAPI):
health.run_startup_checks() health.run_startup_checks()
try:
warmup_timing = embedding_provider.warmup_embedder()
logger.info("Embedding warmup finished: %s", warmup_timing)
except Exception:
logger.exception("Embedding warmup failed")
yield yield
...@@ -24,6 +34,5 @@ app.add_middleware( ...@@ -24,6 +34,5 @@ app.add_middleware(
app.include_router(chat.router) app.include_router(chat.router)
app.include_router(canvas.router) app.include_router(canvas.router)
app.include_router(retrieval.router)
app.include_router(context.router) app.include_router(context.router)
app.include_router(health.router) app.include_router(health.router)
__all__ = [
"hint_tool",
"math_tool",
"retrieval_tool",
]
import re
import sympy as sp
from sympy.parsing.latex import parse_latex
from sympy.parsing.sympy_parser import (
parse_expr,
standard_transformations,
implicit_multiplication_application,
)
transformations = standard_transformations + (implicit_multiplication_application,)
LATEX_HINTS = re.compile(r"(\\[a-zA-Z]+)|(\$[^$]+\$)|(\^\{)|(_\{)")
def looks_like_latex(text: str) -> bool:
return bool(LATEX_HINTS.search(text))
def parse_input(expr_text: str, sympy_symbols: dict[str, sp.Symbol]) -> sp.Expr:
if looks_like_latex(expr_text):
return parse_latex(expr_text)
return parse_expr(expr_text, transformations=transformations, local_dict=sympy_symbols)
def sympy_solve(task: str, input: str, symbols: list[str] | None = None) -> str:
try:
sympy_symbols: dict[str, sp.Symbol] = {}
if symbols:
for s in symbols:
sympy_symbols[s] = sp.symbols(s)
if "=" in input:
lhs, rhs = input.split("=")
expr = sp.Eq(parse_input(lhs, sympy_symbols), parse_input(rhs, sympy_symbols))
else:
expr = parse_input(input, sympy_symbols)
if task == "solve":
result = sp.solve(expr, list(sympy_symbols.values()) if symbols else None)
elif task == "simplify":
result = sp.simplify(expr)
elif task == "diff":
result = sp.diff(expr, *sympy_symbols.values())
elif task == "integrate":
result = sp.integrate(expr, *sympy_symbols.values())
else:
return f"Unsupported task: {task}"
return str(result)
except Exception as exc:
return f"SymPy error: {str(exc)}"
TOOL_SPEC = {
"type": "function",
"function": {
"name": "sympy_solve",
"description": (
"Loese oder bearbeite mathematische Ausdruecke mit SymPy. "
"Nutze dieses Tool, wenn eine mathematische Formel oder Gleichung erscheint."
),
"parameters": {
"type": "object",
"properties": {
"task": {
"type": "string",
"enum": ["solve", "simplify", "diff", "integrate"],
"description": "Mathematische Operation",
},
"input": {
"type": "string",
"description": "Mathematischer Ausdruck oder Gleichung, z. B. x**2 - 4 = 0",
},
"symbols": {
"type": "array",
"items": {"type": "string"},
"description": 'Variablen, z. B. ["x"]',
},
},
"required": ["task", "input"],
},
},
}
...@@ -10,3 +10,9 @@ sympy ...@@ -10,3 +10,9 @@ sympy
psycopg[binary] psycopg[binary]
pgvector pgvector
pyyaml pyyaml
sentence-transformers
transformers
torch
peft
torchvision
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import annotations from __future__ import annotations
from app.deterministic_services import retrieval_store, vector_store
from app.deterministic_services.embeddings import BaseEmbeddings, EmbeddingFactory
import argparse import argparse
import os import os
...@@ -17,13 +19,11 @@ ROOT_DIR = Path(__file__).resolve().parents[1] ...@@ -17,13 +19,11 @@ ROOT_DIR = Path(__file__).resolve().parents[1]
if str(ROOT_DIR) not in sys.path: if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR)) sys.path.insert(0, str(ROOT_DIR))
from app.deterministic_services.embeddings import BaseEmbeddings, EmbeddingFactory
from app.deterministic_services import vector_store
def build_embedder() -> BaseEmbeddings: def build_embedder() -> BaseEmbeddings:
return EmbeddingFactory.create(config.get_embedding_settings()) return EmbeddingFactory.create(config.get_embedding_settings())
def cli_init_db(args: argparse.Namespace) -> None: def cli_init_db(args: argparse.Namespace) -> None:
pg_url = args.pg or os.getenv("POSTGRES_URL") pg_url = args.pg or os.getenv("POSTGRES_URL")
if not pg_url: if not pg_url:
...@@ -36,7 +36,8 @@ def cli_ingest(args: argparse.Namespace) -> None: ...@@ -36,7 +36,8 @@ def cli_ingest(args: argparse.Namespace) -> None:
embedder = build_embedder() embedder = build_embedder()
base_dir = Path(args.base) base_dir = Path(args.base)
docs = vector_store.load_docs(base_dir) docs = vector_store.load_docs(base_dir)
embeddings = vector_store.embed_passages(embedder, [doc.markdown for doc in docs]) embeddings = vector_store.embed_documents(
embedder, [doc.markdown for doc in docs])
pg_url = args.pg or os.getenv("POSTGRES_URL") pg_url = args.pg or os.getenv("POSTGRES_URL")
if not pg_url: if not pg_url:
raise ValueError("Missing POSTGRES_URL") raise ValueError("Missing POSTGRES_URL")
...@@ -51,7 +52,7 @@ def cli_query(args: argparse.Namespace) -> None: ...@@ -51,7 +52,7 @@ def cli_query(args: argparse.Namespace) -> None:
pg_url = args.pg or os.getenv("POSTGRES_URL") pg_url = args.pg or os.getenv("POSTGRES_URL")
if not pg_url: if not pg_url:
raise ValueError("Missing POSTGRES_URL") raise ValueError("Missing POSTGRES_URL")
result = vector_store.retrieve( result = retrieval_store.retrieve(
pg_url=pg_url, pg_url=pg_url,
embedder=embedder, embedder=embedder,
query=args.q, query=args.q,
...@@ -70,18 +71,24 @@ def main() -> int: ...@@ -70,18 +71,24 @@ def main() -> int:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
sub = parser.add_subparsers(required=True) sub = parser.add_subparsers(required=True)
ap_init = sub.add_parser("init-db", help="Create tables and indexes in Postgres") ap_init = sub.add_parser(
ap_init.add_argument("--pg", default=None, help="Postgres URL (or set POSTGRES_URL)") "init-db", help="Create tables and indexes in Postgres")
ap_init.add_argument("--pg", default=None,
help="Postgres URL (or set POSTGRES_URL)")
ap_init.set_defaults(func=cli_init_db) ap_init.set_defaults(func=cli_init_db)
ap_ing = sub.add_parser("ingest", help="Embed and upsert markdown docs") ap_ing = sub.add_parser("ingest", help="Embed and upsert markdown docs")
ap_ing.add_argument("--pg", default=None, help="Postgres URL (or set POSTGRES_URL)") ap_ing.add_argument("--pg", default=None,
ap_ing.add_argument("--base", default="markdown", help="Base folder with sections/subsections/childs") help="Postgres URL (or set POSTGRES_URL)")
ap_ing.add_argument("--clear", action="store_true", help="Clear docs table before ingest") ap_ing.add_argument("--base", default="markdown",
help="Base folder with sections/subsections/childs")
ap_ing.add_argument("--clear", action="store_true",
help="Clear docs table before ingest")
ap_ing.set_defaults(func=cli_ingest) ap_ing.set_defaults(func=cli_ingest)
ap_q = sub.add_parser("query", help="Run retrieval") ap_q = sub.add_parser("query", help="Run retrieval")
ap_q.add_argument("--pg", default=None, help="Postgres URL (or set POSTGRES_URL)") ap_q.add_argument("--pg", default=None,
help="Postgres URL (or set POSTGRES_URL)")
ap_q.add_argument("--q", required=True, help="Query text") ap_q.add_argument("--q", required=True, help="Query text")
ap_q.add_argument("--k", type=int, default=8) ap_q.add_argument("--k", type=int, default=8)
ap_q.add_argument("--expand", action="store_true") ap_q.add_argument("--expand", action="store_true")
......
import argparse import argparse
import json import json
from typing import Any, Iterable from typing import Any
from app.LLM_services import decision_LLM from app.LLM_services import decision_LLM
from app.deterministic_services import context_store from app.deterministic_services import context_store
...@@ -17,37 +17,27 @@ def _resolve_sheet(args: argparse.Namespace) -> dict[str, Any]: ...@@ -17,37 +17,27 @@ def _resolve_sheet(args: argparse.Namespace) -> dict[str, Any]:
if args.chat_id: if args.chat_id:
sheet = context_store.load_sheet(args.chat_id) sheet = context_store.load_sheet(args.chat_id)
if sheet is None: if sheet is None:
raise FileNotFoundError(f"Kein Context-Sheet gefunden fuer chat_id={args.chat_id}") raise FileNotFoundError(
f"Kein Context-Sheet gefunden fuer chat_id={args.chat_id}")
return sheet return sheet
raise ValueError("Bitte --sheet oder --chat-id angeben.") raise ValueError("Bitte --sheet oder --chat-id angeben.")
def _last_user_inputs(history: list[dict[str, Any]], count: int) -> list[str]:
if count <= 0:
return []
user_msgs = [msg.get("content", "") for msg in history if msg.get("role") == "user"]
return [text for text in user_msgs[-count:] if text]
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description="Isolierter Decision-LLM Test mit Context Sheet.") parser = argparse.ArgumentParser(
description="Isolierter Decision-LLM Test mit Context Sheet.")
parser.add_argument("--sheet", help="Pfad zu einem Context Sheet JSON.") parser.add_argument("--sheet", help="Pfad zu einem Context Sheet JSON.")
parser.add_argument("--chat-id", help="Chat-ID zum Laden aus logs/context_sheets.")
parser.add_argument( parser.add_argument(
"--last-user-count", "--chat-id", help="Chat-ID zum Laden aus logs/context_sheets.")
type=int,
default=1,
help="Wie viele der letzten Nutzer-Eingaben ausgegeben werden sollen.",
)
args = parser.parse_args() args = parser.parse_args()
sheet = _resolve_sheet(args) sheet = _resolve_sheet(args)
history_text = context_store.get_history(sheet)
context_text = context_store.format_sheet(sheet) context_text = context_store.format_sheet(sheet)
decision = decision_LLM.needs_more_context(history_text, context_text) decision = decision_LLM.needs_more_context(context_text)
last_inputs = _last_user_inputs(sheet.get("history", []), args.last_user_count) history_turns = context_store.get_history_turns(sheet)
last_inputs = history_turns[-1]["content"] if history_turns else ""
print("LAST_USER_INPUTS:", json.dumps(last_inputs, ensure_ascii=True)) print("LAST_USER_INPUTS:", json.dumps(last_inputs, ensure_ascii=True))
print("OUTPUT:", json.dumps(decision, ensure_ascii=True)) print("OUTPUT:", json.dumps(decision, ensure_ascii=True))
......
import argparse import argparse
import json import json
import os
from typing import Any from typing import Any
from app.LLM_services import hint_LLM from app.LLM_services import hint_LLM
...@@ -18,27 +17,33 @@ def _resolve_sheet(args: argparse.Namespace) -> dict[str, Any]: ...@@ -18,27 +17,33 @@ def _resolve_sheet(args: argparse.Namespace) -> dict[str, Any]:
if args.chat_id: if args.chat_id:
sheet = context_store.load_sheet(args.chat_id) sheet = context_store.load_sheet(args.chat_id)
if sheet is None: if sheet is None:
raise FileNotFoundError(f"Kein Context-Sheet gefunden fuer chat_id={args.chat_id}") raise FileNotFoundError(
f"Kein Context-Sheet gefunden fuer chat_id={args.chat_id}")
return sheet return sheet
raise ValueError("Bitte --sheet oder --chat-id angeben.") raise ValueError("Bitte --sheet oder --chat-id angeben.")
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description="Isolierter Hint-LLM Test mit Context Sheet.") parser = argparse.ArgumentParser(
description="Isolierter Hint-LLM Test mit Context Sheet.")
parser.add_argument("--sheet", help="Pfad zu einem Context Sheet JSON.") parser.add_argument("--sheet", help="Pfad zu einem Context Sheet JSON.")
parser.add_argument("--chat-id", help="Chat-ID zum Laden aus logs/context_sheets.") parser.add_argument(
"--chat-id", help="Chat-ID zum Laden aus logs/context_sheets.")
args = parser.parse_args() args = parser.parse_args()
sheet = _resolve_sheet(args) sheet = _resolve_sheet(args)
hint_args = { history_turns = context_store.get_history_turns(sheet)
args = {
"query": history_turns[-1]["content"] if history_turns else "",
"task": context_store.get_task(sheet), "task": context_store.get_task(sheet),
"LLM_solution": context_store.last_LLM_solution(sheet), "LLM_solution": context_store.last_LLM_solution(sheet),
"math_solution": context_store.first_math_solution(sheet), "math_solution": context_store.first_math_solution(sheet),
"history": context_store.get_history(sheet), "history": history_turns,
"retrival": context_store.get_retrieval(sheet), "sources": "\n".join([source.to_string() for source in context_store.get_retrieval(sheet)]),
} }
reply = hint_LLM.generate_hint(**hint_args) reply = hint_LLM.generate_hint(**args)
print(reply) print(reply)
......
...@@ -21,7 +21,8 @@ def _iter_inputs(args: argparse.Namespace) -> Iterable[str]: ...@@ -21,7 +21,8 @@ def _iter_inputs(args: argparse.Namespace) -> Iterable[str]:
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description="Isolierter Math-Intent Test.") parser = argparse.ArgumentParser(
description="Isolierter Math-Intent Test.")
parser.add_argument( parser.add_argument(
"--input", "--input",
action="append", action="append",
......
import argparse import argparse
from app import config from app import config
from app.deterministic_services.embeddings import OpenAILikeEmbeddings from app.deterministic_services.embeddings import EmbeddingFactory
from app.deterministic_services import vector_store from app.deterministic_services import retrieval_store
def _get_embedder() -> OpenAILikeEmbeddings:
settings = config.get_embedding_settings()
return OpenAILikeEmbeddings(
base_url=settings.base_url,
api_key=settings.api_key,
model=settings.model,
target_dim=settings.target_dim,
)
def _normalize_sources(result: object) -> list[vector_store.Source]:
if isinstance(result, list):
return result
if isinstance(result, dict):
groups = {k: v for k, v in result.items() if isinstance(v, list)}
return vector_store._retrivla_to_sources(groups)
raise TypeError("Unexpected retrieval result type")
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description="Isolierter Vector-Store Retrieval Test.") parser = argparse.ArgumentParser(
description="Isolierter Vector-Store Retrieval Test.")
parser.add_argument("--query", required=True, help="Query text") parser.add_argument("--query", required=True, help="Query text")
parser.add_argument("--pg", default=None, help="Postgres URL (oder set POSTGRES_URL)") parser.add_argument("--pg", default=None,
help="Postgres URL (oder set POSTGRES_URL)")
parser.add_argument("--k", type=int, default=8) parser.add_argument("--k", type=int, default=8)
parser.add_argument("--expand", action="store_true") parser.add_argument("--expand", action="store_true")
parser.add_argument("--section-index", type=int, default=None) parser.add_argument("--section-index", type=int, default=None)
...@@ -38,9 +21,9 @@ def main() -> None: ...@@ -38,9 +21,9 @@ def main() -> None:
args = parser.parse_args() args = parser.parse_args()
pg_url = args.pg or config.get_postgres_url() pg_url = args.pg or config.get_postgres_url()
embedder = _get_embedder() embedder = EmbeddingFactory.create(config.get_embedding_settings())
result = vector_store.retrieve( sources = retrieval_store.retrieve(
pg_url=pg_url, pg_url=pg_url,
embedder=embedder, embedder=embedder,
query=args.query, query=args.query,
...@@ -52,14 +35,13 @@ def main() -> None: ...@@ -52,14 +35,13 @@ def main() -> None:
neighbor_expand=args.neighbor_expand, neighbor_expand=args.neighbor_expand,
) )
sources = _normalize_sources(result)
if not sources: if not sources:
print("Keine Quellen gefunden.") print("Keine Quellen gefunden.")
return return
print(f"Gefundene Quellen: {len(sources)}") print(f"Gefundene Quellen: {len(sources)}")
for source in sources: for source in sources:
print(source.to_string()) print(f"[{source.score:.4f}] {source.source_id.title} ({source.source_type})")
if __name__ == "__main__": if __name__ == "__main__":
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment