Skip to content

Commit 905222d

Browse files
authored
Merge branch 'main' into oag-rest-postman-request-sample
2 parents 1daf26c + 66471a8 commit 905222d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+902
-2264
lines changed

ai/gen-ai-agents/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Oracle’s Generative AI Agents is a fully managed service that combines the pow
1818
## Reusable Assets Overview
1919
- [HCM agent created by partner Conneqtion Group which contains agents to connect to Fusion HCM, Expense and many others](https://www.youtube.com/watch?v=OhZcWx_H_tQ)
2020
- [Finance analytics agent created by our partner TPX impact](https://bit.ly/genai4analyst)
21+
- [Custom RAG agent, based on Langgraph](./custom-rag-agent)
2122

2223
# Useful Links
2324

@@ -38,3 +39,4 @@ Copyright (c) 2025 Oracle and/or its affiliates.
3839
Licensed under the Universal Permissive License (UPL), Version 1.0.
3940

4041
See [LICENSE](https://github.com/oracle-devrel/technology-engineering/blob/main/LICENSE) for more details.
42+

ai/gen-ai-agents/custom-rag-agent/LICENSE

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,46 @@
11
![UI](images/ui_image.png)
22

33
# 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**
4+
This repository contains the code for the development of a **custom RAG Agent**, based on **OCI Generative AI**, **Oracle 23AI** Vector Store and **LangGraph**
5+
6+
**Author**: L. Saetta
7+
8+
**Last updated**: 11/09/2025
59

610
## Design and implementation
711
* The agent is implemented using **LangGraph**
812
* Vector Search is implemented, using Langchain, on top of Oracle 23AI
913
* A **reranker** can be used to refine the search
1014

11-
Design decisions:
15+
### Design decisions:
1216
* 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
17+
* **Reranker** is implemented using a LLM. As other option, it is easy to plug-in, for example, Cohere reranker
1418
* The agent is integrated with **OCI APM**, for **Observability**; Integration using **py-zipkin**
1519
* UI implemented using **Streamlit**
20+
* **Semantic Search** is also exposed as a [MCP server](./mcp_semantic_search_with_iam.py)
1621

17-
Streaming:
22+
### Streaming:
1823
* Support for streaming events from the agent: as soon as a step is completed (Vector Search, Reranking, ...) the UI is updated.
1924
For example, links to the documentation' chunks are displayed before the final answer is ready.
2025
* Streaming of the final answer.
2126

27+
### MCP support:
28+
(07/2025) I have added an implementation of an **MCP** server that exposes the Semantic Search feature.
29+
* added a [demo LLM with MCP](./ui_mcp_agent.py) showing how to integrate a generic MCP server in a Chatbot using a LLM.
30+
31+
**Security** can be handled in two ways:
32+
* custom: generate the **JWT token** using the library **PyJWT**
33+
* **OCI**: generate the JWT token using **OCI IAM**
34+
2235
## Status
23-
It is **wip**.
36+
It is always and proudly **WIP**.
2437

2538
## References
39+
For more information:
2640
* [Integration with OCI APM](https://luigi-saetta.medium.com/enhancing-observability-in-rag-solutions-with-oracle-cloud-6f93b2675f40)
2741

2842
## Advantages of the Agentic approach
29-
One of the primary advantages of the agentic approach is its modularity.
43+
One of the primary advantages of the agentic approach is its **modularity**.
3044
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.​
3145

3246
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.
@@ -35,3 +49,5 @@ For example, to ensure that final responses do not disclose Personally Identifia
3549
* use Python 3.11
3650
* use the requirements.txt
3751
* create your config_private.py using the template provided
52+
* for MCP server: create a confidential application in **OCI IAM** to handle JWT tokens.
53+

ai/gen-ai-agents/custom-rag-agent/agent_state.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ class State(TypedDict):
4040
standalone_question: str = ""
4141

4242
# similarity_search
43+
# 30/06: modified, now they're a dict with
44+
# page_content and metadata
45+
# populated with docs_serializable (utils.py)
4346
retriever_docs: Optional[list] = []
4447
# reranker
4548
reranker_docs: Optional[list] = []

ai/gen-ai-agents/custom-rag-agent/answer_generator.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
File name: answer_generator.py
33
Author: Luigi Saetta
4-
Date last modified: 2025-03-31
4+
Date last modified: 2025-04-02
55
Python Version: 3.11
66
77
Description:
@@ -67,10 +67,8 @@ def build_context_for_llm(self, docs: list):
6767
6868
docs: list[Documents]
6969
"""
70-
_context = ""
71-
72-
for doc in docs:
73-
_context += doc.page_content + "\n\n"
70+
# more Pythonic
71+
_context = "\n\n".join(doc["page_content"] for doc in docs)
7472

7573
return _context
7674

@@ -79,7 +77,7 @@ def invoke(self, input: State, config=None, **kwargs):
7977
"""
8078
Generate the final answer
8179
"""
82-
# get the config
80+
# get the model_id from config
8381
model_id = config["configurable"]["model_id"]
8482

8583
if config["configurable"]["main_language"] in self.dict_languages:
@@ -102,6 +100,7 @@ def invoke(self, input: State, config=None, **kwargs):
102100
try:
103101
llm = get_llm(model_id=model_id)
104102

103+
# docs are returned from the reranker
105104
_context = self.build_context_for_llm(input["reranker_docs"])
106105

107106
system_prompt = PromptTemplate(

ai/gen-ai-agents/custom-rag-agent/assistant_ui_langgraph.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
File name: assistant_ui.py
33
Author: Luigi Saetta
44
Date created: 2024-12-04
5-
Date last modified: 2025-03-31
5+
Date last modified: 2025-07-01
66
Python Version: 3.11
77
88
Description:
@@ -15,7 +15,7 @@
1515
This code is released under the MIT License.
1616
1717
Notes:
18-
This is part of a demo fro a RAG solution implemented
18+
This is part of a demo for a RAG solution implemented
1919
using LangGraph
2020
2121
Warnings:
@@ -38,7 +38,7 @@
3838
from transport import http_transport
3939
from utils import get_console_logger
4040

41-
# changed to better manage ENABLE_TRACING
41+
# changed to better manage ENABLE_TRACING (can be enabled from UI)
4242
import config
4343

4444
# Constant
@@ -142,13 +142,14 @@ def register_feedback():
142142

143143
st.sidebar.header("Options")
144144

145+
st.sidebar.text_input(label="Region", value=config.REGION, disabled=True)
146+
145147
# the collection used for semantic search
146148
st.session_state.collection_name = st.sidebar.selectbox(
147149
"Collection name",
148150
config.COLLECTION_LIST,
149151
)
150152

151-
# add the choice of LLM (not used for now)
152153
st.session_state.main_language = st.sidebar.selectbox(
153154
"Select the language for the answer",
154155
config.LANGUAGE_LIST,
@@ -157,6 +158,9 @@ def register_feedback():
157158
"Select the Chat Model",
158159
config.MODEL_LIST,
159160
)
161+
162+
st.sidebar.text_input(label="Embed Model", value=config.EMBED_MODEL_ID, disabled=True)
163+
160164
st.session_state.enable_reranker = st.sidebar.checkbox(
161165
"Enable Reranker", value=True, disabled=False
162166
)
@@ -203,11 +207,11 @@ def register_feedback():
203207
encoding=Encoding.V2_JSON,
204208
sample_rate=100,
205209
) as span:
206-
# loop to manage streaming
207210
# set the agent config
208211
agent_config = {
209212
"configurable": {
210213
"model_id": st.session_state.model_id,
214+
"embed_model_type": config.EMBED_MODEL_TYPE,
211215
"enable_reranker": st.session_state.enable_reranker,
212216
"enable_tracing": config.ENABLE_TRACING,
213217
"main_language": st.session_state.main_language,
@@ -219,6 +223,7 @@ def register_feedback():
219223
if config.DEBUG:
220224
logger.info("Agent config: %s", agent_config)
221225

226+
# loop to manage streaming
222227
for event in st.session_state.workflow.stream(
223228
input_state,
224229
config=agent_config,
@@ -248,13 +253,13 @@ def register_feedback():
248253
# Stream
249254
with st.chat_message(ASSISTANT):
250255
response_container = st.empty()
251-
full_response = ""
256+
FULL_RESPONSE = ""
252257

253258
for chunk in answer_generator:
254-
full_response += chunk.content
255-
response_container.markdown(full_response + "▌")
259+
FULL_RESPONSE += chunk.content
260+
response_container.markdown(FULL_RESPONSE + "▌")
256261

257-
response_container.markdown(full_response)
262+
response_container.markdown(FULL_RESPONSE)
258263

259264
elapsed_time = round((time.time() - time_start), 1)
260265
logger.info("Elapsed time: %s sec.", elapsed_time)
@@ -268,7 +273,7 @@ def register_feedback():
268273

269274
# Add user/assistant message to chat history
270275
add_to_chat_history(HumanMessage(content=question))
271-
add_to_chat_history(AIMessage(content=full_response))
276+
add_to_chat_history(AIMessage(content=FULL_RESPONSE))
272277

273278
# get the feedback
274279
if st.session_state.get_feedback:

ai/gen-ai-agents/custom-rag-agent/bm25_search.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,22 @@ def fetch_text_data(self):
5757
cursor.execute(query)
5858

5959
while True:
60-
rows = cursor.fetchmany(self.batch_size) # Fetch records in batches
60+
# Fetch records in batches
61+
rows = cursor.fetchmany(self.batch_size)
6162
if not rows:
62-
break # Exit loop when no more data
63+
# Exit loop when no more data
64+
break
6365

6466
for row in rows:
65-
lob_data = row[0] # This is a CLOB object
67+
# This is a CLOB object
68+
lob_data = row[0]
6669

6770
if isinstance(lob_data, oracledb.LOB):
68-
_results.append(lob_data.read()) # Read LOB content
71+
# Read LOB content
72+
_results.append(lob_data.read())
6973
else:
70-
_results.append(str(lob_data)) # Fallback for non-LOB data
74+
# Fallback for non-LOB data
75+
_results.append(str(lob_data))
7176

7277
return _results
7378

@@ -116,18 +121,33 @@ def search(self, query, top_n=5):
116121

117122
# Example Usage:
118123
# credential are packed in CONNECT_ARGS
119-
table_name = "BOOKS"
120-
text_column = "TEXT"
121124

122-
# create the index
123-
bm25_search = BM25OracleSearch(table_name, text_column)
124125

125-
questions = ["Chi è Luigi Saetta?", "What are the main innovation produced by GPT-4?"]
126+
def run_test():
127+
"""
128+
To run a quick test.
129+
"""
130+
table_name = "BOOKS"
131+
text_column = "TEXT"
132+
133+
# create the index
134+
bm25_search = BM25OracleSearch(table_name, text_column)
135+
136+
questions = [
137+
"Chi è Luigi Saetta?",
138+
"What are the main innovation produced by GPT-4?",
139+
]
140+
141+
for _question in questions:
142+
results = bm25_search.search(_question, top_n=2)
143+
144+
# Print search results
145+
for text, score in results:
146+
print(f"Score: {score:.2f} - Text: {text}")
147+
print("")
126148

127-
for _question in questions:
128-
results = bm25_search.search(_question, top_n=2)
129149

130-
# Print search results
131-
for text, score in results:
132-
print(f"Score: {score:.2f} - Text: {text}")
133-
print("")
150+
#
151+
# Main
152+
#
153+
run_test()

0 commit comments

Comments
 (0)