Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
81826b7
wip(rag): V1 of a rag working with an ollama llm working with the cur…
MatthiasvonRakowski May 7, 2026
87588fb
wip(rag): set the filter at None to be able to restrieve collections …
MatthiasvonRakowski May 7, 2026
fe78725
Merge remote-tracking branch 'origin/dev' into mvr/#38/setupRAG
MatthiasvonRakowski May 11, 2026
9b7dade
feat(id): add ids to make it work with the rag system + an ingestion …
MatthiasvonRakowski May 11, 2026
52b2cc5
clean(id): clean code
MatthiasvonRakowski May 11, 2026
209969e
feat(ingestion): ingestion done with the possibility of semantic and …
MatthiasvonRakowski May 12, 2026
b5f810f
wip(todo): Add some todos to not forget the work I have to do
MatthiasvonRakowski May 12, 2026
a0418a9
refacto(user_ids): user_id -> user_id
MatthiasvonRakowski May 15, 2026
529297a
wip(pr): add a module with ids and generate a rag class with module w…
MatthiasvonRakowski May 15, 2026
9cc6083
Merge remote-tracking branch 'origin/mvr/#14/ids_managment' into mvr/…
MatthiasvonRakowski May 15, 2026
04073a0
wip(docker): add a docker that launch with one commande. Only work wi…
MatthiasvonRakowski May 15, 2026
de72995
wip(history): history v1 : just short term history
MatthiasvonRakowski May 16, 2026
4080a33
wip(history): get ready for the save part
MatthiasvonRakowski May 16, 2026
657c9e0
feat(ingestion): add a better management for the personal data of the…
MatthiasvonRakowski May 18, 2026
eebb216
wip(config): move qdrant, ollama into a config file.
MatthiasvonRakowski May 18, 2026
52c03bb
wip(config): config file client updated
MatthiasvonRakowski May 18, 2026
7bdcc71
feat(huri): update config file
MatthiasvonRakowski May 18, 2026
452fc4d
fix(ingestion): update for the wrong branch now fixed
MatthiasvonRakowski May 18, 2026
2f691fa
merge: dev -> launch docker
MatthiasvonRakowski May 18, 2026
96d1da7
merge: dev -> launch docker
MatthiasvonRakowski May 18, 2026
eb4f98c
fix(config): huri.yaml fix
MatthiasvonRakowski May 18, 2026
450f21d
remove(main): remove unnecessary function
MatthiasvonRakowski May 18, 2026
2d8b547
wip(lint): cleaner code with a make lint
MatthiasvonRakowski May 22, 2026
44ffeb7
update(requierements): update requierements.txt
MatthiasvonRakowski May 22, 2026
edc539b
merge: launch docker -> conversation handler
MatthiasvonRakowski May 22, 2026
e6b7c6f
wip(history): get shorterm history
MatthiasvonRakowski May 22, 2026
cb5a6e9
delete(reasoning): Reasoning for later
MatthiasvonRakowski May 22, 2026
3dd2342
merge: dev -> conversation
MatthiasvonRakowski May 22, 2026
43e1a9d
merge: dev -> conversation
MatthiasvonRakowski May 25, 2026
2716e3a
wip(linter): lint fixing
MatthiasvonRakowski May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 4 additions & 15 deletions src/core/client.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import asyncio
import json
import os
from dataclasses import asdict
from typing import Dict, List, Optional, Type
from typing import Dict, List, Type

import websockets

from src.core.dataclasses.config import ClientConfig
from src.core.user_config import get_or_create_user_id

from .client_senders import ClientSender, get_senders

Expand All @@ -17,23 +17,13 @@ class Client:
def __init__(
self,
config: ClientConfig,
user_id_file: str = os.path.expanduser("~/.huri_user_id"),
user_id_file: str | None = None,
senders_dict: Dict[str, Type[ClientSender]] = get_senders(),
):
self.config = config
self.user_id_file = user_id_file
self.senders_dict = senders_dict

def _load_user_id(self) -> Optional[str]:
if os.path.exists(self.user_id_file):
with open(self.user_id_file) as f:
return f.read().strip()
return None

def _save_user_id(self, _user_id: str):
with open(self.user_id_file, "w") as f:
f.write(_user_id)

async def _receive_loop(self, ws: websockets.ClientConnection):
try:
while True:
Expand All @@ -48,7 +38,7 @@ async def run(self):
async with websockets.connect(self.config.huri_url) as ws:
print("Connected to server")

self.config.user_id = self._load_user_id()
self.config.user_id = get_or_create_user_id(self.user_id_file)

senders: List[ClientSender] = [
self.senders_dict[config.name](ws=ws, **config.args)
Expand All @@ -60,7 +50,6 @@ async def run(self):
init_msg = json.loads(await ws.recv())
if init_msg.get("type") == "session_init":
user_id = init_msg["user_id"]
self._save_user_id(user_id)
print(f"Session started with _user_id: {user_id}")

receive_task = asyncio.create_task(self._receive_loop(ws))
Expand Down
60 changes: 60 additions & 0 deletions src/core/user_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import os
import platform
import uuid
from pathlib import Path


def get_config_dir() -> Path:
"""Cross-platform config directory."""
system = platform.system()

if system == "Windows":
# TODO: To be tested -> also consider language-specific if needed
base = os.environ.get("APPDATA", os.path.expanduser("~/AppData/Roaming"))
elif system == "Darwin":
# TODO: To be tested -> also consider language-specific if needed
base = os.path.expanduser("~/Library/Application Support")
else:
base = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))

config_dir = Path(base) / "huri"
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir


def load_user_id(path: str | None = None) -> str | None:
"""Load existing _user_id, or return None if new user."""
id_file: Path

if path is None:
id_file = get_config_dir() / "_user_id"
else:
id_file = Path(path)
if id_file.exists():
uid = id_file.read_text().strip()
if uid:
return uid
return None


def save_user_id(_user_id: str, path: str | None = None):
id_file: Path

if path is None:
id_file = get_config_dir() / "_user_id"
else:
id_file = Path(path)

id_file.write_text(_user_id)
if platform.system() != "Windows":
id_file.chmod(0o600)


def get_or_create_user_id(path: str | None = None) -> str:
"""Load existing or generate new _user_id."""
uid = load_user_id(path)
if uid:
return uid
uid = str(uuid.uuid4())
save_user_id(uid, path)
return uid
Empty file added src/modules/rag/__init__.py
Empty file.
25 changes: 4 additions & 21 deletions src/modules/rag/ingestion.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import argparse
import os
import re
import sys
import uuid
Expand All @@ -17,10 +16,10 @@
PointStruct,
VectorParams,
)
from semantic_chunker import SemanticChunker
from sentence_transformers import SentenceTransformer

USER_ID_FILE = os.path.expanduser("~/.huri_user_id")
from src.core.user_config import get_or_create_user_id
from src.modules.rag.semantic_chunker import SemanticChunker


def _split_sentences(text: str) -> list[str]:
Expand Down Expand Up @@ -89,21 +88,6 @@ def extract_text_from_pdf(pdf_path: str) -> str:
sys.exit(1)


def get_user_id(provided_id: str | None = None) -> str:
if provided_id:
return provided_id
if os.path.exists(USER_ID_FILE):
with open(USER_ID_FILE) as f:
uid = f.read().strip()
if uid:
return uid
new_id = str(uuid.uuid4())
with open(USER_ID_FILE, "w") as f:
f.write(new_id)
print(f"Generated new user_id: {new_id}")
return new_id


def ensure_collection(client: QdrantClient, collection: str, vector_size: int):
collections = [c.name for c in client.get_collections().collections]
if collection not in collections:
Expand Down Expand Up @@ -145,7 +129,6 @@ def ingest_chunks(
)

if points:
# Upsert in batches of 100
batch_size = 100
for i in range(0, len(points), batch_size):
batch = points[i : i + batch_size]
Expand Down Expand Up @@ -403,7 +386,7 @@ def main():

args = parser.parse_args()

_user_id = get_user_id(args._user_id)
_user_id = get_or_create_user_id()
print(f"User: {_user_id}")

client = QdrantClient(url=args.qdrant_url)
Expand Down Expand Up @@ -440,7 +423,7 @@ def main():
# Ingest a text file
python ingestion.py text notes.txt story.md

# Specify a user ID (otherwise reads from ~/.huri_user_id)
# Specify a user ID (otherwise it will be auto-generated and saved)
python ingestion.py --user-id "abc-123" pdf report.pdf

# Use a different collection
Expand Down
54 changes: 40 additions & 14 deletions src/modules/rag/rag.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class RAGQuery:
_user_id: str
question: str
preferences: dict = field(default_factory=dict)
history: list[dict] | None = None
# preferences can include: language, tone,
# response_format, max_length, system_prompt, extra_instructions, etc.

Expand Down Expand Up @@ -110,36 +111,45 @@ def _search(
]
qdrant_filter = Filter(must=conditions)

doc_results = []
try:
results = qdrant.query_points(
doc_results = qdrant.query_points(
collection_name=collection,
query=query_vector,
query_filter=qdrant_filter,
limit=self.top_k,
score_threshold=self.score_threshold,
).points
except Exception:
results = []
pass

return [
{
"text": point.payload.get("text", ""),
"score": point.score,
"metadata": {k: v for k, v in point.payload.items() if k != "text"},
}
for point in results
for point in doc_results
]

def _build_prompt(
self,
question: str,
chunks: list[dict],
preferences: dict,
history=None,
) -> tuple[str, str]:

parts = [
"You are a robot speaking to a user. Answer based on the provided context.",
"If the context is insufficient, say so clearly.",
]
parts = []

if history:
lines = [f"{m['role']}: {m['content']}" for m in history]
parts.append("[Recent conversation]\n" + "\n".join(lines))

parts.append(
"You are a robot speaking to a user. Answer based on the provided context."
+ " If the context is insufficient, say so clearly.",
)
if preferences.get("language"):
parts.append(f"Always respond in {preferences['language']}.")
if preferences.get("tone"):
Expand All @@ -153,11 +163,7 @@ def _build_prompt(
system_prompt = " ".join(parts)

if not chunks:
user_prompt = (
"No relevant context was found.\n\n"
f"Question: {question}\n\n"
"Answer based on general knowledge."
)
user_prompt = f"Question: {question}\n\n"
else:
context_parts = []
for i, chunk in enumerate(chunks, 1):
Expand Down Expand Up @@ -259,7 +265,7 @@ async def process(self, query: RAGQuery) -> RAGResult:
print(f" - score: {c['score']:.2f} | {c['text'][:100]}...")

system_prompt, user_prompt = self._build_prompt(
query.question, chunks, query.preferences
query.question, chunks, query.preferences, query.history
)
print(f"[RAG] System prompt: {system_prompt[:200]}...")
answer = await self._llm_generate(system_prompt, user_prompt, query.preferences)
Expand Down Expand Up @@ -288,6 +294,7 @@ def __init__(
response_format="paragraph",
max_length=1024,
extra_instructions="",
max_history=10,
**kwargs,
):
super().__init__(_handle=_handle, _user_id=_user_id, **kwargs)
Expand All @@ -299,6 +306,8 @@ def __init__(
"max_length": max_length,
"extra_instructions": extra_instructions,
}
self.history: list[dict] = []
self.max_history = max_history

async def process(self, data: Sentence) -> Optional[RAGResult]:
"""
Expand All @@ -311,9 +320,26 @@ async def process(self, data: Sentence) -> Optional[RAGResult]:
_user_id=self._user_id if self._user_id else "anonymous",
question=question_text,
preferences=self.preferences,
history=(
self.history
if len(self.history) <= self.max_history
else self.history[-self.max_history :]
),
)

if self._handle is None:
print("[RAG] No handle available, returning None")
return None

result: RAGResult | None = None
if self._handle is not None:
result = await self._handle.process.remote(query)

self.history.append({"role": "user", "content": question_text})
self.history.append(
{"role": "assistant", "content": result.answer if result else None}
)

result: RAGResult = await self._handle.process.remote(query)
return result

def update_preferences(self, new_preferences: dict):
Expand Down