GitHub Actions commited on
Commit ·
fdfd35a
1
Parent(s): ff861b8
Sync from GitHub Actions
Browse files- Lawverse/agents/tools.py +6 -6
- Lawverse/memory/langchain_memory.py +32 -22
- Lawverse/retrieval/hybrid.py +1 -1
- Lawverse/retrieval/sparse.py +27 -30
- api/app.py +33 -47
Lawverse/agents/tools.py
CHANGED
|
@@ -7,13 +7,13 @@ from langchain_core.documents import Document
|
|
| 7 |
def retrieve_with_hybrid_tool(retriever, query: str, top_k: int = 5) -> List[Document]:
|
| 8 |
if retriever is None:
|
| 9 |
return []
|
| 10 |
-
|
|
|
|
| 11 |
docs = retriever.invoke(query)
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
docs = retriever._get_relevant_documents(query)
|
| 17 |
|
| 18 |
return list(docs or [])[:top_k]
|
| 19 |
|
|
|
|
| 7 |
def retrieve_with_hybrid_tool(retriever, query: str, top_k: int = 5) -> List[Document]:
|
| 8 |
if retriever is None:
|
| 9 |
return []
|
| 10 |
+
|
| 11 |
+
if hasattr(retriever, "invoke"):
|
| 12 |
docs = retriever.invoke(query)
|
| 13 |
+
elif hasattr(retriever, "get_relevant_documents"):
|
| 14 |
+
docs = retriever.get_relevant_documents(query)
|
| 15 |
+
else:
|
| 16 |
+
docs = retriever._get_relevant_documents(query)
|
|
|
|
| 17 |
|
| 18 |
return list(docs or [])[:top_k]
|
| 19 |
|
Lawverse/memory/langchain_memory.py
CHANGED
|
@@ -1,28 +1,42 @@
|
|
| 1 |
import sys
|
| 2 |
from flask import session, has_request_context
|
| 3 |
from datetime import datetime
|
| 4 |
-
from
|
| 5 |
from Lawverse.logger import logging
|
| 6 |
from Lawverse.exception import ExceptionHandle
|
| 7 |
from Lawverse.storage.factory import get_chat_store
|
| 8 |
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
class ChatMemory:
|
| 11 |
def __init__(self, chat_id=None, user_id=None):
|
| 12 |
try:
|
| 13 |
self.chat_id = chat_id or self._create_new_chat_id()
|
| 14 |
self.user_id = str(user_id or self._get_current_user_id())
|
| 15 |
self.store = get_chat_store()
|
| 16 |
-
|
| 17 |
-
self.memory = ConversationBufferMemory(
|
| 18 |
-
memory_key="chat_history",
|
| 19 |
-
return_messages=True,
|
| 20 |
-
output_key="answer",
|
| 21 |
-
)
|
| 22 |
-
|
| 23 |
self._load_memory()
|
| 24 |
logging.info(f"ChatMemory initialized for user_id={self.user_id}, chat_id={self.chat_id}")
|
| 25 |
-
|
| 26 |
except Exception as e:
|
| 27 |
raise ExceptionHandle(e, sys) from e
|
| 28 |
|
|
@@ -37,22 +51,19 @@ class ChatMemory:
|
|
| 37 |
def _load_memory(self):
|
| 38 |
try:
|
| 39 |
data = self.store.load_chat(self.user_id, self.chat_id)
|
| 40 |
-
|
| 41 |
if not data:
|
| 42 |
logging.info(f"No existing memory found for chat_id: {self.chat_id}")
|
| 43 |
return
|
| 44 |
|
| 45 |
-
for msg in data.get("history", []):
|
| 46 |
user_msg = msg.get("user", "")
|
| 47 |
ai_msg = msg.get("ai", "")
|
| 48 |
-
|
| 49 |
if user_msg:
|
| 50 |
self.memory.chat_memory.add_user_message(user_msg)
|
| 51 |
if ai_msg:
|
| 52 |
self.memory.chat_memory.add_ai_message(ai_msg)
|
| 53 |
|
| 54 |
logging.info(f"Memory loaded successfully for chat_id: {self.chat_id}")
|
| 55 |
-
|
| 56 |
except Exception as e:
|
| 57 |
raise ExceptionHandle(e, sys) from e
|
| 58 |
|
|
@@ -65,19 +76,16 @@ class ChatMemory:
|
|
| 65 |
def _history_as_pairs(self):
|
| 66 |
messages = self.memory.chat_memory.messages
|
| 67 |
history = []
|
| 68 |
-
|
| 69 |
i = 0
|
| 70 |
while i < len(messages):
|
| 71 |
user_msg = messages[i].content if i < len(messages) else ""
|
| 72 |
ai_msg = messages[i + 1].content if i + 1 < len(messages) else ""
|
| 73 |
-
|
| 74 |
if user_msg or ai_msg:
|
| 75 |
history.append({"user": user_msg, "ai": ai_msg})
|
| 76 |
i += 2
|
| 77 |
-
|
| 78 |
return history
|
| 79 |
|
| 80 |
-
def save_memory(self):
|
| 81 |
try:
|
| 82 |
self.store.save_chat(
|
| 83 |
user_id=self.user_id,
|
|
@@ -85,14 +93,17 @@ class ChatMemory:
|
|
| 85 |
title=self._get_title(),
|
| 86 |
history=self._history_as_pairs(),
|
| 87 |
)
|
| 88 |
-
|
| 89 |
logging.info(f"Memory saved successfully for chat_id: {self.chat_id}")
|
|
|
|
| 90 |
except Exception as e:
|
| 91 |
-
|
|
|
|
| 92 |
|
| 93 |
def _get_title(self):
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
| 96 |
return "New legal query"
|
| 97 |
|
| 98 |
def clear_memory(self):
|
|
@@ -100,6 +111,5 @@ class ChatMemory:
|
|
| 100 |
self.memory.clear()
|
| 101 |
self.store.delete_chat(self.user_id, self.chat_id)
|
| 102 |
logging.info(f"Memory deleted for chat_id: {self.chat_id}")
|
| 103 |
-
|
| 104 |
except Exception as e:
|
| 105 |
raise ExceptionHandle(e, sys) from e
|
|
|
|
| 1 |
import sys
|
| 2 |
from flask import session, has_request_context
|
| 3 |
from datetime import datetime
|
| 4 |
+
from langchain_core.messages import HumanMessage, AIMessage
|
| 5 |
from Lawverse.logger import logging
|
| 6 |
from Lawverse.exception import ExceptionHandle
|
| 7 |
from Lawverse.storage.factory import get_chat_store
|
| 8 |
|
| 9 |
|
| 10 |
+
class _SimpleChatHistory:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.messages = []
|
| 13 |
+
|
| 14 |
+
def add_user_message(self, content: str):
|
| 15 |
+
self.messages.append(HumanMessage(content=content or ""))
|
| 16 |
+
|
| 17 |
+
def add_ai_message(self, content: str):
|
| 18 |
+
self.messages.append(AIMessage(content=content or ""))
|
| 19 |
+
|
| 20 |
+
def clear(self):
|
| 21 |
+
self.messages.clear()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class _SimpleMemory:
|
| 25 |
+
def __init__(self):
|
| 26 |
+
self.chat_memory = _SimpleChatHistory()
|
| 27 |
+
|
| 28 |
+
def clear(self):
|
| 29 |
+
self.chat_memory.clear()
|
| 30 |
+
|
| 31 |
class ChatMemory:
|
| 32 |
def __init__(self, chat_id=None, user_id=None):
|
| 33 |
try:
|
| 34 |
self.chat_id = chat_id or self._create_new_chat_id()
|
| 35 |
self.user_id = str(user_id or self._get_current_user_id())
|
| 36 |
self.store = get_chat_store()
|
| 37 |
+
self.memory = _SimpleMemory()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
self._load_memory()
|
| 39 |
logging.info(f"ChatMemory initialized for user_id={self.user_id}, chat_id={self.chat_id}")
|
|
|
|
| 40 |
except Exception as e:
|
| 41 |
raise ExceptionHandle(e, sys) from e
|
| 42 |
|
|
|
|
| 51 |
def _load_memory(self):
|
| 52 |
try:
|
| 53 |
data = self.store.load_chat(self.user_id, self.chat_id)
|
|
|
|
| 54 |
if not data:
|
| 55 |
logging.info(f"No existing memory found for chat_id: {self.chat_id}")
|
| 56 |
return
|
| 57 |
|
| 58 |
+
for msg in data.get("history", []) or []:
|
| 59 |
user_msg = msg.get("user", "")
|
| 60 |
ai_msg = msg.get("ai", "")
|
|
|
|
| 61 |
if user_msg:
|
| 62 |
self.memory.chat_memory.add_user_message(user_msg)
|
| 63 |
if ai_msg:
|
| 64 |
self.memory.chat_memory.add_ai_message(ai_msg)
|
| 65 |
|
| 66 |
logging.info(f"Memory loaded successfully for chat_id: {self.chat_id}")
|
|
|
|
| 67 |
except Exception as e:
|
| 68 |
raise ExceptionHandle(e, sys) from e
|
| 69 |
|
|
|
|
| 76 |
def _history_as_pairs(self):
|
| 77 |
messages = self.memory.chat_memory.messages
|
| 78 |
history = []
|
|
|
|
| 79 |
i = 0
|
| 80 |
while i < len(messages):
|
| 81 |
user_msg = messages[i].content if i < len(messages) else ""
|
| 82 |
ai_msg = messages[i + 1].content if i + 1 < len(messages) else ""
|
|
|
|
| 83 |
if user_msg or ai_msg:
|
| 84 |
history.append({"user": user_msg, "ai": ai_msg})
|
| 85 |
i += 2
|
|
|
|
| 86 |
return history
|
| 87 |
|
| 88 |
+
def save_memory(self) -> bool:
|
| 89 |
try:
|
| 90 |
self.store.save_chat(
|
| 91 |
user_id=self.user_id,
|
|
|
|
| 93 |
title=self._get_title(),
|
| 94 |
history=self._history_as_pairs(),
|
| 95 |
)
|
|
|
|
| 96 |
logging.info(f"Memory saved successfully for chat_id: {self.chat_id}")
|
| 97 |
+
return True
|
| 98 |
except Exception as e:
|
| 99 |
+
logging.error(f"Memory save failed for chat_id={self.chat_id}: {e}")
|
| 100 |
+
return False
|
| 101 |
|
| 102 |
def _get_title(self):
|
| 103 |
+
for msg in self.memory.chat_memory.messages:
|
| 104 |
+
content = getattr(msg, "content", "") or ""
|
| 105 |
+
if content.strip():
|
| 106 |
+
return content.strip()[:40]
|
| 107 |
return "New legal query"
|
| 108 |
|
| 109 |
def clear_memory(self):
|
|
|
|
| 111 |
self.memory.clear()
|
| 112 |
self.store.delete_chat(self.user_id, self.chat_id)
|
| 113 |
logging.info(f"Memory deleted for chat_id: {self.chat_id}")
|
|
|
|
| 114 |
except Exception as e:
|
| 115 |
raise ExceptionHandle(e, sys) from e
|
Lawverse/retrieval/hybrid.py
CHANGED
|
@@ -37,7 +37,7 @@ def hybrid_retrieve(
|
|
| 37 |
):
|
| 38 |
try:
|
| 39 |
dense_results = faiss_db.similarity_search(query, k=initial_top_k)
|
| 40 |
-
sparse_results = bm25_retrieve(bm25,
|
| 41 |
|
| 42 |
doc_map: Dict[str, Document] = {}
|
| 43 |
rrf_scores = defaultdict(float)
|
|
|
|
| 37 |
):
|
| 38 |
try:
|
| 39 |
dense_results = faiss_db.similarity_search(query, k=initial_top_k)
|
| 40 |
+
sparse_results = bm25_retrieve(bm25, query, chunks, top_k=initial_top_k)
|
| 41 |
|
| 42 |
doc_map: Dict[str, Document] = {}
|
| 43 |
rrf_scores = defaultdict(float)
|
Lawverse/retrieval/sparse.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
import re
|
| 3 |
import sys
|
| 4 |
-
from typing import List, Tuple, Union
|
| 5 |
from rank_bm25 import BM25Okapi
|
| 6 |
from langchain_core.documents import Document
|
| 7 |
from Lawverse.logger import logging
|
|
@@ -18,41 +18,34 @@ STOP_WORDS = {
|
|
| 18 |
}
|
| 19 |
|
| 20 |
|
| 21 |
-
def bm25_tokenizer(text: Union[str,
|
| 22 |
if text is None:
|
| 23 |
return []
|
| 24 |
|
| 25 |
-
if isinstance(text, (list, tuple)):
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
for token in text
|
| 29 |
-
if str(token).strip() and str(token).lower().strip() not in STOP_WORDS
|
| 30 |
-
]
|
| 31 |
|
| 32 |
text = str(text).lower()
|
| 33 |
english_tokens = re.findall(r"[a-zA-Z0-9]+", text)
|
| 34 |
bangla_tokens = re.findall(r"[\u0980-\u09FF]+", text)
|
| 35 |
tokens = english_tokens + bangla_tokens
|
| 36 |
-
|
| 37 |
-
return [
|
| 38 |
-
token
|
| 39 |
-
for token in tokens
|
| 40 |
-
if len(token) > 1 and token not in STOP_WORDS
|
| 41 |
-
]
|
| 42 |
|
| 43 |
|
| 44 |
def build_sparse_index(chunks: List[Document], k1: float = 1.5, b: float = 0.8) -> BM25Okapi:
|
| 45 |
try:
|
| 46 |
logging.info("Building sparse BM25 index...")
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
bm25 = BM25Okapi(tokenized_corpus, k1=k1, b=b)
|
| 53 |
-
logging.info(f"BM25 sparse index successfully built with {len(
|
| 54 |
return bm25
|
| 55 |
-
|
| 56 |
except Exception as e:
|
| 57 |
logging.error(f"Failed to build BM25 sparse index. Error: {e}")
|
| 58 |
raise ExceptionHandle(e, sys)
|
|
@@ -65,24 +58,28 @@ def bm25_retrieve(
|
|
| 65 |
top_k: int = 10,
|
| 66 |
) -> List[Tuple[Document, float]]:
|
| 67 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
query_tokens = bm25_tokenizer(query)
|
| 69 |
scores = bm25.get_scores(query_tokens)
|
|
|
|
| 70 |
|
| 71 |
-
|
| 72 |
-
enumerate(scores),
|
| 73 |
-
key=lambda item: item[1],
|
| 74 |
-
reverse=True,
|
| 75 |
-
)[:top_k]
|
| 76 |
-
|
| 77 |
-
results = []
|
| 78 |
for idx, score in ranked:
|
|
|
|
|
|
|
| 79 |
doc = chunks[idx]
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
| 81 |
doc.metadata["bm25_score"] = float(score)
|
| 82 |
results.append((doc, float(score)))
|
| 83 |
|
| 84 |
return results
|
| 85 |
-
|
| 86 |
except Exception as e:
|
| 87 |
logging.error(f"BM25 retrieval failed. Error: {e}")
|
| 88 |
raise ExceptionHandle(e, sys)
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
import re
|
| 3 |
import sys
|
| 4 |
+
from typing import List, Tuple, Union, Iterable
|
| 5 |
from rank_bm25 import BM25Okapi
|
| 6 |
from langchain_core.documents import Document
|
| 7 |
from Lawverse.logger import logging
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
|
| 21 |
+
def bm25_tokenizer(text: Union[str, Iterable[str], None]) -> List[str]:
|
| 22 |
if text is None:
|
| 23 |
return []
|
| 24 |
|
| 25 |
+
if isinstance(text, (list, tuple, set)):
|
| 26 |
+
tokens = [str(token).lower().strip() for token in text]
|
| 27 |
+
return [token for token in tokens if len(token) > 1 and token not in STOP_WORDS]
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
text = str(text).lower()
|
| 30 |
english_tokens = re.findall(r"[a-zA-Z0-9]+", text)
|
| 31 |
bangla_tokens = re.findall(r"[\u0980-\u09FF]+", text)
|
| 32 |
tokens = english_tokens + bangla_tokens
|
| 33 |
+
return [token for token in tokens if len(token) > 1 and token not in STOP_WORDS]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
def build_sparse_index(chunks: List[Document], k1: float = 1.5, b: float = 0.8) -> BM25Okapi:
|
| 37 |
try:
|
| 38 |
logging.info("Building sparse BM25 index...")
|
| 39 |
+
safe_chunks = [chunk for chunk in chunks if isinstance(chunk, Document)]
|
| 40 |
+
if len(safe_chunks) != len(chunks):
|
| 41 |
+
logging.warning(
|
| 42 |
+
"BM25 received non-Document chunks; ignored %s invalid items.",
|
| 43 |
+
len(chunks) - len(safe_chunks),
|
| 44 |
+
)
|
| 45 |
+
tokenized_corpus = [bm25_tokenizer(chunk.page_content) for chunk in safe_chunks]
|
| 46 |
bm25 = BM25Okapi(tokenized_corpus, k1=k1, b=b)
|
| 47 |
+
logging.info(f"BM25 sparse index successfully built with {len(safe_chunks)} chunks.")
|
| 48 |
return bm25
|
|
|
|
| 49 |
except Exception as e:
|
| 50 |
logging.error(f"Failed to build BM25 sparse index. Error: {e}")
|
| 51 |
raise ExceptionHandle(e, sys)
|
|
|
|
| 58 |
top_k: int = 10,
|
| 59 |
) -> List[Tuple[Document, float]]:
|
| 60 |
try:
|
| 61 |
+
if not isinstance(chunks, list):
|
| 62 |
+
raise TypeError(
|
| 63 |
+
f"chunks must be a list[Document], got {type(chunks).__name__}. "
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
query_tokens = bm25_tokenizer(query)
|
| 67 |
scores = bm25.get_scores(query_tokens)
|
| 68 |
+
ranked = sorted(enumerate(scores), key=lambda item: item[1], reverse=True)[:top_k]
|
| 69 |
|
| 70 |
+
results: List[Tuple[Document, float]] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
for idx, score in ranked:
|
| 72 |
+
if idx >= len(chunks):
|
| 73 |
+
continue
|
| 74 |
doc = chunks[idx]
|
| 75 |
+
if not isinstance(doc, Document):
|
| 76 |
+
logging.warning("Skipping BM25 result with invalid chunk type: %s", type(doc).__name__)
|
| 77 |
+
continue
|
| 78 |
+
doc = Document(page_content=doc.page_content, metadata=dict(doc.metadata or {}))
|
| 79 |
doc.metadata["bm25_score"] = float(score)
|
| 80 |
results.append((doc, float(score)))
|
| 81 |
|
| 82 |
return results
|
|
|
|
| 83 |
except Exception as e:
|
| 84 |
logging.error(f"BM25 retrieval failed. Error: {e}")
|
| 85 |
raise ExceptionHandle(e, sys)
|
api/app.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
from flask import Flask, render_template, request, jsonify, session, stream_with_context, Response
|
| 2 |
from dotenv import load_dotenv
|
| 3 |
-
import secrets
|
| 4 |
-
import os
|
| 5 |
from threading import Lock
|
|
|
|
|
|
|
| 6 |
import logging as py_logging
|
| 7 |
from Lawverse.pipeline.rag_pipeline import rag_components
|
| 8 |
from Lawverse.pipeline.llm_loader import llm
|
|
@@ -13,15 +13,12 @@ from Lawverse.agents.graph import create_agentic_chain
|
|
| 13 |
from Lawverse.storage.factory import get_chat_store
|
| 14 |
from api.auth import auth_bp, login_required
|
| 15 |
for logger_name in [
|
| 16 |
-
"httpcore",
|
| 17 |
-
"
|
| 18 |
-
"hpack",
|
| 19 |
-
"filelock",
|
| 20 |
-
"sentence_transformers",
|
| 21 |
-
"urllib3",
|
| 22 |
]:
|
| 23 |
py_logging.getLogger(logger_name).setLevel(py_logging.WARNING)
|
| 24 |
-
|
|
|
|
| 25 |
load_dotenv()
|
| 26 |
app = Flask(__name__, template_folder="../templates")
|
| 27 |
app.secret_key = os.getenv("SECRET_KEY") or secrets.token_hex(32)
|
|
@@ -31,11 +28,12 @@ app.register_blueprint(monitor_bp)
|
|
| 31 |
|
| 32 |
BASE_COMPONENTS = None
|
| 33 |
BASE_COMPONENTS_LOCK = Lock()
|
|
|
|
|
|
|
| 34 |
active_chains = {}
|
| 35 |
|
| 36 |
def get_base_components():
|
| 37 |
global BASE_COMPONENTS
|
| 38 |
-
|
| 39 |
if BASE_COMPONENTS is not None:
|
| 40 |
return BASE_COMPONENTS
|
| 41 |
|
|
@@ -44,19 +42,28 @@ def get_base_components():
|
|
| 44 |
logging.info("Loading Lawverse RAG base components...")
|
| 45 |
BASE_COMPONENTS = rag_components()
|
| 46 |
logging.info("Lawverse RAG base components loaded successfully.")
|
| 47 |
-
|
| 48 |
return BASE_COMPONENTS
|
| 49 |
|
| 50 |
|
| 51 |
-
def
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
active_chains[memory_manager.chat_id] = (chain, memory_manager)
|
| 57 |
session["chat_id"] = memory_manager.chat_id
|
| 58 |
-
|
| 59 |
-
|
| 60 |
return chain, memory_manager
|
| 61 |
|
| 62 |
@app.route("/", methods=["GET"])
|
|
@@ -69,18 +76,13 @@ def chat():
|
|
| 69 |
chat_id = session.get("chat_id")
|
| 70 |
if not chat_id or chat_id not in active_chains:
|
| 71 |
create_agent_session()
|
| 72 |
-
|
| 73 |
return render_template("chat.html")
|
| 74 |
|
| 75 |
@app.route("/new_chat", methods=["POST"])
|
| 76 |
@login_required
|
| 77 |
def new_chat():
|
| 78 |
_, memory_manager = create_agent_session()
|
| 79 |
-
|
| 80 |
-
return jsonify({
|
| 81 |
-
"chat_id": memory_manager.chat_id,
|
| 82 |
-
"title": memory_manager._get_title()
|
| 83 |
-
})
|
| 84 |
|
| 85 |
|
| 86 |
@app.route("/response", methods=["POST"])
|
|
@@ -95,12 +97,12 @@ def rag_response():
|
|
| 95 |
qa, memory_manager = active_chains[chat_id]
|
| 96 |
data = request.get_json(silent=True) or {}
|
| 97 |
query = data.get("message", "").strip()
|
| 98 |
-
|
| 99 |
if not query:
|
| 100 |
return jsonify({"error": "Empty message"}), 400
|
| 101 |
|
| 102 |
def generate():
|
| 103 |
answer_parts = []
|
|
|
|
| 104 |
try:
|
| 105 |
for chunk in qa.stream({
|
| 106 |
"input": query,
|
|
@@ -109,15 +111,15 @@ def rag_response():
|
|
| 109 |
text = chunk if isinstance(chunk, str) else str(chunk)
|
| 110 |
answer_parts.append(text)
|
| 111 |
yield text
|
|
|
|
|
|
|
|
|
|
| 112 |
|
|
|
|
| 113 |
full_answer = "".join(answer_parts).strip()
|
| 114 |
memory_manager.append_exchange(query, full_answer)
|
| 115 |
memory_manager.save_memory()
|
| 116 |
|
| 117 |
-
except Exception as e:
|
| 118 |
-
logging.error(f"Error during stream generation: {e}")
|
| 119 |
-
yield "**Error:** An error occurred while processing your request."
|
| 120 |
-
|
| 121 |
return Response(stream_with_context(generate()), mimetype="text/plain")
|
| 122 |
|
| 123 |
except Exception as e:
|
|
@@ -132,7 +134,6 @@ def get_chats():
|
|
| 132 |
user_id = str(session.get("user_id"))
|
| 133 |
chats = get_chat_store().list_chats(user_id)
|
| 134 |
return jsonify(chats), 200
|
| 135 |
-
|
| 136 |
except Exception as e:
|
| 137 |
logging.error(f"Failed to list chats from cloud store: {e}")
|
| 138 |
return jsonify([]), 200
|
|
@@ -147,27 +148,20 @@ def load_chat(chat_id):
|
|
| 147 |
if not data:
|
| 148 |
return jsonify({"error": "Chat not found"}), 404
|
| 149 |
|
| 150 |
-
_, memory_manager = create_agent_session(chat_id=chat_id)
|
| 151 |
-
|
| 152 |
messages_list = memory_manager.memory.chat_memory.messages
|
| 153 |
messages = []
|
| 154 |
-
|
| 155 |
for i in range(0, len(messages_list), 2):
|
| 156 |
user_msg = messages_list[i].content if i < len(messages_list) else None
|
| 157 |
ai_msg = messages_list[i + 1].content if i + 1 < len(messages_list) else ""
|
| 158 |
-
|
| 159 |
if user_msg:
|
| 160 |
-
messages.append({
|
| 161 |
-
"user": user_msg,
|
| 162 |
-
"ai": ai_msg
|
| 163 |
-
})
|
| 164 |
|
| 165 |
return jsonify({
|
| 166 |
"chat_id": chat_id,
|
| 167 |
"title": memory_manager._get_title(),
|
| 168 |
"messages": messages,
|
| 169 |
}), 200
|
| 170 |
-
|
| 171 |
except Exception as e:
|
| 172 |
logging.error(f"Failed to load chat from cloud store: {e}")
|
| 173 |
return jsonify({"error": "Internal Server Error"}), 500
|
|
@@ -178,21 +172,13 @@ def load_chat(chat_id):
|
|
| 178 |
def delete_chat(chat_id):
|
| 179 |
try:
|
| 180 |
user_id = str(session.get("user_id"))
|
| 181 |
-
|
| 182 |
deleted = get_chat_store().delete_chat(user_id, chat_id)
|
| 183 |
-
|
| 184 |
was_active = chat_id in active_chains
|
| 185 |
if was_active:
|
| 186 |
del active_chains[chat_id]
|
| 187 |
-
|
| 188 |
if session.get("chat_id") == chat_id:
|
| 189 |
session.pop("chat_id", None)
|
| 190 |
-
|
| 191 |
-
return jsonify({
|
| 192 |
-
"success": deleted,
|
| 193 |
-
"was_active": was_active
|
| 194 |
-
}), 200
|
| 195 |
-
|
| 196 |
except Exception as e:
|
| 197 |
logging.error(f"Error deleting cloud chat {chat_id}: {e}")
|
| 198 |
return jsonify({"error": "Internal Server Error"}), 500
|
|
|
|
| 1 |
from flask import Flask, render_template, request, jsonify, session, stream_with_context, Response
|
| 2 |
from dotenv import load_dotenv
|
|
|
|
|
|
|
| 3 |
from threading import Lock
|
| 4 |
+
import os
|
| 5 |
+
import secrets
|
| 6 |
import logging as py_logging
|
| 7 |
from Lawverse.pipeline.rag_pipeline import rag_components
|
| 8 |
from Lawverse.pipeline.llm_loader import llm
|
|
|
|
| 13 |
from Lawverse.storage.factory import get_chat_store
|
| 14 |
from api.auth import auth_bp, login_required
|
| 15 |
for logger_name in [
|
| 16 |
+
"httpcore", "httpx", "hpack", "filelock", "sentence_transformers",
|
| 17 |
+
"urllib3", "openai", "openai._base_client", "faiss", "datasets",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
]:
|
| 19 |
py_logging.getLogger(logger_name).setLevel(py_logging.WARNING)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
load_dotenv()
|
| 23 |
app = Flask(__name__, template_folder="../templates")
|
| 24 |
app.secret_key = os.getenv("SECRET_KEY") or secrets.token_hex(32)
|
|
|
|
| 28 |
|
| 29 |
BASE_COMPONENTS = None
|
| 30 |
BASE_COMPONENTS_LOCK = Lock()
|
| 31 |
+
AGENT_CHAIN = None
|
| 32 |
+
AGENT_CHAIN_LOCK = Lock()
|
| 33 |
active_chains = {}
|
| 34 |
|
| 35 |
def get_base_components():
|
| 36 |
global BASE_COMPONENTS
|
|
|
|
| 37 |
if BASE_COMPONENTS is not None:
|
| 38 |
return BASE_COMPONENTS
|
| 39 |
|
|
|
|
| 42 |
logging.info("Loading Lawverse RAG base components...")
|
| 43 |
BASE_COMPONENTS = rag_components()
|
| 44 |
logging.info("Lawverse RAG base components loaded successfully.")
|
|
|
|
| 45 |
return BASE_COMPONENTS
|
| 46 |
|
| 47 |
|
| 48 |
+
def get_agent_chain():
|
| 49 |
+
global AGENT_CHAIN
|
| 50 |
+
if AGENT_CHAIN is not None:
|
| 51 |
+
return AGENT_CHAIN
|
| 52 |
|
| 53 |
+
with AGENT_CHAIN_LOCK:
|
| 54 |
+
if AGENT_CHAIN is None:
|
| 55 |
+
components = get_base_components()
|
| 56 |
+
AGENT_CHAIN = create_agentic_chain(components, llm)
|
| 57 |
+
return AGENT_CHAIN
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def create_agent_session(chat_id=None, save_on_create=True):
|
| 61 |
+
chain = get_agent_chain()
|
| 62 |
+
memory_manager = ChatMemory(chat_id=chat_id)
|
| 63 |
active_chains[memory_manager.chat_id] = (chain, memory_manager)
|
| 64 |
session["chat_id"] = memory_manager.chat_id
|
| 65 |
+
if save_on_create:
|
| 66 |
+
memory_manager.save_memory()
|
| 67 |
return chain, memory_manager
|
| 68 |
|
| 69 |
@app.route("/", methods=["GET"])
|
|
|
|
| 76 |
chat_id = session.get("chat_id")
|
| 77 |
if not chat_id or chat_id not in active_chains:
|
| 78 |
create_agent_session()
|
|
|
|
| 79 |
return render_template("chat.html")
|
| 80 |
|
| 81 |
@app.route("/new_chat", methods=["POST"])
|
| 82 |
@login_required
|
| 83 |
def new_chat():
|
| 84 |
_, memory_manager = create_agent_session()
|
| 85 |
+
return jsonify({"chat_id": memory_manager.chat_id, "title": memory_manager._get_title()})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
|
| 88 |
@app.route("/response", methods=["POST"])
|
|
|
|
| 97 |
qa, memory_manager = active_chains[chat_id]
|
| 98 |
data = request.get_json(silent=True) or {}
|
| 99 |
query = data.get("message", "").strip()
|
|
|
|
| 100 |
if not query:
|
| 101 |
return jsonify({"error": "Empty message"}), 400
|
| 102 |
|
| 103 |
def generate():
|
| 104 |
answer_parts = []
|
| 105 |
+
generation_failed = False
|
| 106 |
try:
|
| 107 |
for chunk in qa.stream({
|
| 108 |
"input": query,
|
|
|
|
| 111 |
text = chunk if isinstance(chunk, str) else str(chunk)
|
| 112 |
answer_parts.append(text)
|
| 113 |
yield text
|
| 114 |
+
except Exception as e:
|
| 115 |
+
generation_failed = True
|
| 116 |
+
logging.error(f"Error during answer generation: {e}")
|
| 117 |
|
| 118 |
+
if not generation_failed:
|
| 119 |
full_answer = "".join(answer_parts).strip()
|
| 120 |
memory_manager.append_exchange(query, full_answer)
|
| 121 |
memory_manager.save_memory()
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
return Response(stream_with_context(generate()), mimetype="text/plain")
|
| 124 |
|
| 125 |
except Exception as e:
|
|
|
|
| 134 |
user_id = str(session.get("user_id"))
|
| 135 |
chats = get_chat_store().list_chats(user_id)
|
| 136 |
return jsonify(chats), 200
|
|
|
|
| 137 |
except Exception as e:
|
| 138 |
logging.error(f"Failed to list chats from cloud store: {e}")
|
| 139 |
return jsonify([]), 200
|
|
|
|
| 148 |
if not data:
|
| 149 |
return jsonify({"error": "Chat not found"}), 404
|
| 150 |
|
| 151 |
+
_, memory_manager = create_agent_session(chat_id=chat_id, save_on_create=False)
|
|
|
|
| 152 |
messages_list = memory_manager.memory.chat_memory.messages
|
| 153 |
messages = []
|
|
|
|
| 154 |
for i in range(0, len(messages_list), 2):
|
| 155 |
user_msg = messages_list[i].content if i < len(messages_list) else None
|
| 156 |
ai_msg = messages_list[i + 1].content if i + 1 < len(messages_list) else ""
|
|
|
|
| 157 |
if user_msg:
|
| 158 |
+
messages.append({"user": user_msg, "ai": ai_msg})
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
return jsonify({
|
| 161 |
"chat_id": chat_id,
|
| 162 |
"title": memory_manager._get_title(),
|
| 163 |
"messages": messages,
|
| 164 |
}), 200
|
|
|
|
| 165 |
except Exception as e:
|
| 166 |
logging.error(f"Failed to load chat from cloud store: {e}")
|
| 167 |
return jsonify({"error": "Internal Server Error"}), 500
|
|
|
|
| 172 |
def delete_chat(chat_id):
|
| 173 |
try:
|
| 174 |
user_id = str(session.get("user_id"))
|
|
|
|
| 175 |
deleted = get_chat_store().delete_chat(user_id, chat_id)
|
|
|
|
| 176 |
was_active = chat_id in active_chains
|
| 177 |
if was_active:
|
| 178 |
del active_chains[chat_id]
|
|
|
|
| 179 |
if session.get("chat_id") == chat_id:
|
| 180 |
session.pop("chat_id", None)
|
| 181 |
+
return jsonify({"success": deleted, "was_active": was_active}), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
except Exception as e:
|
| 183 |
logging.error(f"Error deleting cloud chat {chat_id}: {e}")
|
| 184 |
return jsonify({"error": "Internal Server Error"}), 500
|