Skip to content

Commit 5c9b721

Browse files
authored
Merge pull request #1660 from oracle-devrel/lsa-custom-rag-agent2
added custom_rag_agent demo to genai-agents
2 parents 12f1053 + d9c6b9c commit 5c9b721

19 files changed

+1824
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Luigi Saetta
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
![UI](images/ui_image.png)
2+
3+
# Custom RAG agent
4+
This repository contains the code for the development of a **custom RAG Agent**, based on OCI Generative AI, Oracle 23AI DB and **LangGraph**
5+
6+
## Design and implementation
7+
* The agent is implemented using **LangGraph**
8+
* Vector Search is implemented, using Langchain, on top of Oracle 23AI
9+
* A **reranker** can be used to refine the search
10+
11+
Design decisions:
12+
* For every node of the graph there is a dedicated Python class (a **Runnable**, as QueryRewriter...)
13+
* Reranker is implemented using a LLM. As other option, it is easy to plug-in, for example, Cohere reranker
14+
* The agent is integrated with **OCI APM**, for **Observability**; Integration using **py-zipkin**
15+
* UI implemented using **Streamlit**
16+
17+
Streaming:
18+
* Support for streaming events from the agent: as soon as a step is completed (Vector Search, Reranking, ...) the UI is updated.
19+
For example, links to the documentation' chunks are displayed before the final answer is ready.
20+
* Streaming of the final answer.
21+
22+
## Status
23+
It is **wip**.
24+
25+
## References
26+
* [Integration with OCI APM](https://luigi-saetta.medium.com/enhancing-observability-in-rag-solutions-with-oracle-cloud-6f93b2675f40)
27+
28+
## Advantages of the Agentic approach
29+
One of the primary advantages of the agentic approach is its modularity.
30+
Customer requirements often surpass the simplicity of typical Retrieval-Augmented Generation (RAG) demonstrations. Implementing a framework like **LangGraph** necessitates organizing code into a modular sequence of steps, facilitating the seamless integration of additional features at appropriate places.​
31+
32+
For example, to ensure that final responses do not disclose Personally Identifiable Information (PII) present in the knowledge base, one can simply append a node at the end of the graph. This node would process the generated answer, detect any PII, and anonymize it accordingly.
33+
34+
## Configuration
35+
* use Python 3.11
36+
* use the requirements.txt
37+
* create your config_private.py using the template provided
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
File name: agent_state.py
3+
Author: Luigi Saetta
4+
Date last modified: 2025-03-31
5+
Python Version: 3.11
6+
7+
Description:
8+
This module defines the class that handles the agent's State
9+
10+
11+
Usage:
12+
Import this module into other scripts to use its functions.
13+
Example:
14+
from agent_state import State
15+
16+
License:
17+
This code is released under the MIT License.
18+
19+
Notes:
20+
This is a part of a demo showing how to implement an advanced
21+
RAG solution as a LangGraph agent.
22+
23+
Warnings:
24+
This module is in development, may change in future versions.
25+
"""
26+
27+
from typing_extensions import TypedDict, Optional
28+
29+
30+
class State(TypedDict):
31+
"""
32+
The state of the graph
33+
"""
34+
35+
# the original user request
36+
user_request: str
37+
chat_history: list = []
38+
39+
# the question reformulated using chat_history
40+
standalone_question: str = ""
41+
42+
# similarity_search
43+
retriever_docs: Optional[list] = []
44+
# reranker
45+
reranker_docs: Optional[list] = []
46+
# Answer
47+
final_answer: str
48+
# Citations
49+
citations: list = []
50+
51+
# if any step encounter an error
52+
error: Optional[str] = None
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""
2+
File name: answer_generator.py
3+
Author: Luigi Saetta
4+
Date last modified: 2025-03-31
5+
Python Version: 3.11
6+
7+
Description:
8+
This module implements the last step in the workflow: generation
9+
of the answer form the LLM:
10+
11+
12+
Usage:
13+
Import this module into other scripts to use its functions.
14+
Example:
15+
from answer_generator import AnswerGenerator
16+
17+
License:
18+
This code is released under the MIT License.
19+
20+
Notes:
21+
This is a part of a demo showing how to implement an advanced
22+
RAG solution as a LangGraph agent.
23+
24+
Warnings:
25+
This module is in development, may change in future versions.
26+
"""
27+
28+
from langchain_core.runnables import Runnable
29+
from langchain_core.messages import SystemMessage, HumanMessage
30+
from langchain.prompts import PromptTemplate
31+
32+
# integration with APM
33+
from py_zipkin.zipkin import zipkin_span
34+
35+
from agent_state import State
36+
from oci_models import get_llm
37+
from prompts import (
38+
ANSWER_PROMPT_TEMPLATE,
39+
)
40+
41+
from utils import get_console_logger
42+
from config import AGENT_NAME, DEBUG
43+
44+
logger = get_console_logger()
45+
46+
47+
class AnswerGenerator(Runnable):
48+
"""
49+
Takes the user request and the chat history and rewrite the user query
50+
in a standalone question that is used for the semantic search
51+
"""
52+
53+
def __init__(self):
54+
"""
55+
Init
56+
"""
57+
self.dict_languages = {
58+
"en": "English",
59+
"fr": "French",
60+
"it": "Italian",
61+
"es": "Spanish",
62+
}
63+
64+
def build_context_for_llm(self, docs: list):
65+
"""
66+
Build the context for the final answer from LLM
67+
68+
docs: list[Documents]
69+
"""
70+
_context = ""
71+
72+
for doc in docs:
73+
_context += doc.page_content + "\n\n"
74+
75+
return _context
76+
77+
@zipkin_span(service_name=AGENT_NAME, span_name="answer_generation")
78+
def invoke(self, input: State, config=None, **kwargs):
79+
"""
80+
Generate the final answer
81+
"""
82+
# get the config
83+
model_id = config["configurable"]["model_id"]
84+
85+
if config["configurable"]["main_language"] in self.dict_languages:
86+
# want to change language
87+
main_language = self.dict_languages.get(
88+
config["configurable"]["main_language"]
89+
)
90+
else:
91+
# "same as the question" (default)
92+
# answer will be in the same language as the question
93+
main_language = None
94+
95+
if DEBUG:
96+
logger.info("AnswerGenerator, model_id: %s", model_id)
97+
logger.info("AnswerGenerator, main_language: %s", main_language)
98+
99+
final_answer = ""
100+
error = None
101+
102+
try:
103+
llm = get_llm(model_id=model_id)
104+
105+
_context = self.build_context_for_llm(input["reranker_docs"])
106+
107+
system_prompt = PromptTemplate(
108+
input_variables=["context"],
109+
template=ANSWER_PROMPT_TEMPLATE,
110+
).format(context=_context)
111+
112+
messages = [
113+
SystemMessage(content=system_prompt),
114+
]
115+
# add the chat history
116+
for msg in input["chat_history"]:
117+
messages.append(msg)
118+
119+
# to force the answer in the selected language
120+
if main_language is not None:
121+
the_question = f"{input['user_request']}. Answer in {main_language}."
122+
else:
123+
# no cross language
124+
the_question = input["user_request"]
125+
126+
messages.append(HumanMessage(content=the_question))
127+
128+
# here we invoke the LLM and we return the generator
129+
final_answer = llm.stream(messages)
130+
131+
except Exception as e:
132+
logger.error("Error in generate_answer: %s", e)
133+
error = str(e)
134+
135+
return {"final_answer": final_answer, "error": error}

0 commit comments

Comments
 (0)