GitHub Actions commited on
Commit
cf2541c
·
1 Parent(s): e174cd6

Sync from GitHub Actions

Browse files
Dockerfile CHANGED
@@ -21,6 +21,6 @@ ENV GDOWN_CACHE=/tmp/lawverse_data/gdown_cache
21
 
22
  COPY . .
23
 
24
- EXPOSE 10000
25
 
26
  CMD ["python", "-m", "api.app"]
 
21
 
22
  COPY . .
23
 
24
+ EXPOSE 7860
25
 
26
  CMD ["python", "-m", "api.app"]
Lawverse/agents/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from Lawverse.agents.graph import AgenticLawverseChain, create_agentic_chain
2
+
3
+ __all__ = ["AgenticLawverseChain", "create_agentic_chain"]
Lawverse/agents/graph.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from typing import Any, Dict, Iterable, Optional
3
+ from langgraph.graph import END, StateGraph
4
+
5
+ from Lawverse.agents.state import AgentState
6
+ from Lawverse.agents.nodes import (
7
+ answer_generator_node,
8
+ citation_verifier_node,
9
+ evidence_grader_node,
10
+ hybrid_retriever_node,
11
+ intent_classifier_node,
12
+ query_rewriter_node,
13
+ retrieval_planner_node,
14
+ )
15
+ from Lawverse.logger import logging
16
+
17
+
18
+ class AgenticLawverseChain:
19
+ def __init__(self, retriever, llm, use_langgraph: bool = True):
20
+ self.retriever = retriever
21
+ self.llm = llm
22
+ self.use_langgraph = use_langgraph
23
+ self._compiled_graph = None
24
+ if use_langgraph:
25
+ self._compiled_graph = self._try_build_langgraph()
26
+
27
+ def _try_build_langgraph(self):
28
+ try:
29
+ graph = StateGraph(AgentState)
30
+ graph.add_node("intent_classifier", lambda s: intent_classifier_node(s, self.llm))
31
+ graph.add_node("query_rewriter", lambda s: query_rewriter_node(s, self.llm))
32
+ graph.add_node("retrieval_planner", lambda s: retrieval_planner_node(s, self.llm))
33
+ graph.add_node("hybrid_retriever", lambda s: hybrid_retriever_node(s, self.retriever))
34
+ graph.add_node("evidence_grader", lambda s: evidence_grader_node(s, self.llm))
35
+ graph.add_node("answer_generator", lambda s: answer_generator_node(s, self.llm))
36
+ graph.add_node("citation_verifier", lambda s: citation_verifier_node(s, self.llm))
37
+
38
+ graph.set_entry_point("intent_classifier")
39
+ graph.add_edge("intent_classifier", "query_rewriter")
40
+ graph.add_edge("query_rewriter", "retrieval_planner")
41
+ graph.add_edge("retrieval_planner", "hybrid_retriever")
42
+ graph.add_edge("hybrid_retriever", "evidence_grader")
43
+ graph.add_edge("evidence_grader", "answer_generator")
44
+ graph.add_edge("answer_generator", "citation_verifier")
45
+ graph.add_edge("citation_verifier", END)
46
+
47
+ compiled = graph.compile()
48
+ logging.info("LangGraph agent workflow compiled successfully.")
49
+ return compiled
50
+
51
+ except Exception as e:
52
+ logging.warning(f"LangGraph is unavailable or failed to compile; using fallback sequential graph. Error: {e}")
53
+ return None
54
+
55
+ def _run_fallback_graph(self, state: AgentState) -> AgentState:
56
+ state = intent_classifier_node(state, self.llm)
57
+ state = query_rewriter_node(state, self.llm)
58
+ state = retrieval_planner_node(state, self.llm)
59
+ state = hybrid_retriever_node(state, self.retriever)
60
+ state = evidence_grader_node(state, self.llm)
61
+ state = answer_generator_node(state, self.llm)
62
+ state = citation_verifier_node(state, self.llm)
63
+ return state
64
+
65
+ def invoke(self, inputs: Dict[str, Any], config: Optional[dict] = None) -> str:
66
+ state: AgentState = {
67
+ "input": inputs.get("input", ""),
68
+ "chat_history": inputs.get("chat_history", []),
69
+ }
70
+
71
+ if self._compiled_graph is not None:
72
+ output_state = self._compiled_graph.invoke(state, config=config)
73
+ else:
74
+ output_state = self._run_fallback_graph(state)
75
+
76
+ return output_state.get("final_answer") or output_state.get("draft_answer") or ""
77
+
78
+ def stream(self, inputs: Dict[str, Any], config: Optional[dict] = None) -> Iterable[str]:
79
+ answer = self.invoke(inputs, config=config)
80
+ chunk_size = 80
81
+ for i in range(0, len(answer), chunk_size):
82
+ yield answer[i:i + chunk_size]
83
+
84
+
85
+ def create_agentic_chain(components, llm, use_langgraph: bool = True) -> AgenticLawverseChain:
86
+ retriever = components["retriever"]
87
+ return AgenticLawverseChain(retriever=retriever, llm=llm, use_langgraph=use_langgraph)
Lawverse/agents/nodes.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from typing import Any, List
3
+ from langchain_core.documents import Document
4
+ from Lawverse.agents.state import AgentState
5
+ from Lawverse.agents.prompts import QUERY_REWRITE_PROMPT, EVIDENCE_GRADER_PROMPT, ANSWER_GENERATION_PROMPT
6
+ from Lawverse.agents.tools import (
7
+ build_source,
8
+ format_docs_for_prompt,
9
+ lexical_evidence_score,
10
+ retrieve_with_hybrid_tool
11
+ )
12
+ from Lawverse.guardrails.answer_policy import (
13
+ CLOSING_RESPONSE,
14
+ GREETING_RESPONSE,
15
+ INSUFFICIENT_EVIDENCE_RESPONSE,
16
+ NON_LEGAL_RESPONSE,
17
+ classify_simple_intent
18
+ )
19
+ from Lawverse.guardrails.legal_disclaimer import append_legal_disclaimer
20
+ from Lawverse.logger import logging
21
+
22
+
23
+ def _content_from_llm_response(response: Any) -> str:
24
+ if response is None:
25
+ return ""
26
+ if hasattr(response, "content"):
27
+ return str(response.content)
28
+ return str(response)
29
+
30
+
31
+ def _history_to_text(chat_history: List[Any], max_items: int = 6) -> str:
32
+ if not chat_history:
33
+ return "No previous chat history."
34
+ items = []
35
+ for msg in chat_history[-max_items:]:
36
+ role = msg.__class__.__name__.replace("Message", "")
37
+ content = getattr(msg, "content", str(msg))
38
+ items.append(f"{role}: {content}")
39
+ return "\n".join(items)
40
+
41
+
42
+ def intent_classifier_node(state: AgentState, llm=None) -> AgentState:
43
+ user_input = state.get("input", "")
44
+ intent, reason = classify_simple_intent(user_input)
45
+ state["intent"] = intent
46
+ state["intent_reason"] = reason
47
+ logging.info(f"Agent intent classified as {intent}: {reason}")
48
+ return state
49
+
50
+
51
+
52
+ def query_rewriter_node(state: AgentState, llm=None) -> AgentState:
53
+ question = state.get("input", "")
54
+ if state.get("intent") != "legal_question":
55
+ state["standalone_query"] = question
56
+ return state
57
+
58
+ try:
59
+ prompt = QUERY_REWRITE_PROMPT.format(
60
+ chat_history=_history_to_text(state.get("chat_history", [])),
61
+ question=question,
62
+ )
63
+ rewritten = _content_from_llm_response(llm.invoke(prompt)).strip() if llm else question
64
+ state["standalone_query"] = rewritten or question
65
+ except Exception as e:
66
+ logging.warning(f"Query rewrite failed; falling back to original query. Error: {e}")
67
+ state["standalone_query"] = question
68
+
69
+ return state
70
+
71
+
72
+ def retrieval_planner_node(state: AgentState, llm=None) -> AgentState:
73
+ if state.get("intent") != "legal_question":
74
+ state["retrieval_plan"] = "no_retrieval"
75
+ else:
76
+ state["retrieval_plan"] = "hybrid_dense_sparse_rerank"
77
+ return state
78
+
79
+
80
+ def hybrid_retriever_node(state: AgentState, retriever=None) -> AgentState:
81
+ if state.get("retrieval_plan") == "no_retrieval":
82
+ state["retrieved_docs"] = []
83
+ return state
84
+
85
+ query = state.get("standalone_query") or state.get("input", "")
86
+ try:
87
+ docs = retrieve_with_hybrid_tool(retriever, query, top_k=5)
88
+ state["retrieved_docs"] = docs
89
+ state["sources"] = build_source(docs)
90
+ except Exception as e:
91
+ logging.error(f"Agent retrieval failed: {e}")
92
+ state["retrieved_docs"] = []
93
+ state["sources"] = []
94
+ state["error"] = str(e)
95
+ return state
96
+
97
+
98
+
99
+ def evidence_grader_node(state: AgentState, llm=None) -> AgentState:
100
+ docs: List[Document] = state.get("retrieved_docs", []) or []
101
+ question = state.get("standalone_query") or state.get("input", "")
102
+
103
+ if state.get("intent") != "legal_question":
104
+ state["has_enough_evidence"] = False
105
+ state["evidence_score"] = 0.0
106
+ state["evidence_reason"] = "No legal retrieval required."
107
+ return state
108
+
109
+ score = lexical_evidence_score(question, docs)
110
+ state["evidence_score"] = score
111
+
112
+ if not docs:
113
+ state["has_enough_evidence"] = False
114
+ state["evidence_reason"] = "No retrieved documents."
115
+ return state
116
+
117
+ try:
118
+ context = format_docs_for_prompt(docs, max_chars=5000)
119
+ prompt = EVIDENCE_GRADER_PROMPT.format(question=question, context=context)
120
+ grade = _content_from_llm_response(llm.invoke(prompt)).strip() if llm else ""
121
+ lower = grade.lower()
122
+ if lower.startswith("sufficient"):
123
+ state["has_enough_evidence"] = True
124
+ state["evidence_reason"] = grade
125
+ return state
126
+ if lower.startswith("insufficient"):
127
+ state["has_enough_evidence"] = score >= 0.45
128
+ state["evidence_reason"] = grade
129
+ return state
130
+ except Exception as e:
131
+ logging.warning(f"LLM evidence grading failed; using lexical score. Error: {e}")
132
+
133
+ state["has_enough_evidence"] = score >= 0.25
134
+ state["evidence_reason"] = f"Lexical evidence score={score}."
135
+ return state
136
+
137
+
138
+ def answer_generator_node(state: AgentState, llm=None) -> AgentState:
139
+ intent = state.get("intent")
140
+
141
+ if intent == "greeting":
142
+ state["draft_answer"] = GREETING_RESPONSE
143
+ return state
144
+ if intent == "closing":
145
+ state["draft_answer"] = CLOSING_RESPONSE
146
+ return state
147
+ if intent in {"non_legal", "empty"}:
148
+ state["draft_answer"] = NON_LEGAL_RESPONSE
149
+ return state
150
+ if not state.get("has_enough_evidence"):
151
+ state["draft_answer"] = INSUFFICIENT_EVIDENCE_RESPONSE
152
+ return state
153
+
154
+ docs = state.get("retrieved_docs", []) or []
155
+ context = format_docs_for_prompt(docs)
156
+ question = state.get("input", "")
157
+
158
+ try:
159
+ prompt = ANSWER_GENERATION_PROMPT.format(question=question, context=context)
160
+ answer = _content_from_llm_response(llm.invoke(prompt)).strip() if llm else ""
161
+ state["draft_answer"] = answer or INSUFFICIENT_EVIDENCE_RESPONSE
162
+ except Exception as e:
163
+ logging.error(f"Answer generation failed: {e}")
164
+ state["draft_answer"] = INSUFFICIENT_EVIDENCE_RESPONSE
165
+ state["error"] = str(e)
166
+
167
+ return state
168
+
169
+
170
+ def citation_verifier_node(state: AgentState, llm=None) -> AgentState:
171
+ answer = state.get("draft_answer", "") or ""
172
+ docs = state.get("retrieved_docs", []) or []
173
+ issues = []
174
+
175
+ if state.get("intent") == "legal_question" and state.get("has_enough_evidence"):
176
+ if "### Sources" not in answer:
177
+ issues.append("Answer did not include a Sources section; sources were appended automatically.")
178
+ sources = build_source(docs)
179
+ source_lines = []
180
+ for src in sources:
181
+ source_lines.append(
182
+ f"- Source {src['rank']}: {src['source']} | Page: {src['page']} | "
183
+ f"Chunk: {src['chunk_id']} | Score: {src['score']}"
184
+ )
185
+ answer = f"{answer}\n\n### Sources\n" + "\n".join(source_lines)
186
+
187
+ state["citation_issues"] = issues
188
+ state["citation_check_passed"] = len(issues) == 0
189
+ state["final_answer"] = append_legal_disclaimer(answer)
190
+ return state
Lawverse/agents/prompts.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ QUERY_REWRITE_PROMPT = """
2
+ You are a legal retrieval query rewriter for Bangladeshi legal documents.
3
+ Rewrite the latest user question into a standalone search query.
4
+ Keep legal keywords, section names, act names, and important facts.
5
+ Do not answer the question.
6
+
7
+ Chat history summary:
8
+ {chat_history}
9
+
10
+ User question:
11
+ {question}
12
+
13
+ Standalone retrieval query:
14
+ """
15
+
16
+ EVIDENCE_GRADER_PROMPT = """
17
+ You are checking whether retrieved legal context is sufficient for answering a user question.
18
+ Return only one of these two labels followed by a short reason:
19
+ - SUFFICIENT: reason
20
+ - INSUFFICIENT: reason
21
+
22
+ Question:
23
+ {question}
24
+
25
+ Retrieved context:
26
+ {context}
27
+ """
28
+
29
+ ANSWER_GENERATION_PROMPT = """
30
+ You are Lawverse, an educational legal document intelligence assistant for Bangladeshi legal documents.
31
+
32
+ BOUNDARIES:
33
+ - You provide legal information from retrieved documents, not professional legal advice.
34
+ - Answer only from the retrieved context.
35
+ - Do not invent laws, sections, document names, citations, page numbers, or facts.
36
+ - If the retrieved context is insufficient, say that the provided documents do not contain sufficient information.
37
+
38
+ User question:
39
+ {question}
40
+
41
+ Retrieved context:
42
+ {context}
43
+
44
+ Required output format:
45
+ ### Answer
46
+ Clear answer based only on the retrieved context.
47
+
48
+ ### Sources
49
+ List sources used. Include document/source name, page if available, chunk id if available, and why it supports the answer.
50
+ """
Lawverse/agents/state.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from typing import Any, Dict, List, Optional, TypedDict
3
+ from langchain_core.documents import Document
4
+
5
+
6
+ class AgentState(TypedDict, total=False):
7
+ input: str
8
+ chat_history: List[Any]
9
+
10
+ intent: str
11
+ intent_reason: str
12
+ standalone_query: str
13
+ retrieval_plan: str
14
+
15
+ retrieved_docs: List[Document]
16
+ evidence_score: float
17
+ has_enough_evidence: bool
18
+ evidence_reason: str
19
+
20
+ draft_answer: str
21
+ final_answer: str
22
+ sources: List[Dict[str, Any]]
23
+ citation_check_passed: bool
24
+ citation_issues: List[str]
25
+
26
+ error: Optional[str]
Lawverse/agents/tools.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from typing import Dict, Any, List
3
+ from langchain_core.documents import Document
4
+
5
+
6
+ def retrieve_with_hybrid_tool(retriever, query: str, top_k: int = 5) -> List[Document]:
7
+ if retriever is None:
8
+ return None
9
+
10
+ try:
11
+ docs = retriever.invoke(query)
12
+ except Exception:
13
+ try:
14
+ docs = retriever.get_relevant_documents(query)
15
+ except Exception:
16
+ docs = retriever._get_relevant_documents(query)
17
+
18
+ return list(docs or [])[:top_k]
19
+
20
+
21
+ def document_to_source(doc: Document, rank: int | None=None) -> Dict[str, Any]:
22
+ metadata = dict(doc.metadata or {})
23
+
24
+ return {
25
+ "rank": rank or metadata.get("rank"),
26
+ "source": metadata.get("source", "unknown"),
27
+ "page": metadata.get("page_label", metadata.get("page", "unknown")),
28
+ "chunk_id": metadata.get("chunk_id", "unknown"),
29
+ "score": metadata.get("score", metadata.get("rrf_score", "unknown")),
30
+ "retriever": metadata.get("retriever", "hybrid"),
31
+ "preview": (doc.page_content or "")[:350].replace("\n", " "),
32
+ }
33
+
34
+
35
+ def format_docs_for_prompt(docs: List[Document], max_chars: int = 7000) -> str:
36
+ blocks = []
37
+ total = 0
38
+ for idx, doc in enumerate(docs or [], 1):
39
+ source = document_to_source(doc, rank=idx)
40
+ text = doc.page_content or ""
41
+ block = (
42
+ f"[Source {idx}: {source['source']} | Page: {source['page']} | "
43
+ f"Chunk: {source['chunk_id']} | Score: {source['score']}]\n{text}"
44
+ )
45
+ if total + len(block) > max_chars:
46
+ break
47
+ blocks.append(block)
48
+ total += len(block)
49
+ return "\n\n---\n\n".join(blocks)
50
+
51
+
52
+ def build_source(docs: List[Document]) -> List[Dict[str, Any]]:
53
+ return [document_to_source(doc, rank=i) for i, doc in enumerate(docs or [], 1)]
54
+
55
+
56
+ def lexical_evidence_score(question: str, docs: List[Document]) -> float:
57
+ if not question or not docs:
58
+ return 0.0
59
+
60
+ stop = {
61
+ "the", "a", "an", "and", "or", "to", "of", "in", "for", "is", "are", "am", "i",
62
+ "what", "how", "why", "when", "can", "could", "should", "me", "my", "about",
63
+ }
64
+ q_words = {w.strip(".,?!;:()[]{}'\"").lower() for w in question.split()}
65
+ q_words = {w for w in q_words if len(w) > 2 and w not in stop}
66
+ if not q_words:
67
+ return 0.0
68
+
69
+ context = " ".join((doc.page_content or "") for doc in docs).lower()
70
+ hits = sum(1 for w in q_words if w in context)
71
+ return round(hits / max(len(q_words), 1), 4)
Lawverse/guardrails/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from Lawverse.guardrails.legal_disclaimer import LEGAL_DISCLAIMER, append_legal_disclaimer
2
+
3
+ __all__ = ["LEGAL_DISCLAIMER", "append_legal_disclaimer"]
Lawverse/guardrails/answer_policy.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ from typing import Iterable
3
+ from langchain_core.documents import Document
4
+
5
+ INSUFFICIENT_EVIDENCE_RESPONSE = (
6
+ "### Answer\n"
7
+ "The provided documents do not contain sufficient information to answer this question safely.\n\n"
8
+ "### Sources\n"
9
+ "No sufficiently relevant source was found in the indexed documents."
10
+ )
11
+
12
+ NON_LEGAL_RESPONSE = (
13
+ "### Answer\n"
14
+ "I'm designed to assist with Bangladeshi legal document questions. "
15
+ "Please ask a legal question or upload/provide legal context."
16
+ )
17
+
18
+ GREETING_RESPONSE = (
19
+ "### Answer\n"
20
+ "Hello! I can help you ask questions about Bangladeshi legal documents and show sources from the retrieved context."
21
+ )
22
+
23
+ CLOSING_RESPONSE = (
24
+ "### Answer\n"
25
+ "You're welcome. Ask another legal-document question whenever you need help."
26
+ )
27
+
28
+
29
+ LEGAL_KEYWORDS = {
30
+ "law", "legal", "court", "case", "act", "section", "rule", "rights", "contract",
31
+ "agreement", "crime", "criminal", "civil", "penalty", "bail", "appeal", "property",
32
+ "labour", "labor", "worker", "employee", "employer", "termination", "notice", "salary",
33
+ "wage", "rent", "tax", "company", "constitution", "ordinance", "বাংলাদেশ", "আইন",
34
+ "ধারা", "আদালত", "মামলা", "অধিকার", "শ্রম", "চুক্তি", "জামিন", "অপরাধ",
35
+ }
36
+
37
+ GREETING_WORDS = {"hi", "hello", "hey", "assalamu", "salam", "হাই", "হ্যালো", "সালাম"}
38
+ CLOSING_WORDS = {"thanks", "thank you", "bye", "goodbye", "ধন্যবাদ", "আচ্ছা", "বিদায়"}
39
+
40
+
41
+ def classify_simple_intent(text: str) -> tuple[str, str]:
42
+ clean = (text or "").strip().lower()
43
+ if not clean:
44
+ return "empty", "Empty user input."
45
+
46
+ token_hits = [kw for kw in LEGAL_KEYWORDS if kw in clean]
47
+ if any(word in clean for word in GREETING_WORDS) and len(clean.split()) <= 8:
48
+ return "greeting", "Short greeting detected."
49
+ if any(word in clean for word in CLOSING_WORDS) and len(clean.split()) <= 10:
50
+ return "closing", "Short closing/thanks message detected."
51
+ if token_hits:
52
+ return "legal_question", f"Legal keywords detected: {', '.join(token_hits[:5])}."
53
+
54
+ if len(clean.split()) >= 8:
55
+ return "legal_question", "Long-form question; routed to retrieval for evidence check."
56
+
57
+ return "non_legal", "No legal intent signal detected."
58
+
59
+ def has_documents(docs: Iterable[Document]) -> bool:
60
+ return bool(list(docs or []))
Lawverse/guardrails/legal_disclaimer.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ LEGAL_DISCLAIMER = (
2
+ "Lawverse is an educational legal information assistant. "
3
+ "It is not a substitute for a licensed lawyer."
4
+ )
5
+
6
+ def append_legal_disclaimer(answer: str) -> str:
7
+ answer = (answer or "").strip()
8
+ if not answer:
9
+ return f"### Legal Disclaimer\n{LEGAL_DISCLAIMER}"
10
+
11
+ if "not a substitute for a licensed lawyer" in answer.lower():
12
+ return answer
13
+
14
+ return f"{answer}\n\n### Legal Disclaimer\n{LEGAL_DISCLAIMER}"
api/app.py CHANGED
@@ -1,8 +1,11 @@
1
- from Lawverse.pipeline.rag_pipeline import rag_components, create_chat_chain
2
  from flask import Flask, render_template, request, jsonify, session, stream_with_context, Response
 
 
 
3
  from Lawverse.utils.config import MEMORY_DIR
4
  from Lawverse.logger import logging
5
  from Lawverse.monitoring.dashboard import monitor_bp
 
6
  from api.auth import auth_bp, login_required
7
  from api.models import db
8
  from api.admin import admin
@@ -27,9 +30,32 @@ app.register_blueprint(monitor_bp)
27
  with app.app_context():
28
  db.create_all()
29
 
30
- BASE_COMPONENTS = rag_components()
31
  active_chains = {}
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  @app.route("/", methods=["GET"])
34
  def home():
35
  return render_template("index.html")
@@ -39,21 +65,19 @@ def home():
39
  def chat():
40
  chat_id = session.get("chat_id")
41
  if not chat_id or chat_id not in active_chains:
42
- chain, memory_manager = create_chat_chain(BASE_COMPONENTS)
43
- active_chains[memory_manager.chat_id] = (chain, memory_manager)
44
- session["chat_id"] = memory_manager.chat_id
45
- memory_manager.save_memory()
46
-
47
  return render_template("chat.html")
48
 
49
  @app.route("/new_chat", methods=["POST"])
50
  @login_required
51
  def new_chat():
52
- chain, memory_manager = create_chat_chain(BASE_COMPONENTS)
53
- active_chains[memory_manager.chat_id] = (chain, memory_manager)
54
- session["chat_id"] = memory_manager.chat_id
55
- memory_manager.save_memory()
56
- return jsonify({"chat_id": memory_manager.chat_id, "title": memory_manager._get_title()})
 
57
 
58
 
59
  @app.route("/response", methods=["POST"])
@@ -62,15 +86,13 @@ def rag_response():
62
  try:
63
  chat_id = session.get("chat_id")
64
  if not chat_id or chat_id not in active_chains:
65
- chain, memory_manager = create_chat_chain(BASE_COMPONENTS)
66
- active_chains[memory_manager.chat_id] = (chain, memory_manager)
67
- session["chat_id"] = memory_manager.chat_id
68
  chat_id = memory_manager.chat_id
69
 
70
  qa, memory_manager = active_chains[chat_id]
71
-
72
  data = request.get_json(silent=True) or {}
73
  query = data.get("message", "").strip()
 
74
  if not query:
75
  return jsonify({"error": "Empty message"}), 400
76
 
@@ -105,19 +127,23 @@ def rag_response():
105
  def get_chats():
106
  chats = []
107
  user_id = session.get("user_id")
108
-
109
  for file_path in glob.glob(f"{MEMORY_DIR}/*.json"):
110
- with open(file_path, "r", encoding="utf-8") as f:
111
- data = json.load(f)
112
-
113
- if data.get("user_id") == user_id:
114
- chats.append({
115
- "chat_id": data.get("chat_id"),
116
- "last_updated": data.get("last_updated"),
117
- "title": data.get("title", f"Chat-{data.get('chat_id')}")
118
- })
 
119
 
120
- chats.sort(key=lambda x: x.get("last_updated", x["chat_id"]), reverse=True)
 
 
 
121
  return jsonify(chats)
122
 
123
 
@@ -129,9 +155,7 @@ def load_chat(chat_id):
129
  if not os.path.exists(memory_path):
130
  return jsonify({"error": "Chat not found"}), 404
131
 
132
- chain, memory_manager = create_chat_chain(BASE_COMPONENTS, chat_id=chat_id)
133
- active_chains[memory_manager.chat_id] = (chain, memory_manager)
134
- session["chat_id"] = memory_manager.chat_id
135
 
136
  messages_list = memory_manager.memory.chat_memory.messages
137
  messages = []
@@ -140,7 +164,10 @@ def load_chat(chat_id):
140
  user_msg = messages_list[i].content if i < len(messages_list) else None
141
  ai_msg = messages_list[i + 1].content if i + 1 < len(messages_list) else ""
142
  if user_msg:
143
- messages.append({"user": user_msg, "ai": ai_msg})
 
 
 
144
 
145
  return jsonify({
146
  "chat_id": chat_id,
@@ -164,7 +191,13 @@ def delete_chat(chat_id):
164
  if was_active:
165
  del active_chains[chat_id]
166
 
167
- return jsonify({"success": True, "was_active": was_active}), 200
 
 
 
 
 
 
168
 
169
  except Exception as e:
170
  logging.error(f"Error deleting chat {chat_id}: {e}")
 
 
1
  from flask import Flask, render_template, request, jsonify, session, stream_with_context, Response
2
+ from Lawverse.pipeline.rag_pipeline import rag_components
3
+ from Lawverse.pipeline.llm_loader import llm
4
+ from Lawverse.memory.langchain_memory import ChatMemory
5
  from Lawverse.utils.config import MEMORY_DIR
6
  from Lawverse.logger import logging
7
  from Lawverse.monitoring.dashboard import monitor_bp
8
+ from Lawverse.agents.graph import create_agentic_chain
9
  from api.auth import auth_bp, login_required
10
  from api.models import db
11
  from api.admin import admin
 
30
  with app.app_context():
31
  db.create_all()
32
 
33
+ BASE_COMPONENTS = None
34
  active_chains = {}
35
 
36
+
37
+ def get_base_components():
38
+ global BASE_COMPONENTS
39
+
40
+ if BASE_COMPONENTS is None:
41
+ logging.info("Loading Lawverse RAG base components...")
42
+ BASE_COMPONENTS = rag_components()
43
+ logging.info("Lawverse RAG base components loaded successfully.")
44
+
45
+ return BASE_COMPONENTS
46
+
47
+
48
+ def create_agent_session(chat_id=None):
49
+ components = get_base_components()
50
+ chain = create_agentic_chain(components, llm)
51
+ memory_manager = ChatMemory(chat_id=chat_id)
52
+
53
+ active_chains[memory_manager.chat_id] = (chain, memory_manager)
54
+ session["chat_id"] = memory_manager.chat_id
55
+ memory_manager.save_memory()
56
+
57
+ return chain, memory_manager
58
+
59
  @app.route("/", methods=["GET"])
60
  def home():
61
  return render_template("index.html")
 
65
  def chat():
66
  chat_id = session.get("chat_id")
67
  if not chat_id or chat_id not in active_chains:
68
+ create_agent_session()
69
+
 
 
 
70
  return render_template("chat.html")
71
 
72
  @app.route("/new_chat", methods=["POST"])
73
  @login_required
74
  def new_chat():
75
+ _, memory_manager = create_agent_session()
76
+
77
+ return jsonify({
78
+ "chat_id": memory_manager.chat_id,
79
+ "title": memory_manager._get_title()
80
+ })
81
 
82
 
83
  @app.route("/response", methods=["POST"])
 
86
  try:
87
  chat_id = session.get("chat_id")
88
  if not chat_id or chat_id not in active_chains:
89
+ _, memory_manager = create_agent_session()
 
 
90
  chat_id = memory_manager.chat_id
91
 
92
  qa, memory_manager = active_chains[chat_id]
 
93
  data = request.get_json(silent=True) or {}
94
  query = data.get("message", "").strip()
95
+
96
  if not query:
97
  return jsonify({"error": "Empty message"}), 400
98
 
 
127
  def get_chats():
128
  chats = []
129
  user_id = session.get("user_id")
130
+ os.makedirs(MEMORY_DIR, exist_ok=True)
131
  for file_path in glob.glob(f"{MEMORY_DIR}/*.json"):
132
+ try:
133
+ with open(file_path, "r", encoding="utf-8") as f:
134
+ data = json.load(f)
135
+
136
+ if data.get("user_id") == user_id:
137
+ chats.append({
138
+ "chat_id": data.get("chat_id"),
139
+ "last_updated": data.get("last_updated"),
140
+ "title": data.get("title", f"Chat-{data.get('chat_id')}")
141
+ })
142
 
143
+ except Exception as e:
144
+ logging.warning(f"Skipping unreadable memory file {file_path}: {e}")
145
+
146
+ chats.sort(key=lambda x: x.get("last_updated") or x.get("chat_id"), reverse=True)
147
  return jsonify(chats)
148
 
149
 
 
155
  if not os.path.exists(memory_path):
156
  return jsonify({"error": "Chat not found"}), 404
157
 
158
+ _, memory_manager = create_agent_session(chat_id=chat_id)
 
 
159
 
160
  messages_list = memory_manager.memory.chat_memory.messages
161
  messages = []
 
164
  user_msg = messages_list[i].content if i < len(messages_list) else None
165
  ai_msg = messages_list[i + 1].content if i + 1 < len(messages_list) else ""
166
  if user_msg:
167
+ messages.append({
168
+ "user": user_msg,
169
+ "ai": ai_msg
170
+ })
171
 
172
  return jsonify({
173
  "chat_id": chat_id,
 
191
  if was_active:
192
  del active_chains[chat_id]
193
 
194
+ if session.get("chat_id") == chat_id:
195
+ session.pop("chat_id", None)
196
+
197
+ return jsonify({
198
+ "success": True,
199
+ "was_active": was_active
200
+ }), 200
201
 
202
  except Exception as e:
203
  logging.error(f"Error deleting chat {chat_id}: {e}")
template.py CHANGED
@@ -31,6 +31,17 @@ list_of_files = [
31
 
32
  f"{project_name}/monitoring/dashboard.py",
33
 
 
 
 
 
 
 
 
 
 
 
 
34
  ".github/workflows/to_hf.yml",
35
 
36
  "api/admin.py",
 
31
 
32
  f"{project_name}/monitoring/dashboard.py",
33
 
34
+ f"{project_name}/agents/__init__.py",
35
+ f"{project_name}/agents/state.py",
36
+ f"{project_name}/agents/prompts.py",
37
+ f"{project_name}/agents/tools.py",
38
+ f"{project_name}/agents/nodes.py",
39
+ f"{project_name}/agents/graph.py",
40
+
41
+ f"{project_name}/guardrails/__init__.py",
42
+ f"{project_name}/guardrails/legal_disclaimer.py",
43
+ f"{project_name}/guardrails/answer_policy.py",
44
+
45
  ".github/workflows/to_hf.yml",
46
 
47
  "api/admin.py",
templates/index.html CHANGED
@@ -369,9 +369,6 @@
369
  <a href="https://www.linkedin.com/in/mohsin416/" target="_blank" class="text-gray-400 hover:text-cyan-300 transition"
370
  ><i data-feather="linkedin"></i
371
  ></a>
372
- <a href="siam.mohsin2005@gmail.com" class="text-gray-400 hover:text-purple-300 transition"
373
- ><i data-feather="mail"></i
374
- ></a>
375
  <a href="https://www.facebook.com/mohsin.siam6" target="_blank" class="text-gray-400 hover:text-blue-300 transition"
376
  ><i data-feather="facebook"></i
377
  ></a>
 
369
  <a href="https://www.linkedin.com/in/mohsin416/" target="_blank" class="text-gray-400 hover:text-cyan-300 transition"
370
  ><i data-feather="linkedin"></i
371
  ></a>
 
 
 
372
  <a href="https://www.facebook.com/mohsin.siam6" target="_blank" class="text-gray-400 hover:text-blue-300 transition"
373
  ><i data-feather="facebook"></i
374
  ></a>