Skip to content

Commit fb26a5f

Browse files
committed
chore: add RAG sample
1 parent 86343b7 commit fb26a5f

8 files changed

Lines changed: 6662 additions & 0 deletions

File tree

samples/RAG-sample/langgraph.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"dependencies": ["."],
3+
"graphs": {
4+
"researcher-and-uploader-agent": "./src/agents/researcher-and-uploader.py:graph",
5+
"quiz-generator-RAG-agent": "./src/agents/quiz-generator-RAG.py:graph"
6+
},
7+
"env": ".env"
8+
}

samples/RAG-sample/pyproject.toml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
[project]
2+
name = "RAG-agents"
3+
version = "0.0.6"
4+
description = "Package containing 2 agents. The first one crawls the internet and adds relevant information to a storage bucket, the first one generates quizzes based on the gathered info and user input."
5+
authors = [
6+
{ name = "Radu Mocanu" }
7+
]
8+
requires-python = ">=3.10"
9+
dependencies = [
10+
"langgraph>=0.2.55",
11+
"langchain-community>=0.3.9",
12+
"langchain-anthropic>=0.3.8",
13+
"langchain-experimental>=0.3.4",
14+
"tavily-python>=0.5.0",
15+
"uipath==2.0.1",
16+
"uipath-langchain==0.0.87"
17+
]
18+
19+
[project.optional-dependencies]
20+
dev = ["mypy>=1.11.1", "ruff>=0.6.1"]
21+
22+
[build-system]
23+
requires = ["setuptools>=73.0.0", "wheel"]
24+
build-backend = "setuptools.build_meta"
25+
26+
[tool.setuptools.package-data]
27+
"*" = ["py.typed"]
28+
29+
[tool.ruff]
30+
lint.select = [
31+
"E", # pycodestyle
32+
"F", # pyflakes
33+
"I", # isort
34+
"D", # pydocstyle
35+
"D401", # First line should be in imperative mood
36+
"T201",
37+
"UP",
38+
]
39+
lint.ignore = [
40+
"UP006",
41+
"UP007",
42+
"UP035",
43+
"D417",
44+
"E501",
45+
]
46+
47+
[tool.ruff.lint.per-file-ignores]
48+
"tests/*" = ["D", "UP"]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
config:
3+
flowchart:
4+
curve: linear
5+
---
6+
graph TD;
7+
__start__([<p>__start__</p>]):::first
8+
invoke_researcher(invoke_researcher)
9+
create_quiz(create_quiz)
10+
__end__([<p>__end__</p>]):::last
11+
__start__ --> invoke_researcher;
12+
create_quiz --> __end__;
13+
invoke_researcher --> create_quiz;
14+
classDef default fill:#f2f0ff,line-height:1.2
15+
classDef first fill-opacity:0
16+
classDef last fill:#bfb6fc
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
config:
3+
flowchart:
4+
curve: linear
5+
---
6+
graph TD;
7+
__start__([<p>__start__</p>]):::first
8+
upload_to_bucket(upload_to_bucket)
9+
prepare_input(prepare_input)
10+
__end__([<p>__end__</p>]):::last
11+
__start__ --> prepare_input;
12+
prepare_input --> researcher___start__;
13+
researcher___end__ --> upload_to_bucket;
14+
upload_to_bucket --> __end__;
15+
subgraph researcher
16+
researcher___start__(<p>__start__</p>)
17+
researcher_agent(agent)
18+
researcher_tools(tools)
19+
researcher___end__(<p>__end__</p>)
20+
researcher___start__ --> researcher_agent;
21+
researcher_tools --> researcher_agent;
22+
researcher_agent -.-> researcher_tools;
23+
researcher_agent -.-> researcher___end__;
24+
end
25+
classDef default fill:#f2f0ff,line-height:1.2
26+
classDef first fill-opacity:0
27+
classDef last fill:#bfb6fc
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
from typing import Optional, List, Literal
2+
from langgraph.graph import END, START, MessagesState, StateGraph
3+
from langgraph.types import Command, interrupt
4+
from pydantic import BaseModel, Field, field_validator, ValidationInfo
5+
from uipath import UiPath
6+
from langchain_core.output_parsers import PydanticOutputParser
7+
import logging
8+
import time
9+
from uipath.models import InvokeProcess, IngestionInProgressException
10+
from langchain_core.messages import HumanMessage
11+
from uipath_langchain.retrievers import ContextGroundingRetriever
12+
from langchain_anthropic import ChatAnthropic
13+
14+
15+
logger = logging.getLogger(__name__)
16+
17+
llm = ChatAnthropic(model="claude-3-5-sonnet-latest")
18+
19+
class QuizItem(BaseModel):
20+
question: str = Field(
21+
description="One quiz question"
22+
)
23+
difficulty: float = Field(
24+
description="How difficult is the question", ge=0.0, le=1.0
25+
)
26+
answer: str = Field(
27+
description="The expected answer to the question",
28+
)
29+
class Quiz(BaseModel):
30+
quiz_items: List[QuizItem] = Field(
31+
description="A list of quiz items"
32+
)
33+
class QuizOrInsufficientInfo(BaseModel):
34+
quiz: Optional[Quiz] = Field(
35+
description="A quiz based on user input and available documents."
36+
)
37+
additional_info: Optional[str] = Field(
38+
description="String that controls whether additional information is required",
39+
)
40+
41+
@field_validator("quiz")
42+
def check_quiz(cls, v, info: ValidationInfo):
43+
additional_info = info.data.get("additional_info")
44+
if additional_info == "false" and v is None:
45+
raise ValueError("Quiz should be None when additional_info is not 'false'")
46+
return v
47+
48+
output_parser = PydanticOutputParser(pydantic_object=QuizOrInsufficientInfo)
49+
50+
system_message ="""You are a quiz generator. Try to generate a quiz about {quiz_topic} with multiple questions ONLY based on the following documents. Do not use any extra information from your knowledgebase.
51+
If the documents do not provide enough info, respond with as little words as possible in the format 'additional_info=Need data about ...'. The additional_info should be around 10-15 words.
52+
If they provide enough info, create the quiz and set additional_info='false'
53+
54+
This is the context data: {context}
55+
56+
{format_instructions}
57+
58+
Respond with the classification in the requested JSON format."""
59+
60+
uipath = UiPath()
61+
62+
63+
class GraphOutput(BaseModel):
64+
quiz: Quiz
65+
66+
class GraphInput(BaseModel):
67+
quiz_topic: str
68+
bucket_name: str
69+
index_name: str
70+
bucket_folder: Optional[str] = None
71+
72+
class GraphState(MessagesState):
73+
quiz_topic: str
74+
bucket_name: str
75+
bucket_folder: Optional[str]
76+
index_name: str
77+
additional_info: Optional[bool]
78+
quiz: Optional[Quiz]
79+
80+
def prepare_input(state: GraphInput) -> GraphState:
81+
return GraphState(
82+
quiz_topic=state.quiz_topic,
83+
bucket_name=state.bucket_name,
84+
index_name=state.index_name,
85+
additional_info="false",
86+
messages=("user", f"create a quiz about {state.quiz_topic}"),
87+
bucket_folder=state.bucket_folder,
88+
)
89+
90+
async def invoke_researcher(state: GraphState) -> Command:
91+
state["messages"].append(HumanMessage(f"{state['additional_info']}")),
92+
93+
input_args_json = {
94+
"messages": state["messages"],
95+
"bucket_name": state["bucket_name"],
96+
"bucket_folder": state.get("bucket_folder", None),
97+
}
98+
agent_response = interrupt(InvokeProcess(
99+
name = "researcher-and-uploader-agent",
100+
input_arguments = input_args_json,
101+
))
102+
103+
return Command(
104+
update={
105+
"messages": [agent_response["messages"][-1]],
106+
})
107+
108+
async def create_quiz(state: GraphState) -> Command:
109+
no_of_retries = 5
110+
context_data = None
111+
data_queried = False
112+
index = uipath.context_grounding.get_or_create_index(state["index_name"], storage_bucket_name=state["bucket_name"], storage_bucket_folder_path=state["bucket_folder"])
113+
uipath.context_grounding.ingest_data(index)
114+
while no_of_retries != 0:
115+
try:
116+
context_data = await ContextGroundingRetriever(
117+
index_name=state["index_name"],
118+
uipath_sdk=uipath,
119+
number_of_results=10
120+
).ainvoke(state["quiz_topic"])
121+
data_queried = True
122+
break
123+
except IngestionInProgressException as ex:
124+
logger.info(ex.message)
125+
no_of_retries -= 1
126+
logger.info(f"{no_of_retries} retries left")
127+
time.sleep(5)
128+
if not data_queried:
129+
raise Exception("Ingestion is taking too long.")
130+
message = system_message.format(format_instructions=output_parser.get_format_instructions(),
131+
context = context_data if context_data else "No context available yet",
132+
quiz_topic=state["quiz_topic"])
133+
result = llm.invoke(message)
134+
try:
135+
llm_response = output_parser.parse(result.content)
136+
return Command(
137+
update={
138+
"quiz": llm_response.quiz if llm_response.additional_info == "false" else None,
139+
"additional_info": llm_response.additional_info,
140+
}
141+
)
142+
except Exception as e:
143+
print(f"Failed to parse {e}")
144+
return Command(goto=END)
145+
146+
def check_quiz_creation(state: GraphState) -> Literal["invoke_researcher", "return_quiz"]:
147+
if state["additional_info"] != "false":
148+
return "invoke_researcher"
149+
return "return_quiz"
150+
151+
def return_quiz(state: GraphState) -> GraphOutput:
152+
return GraphOutput(quiz=state["quiz"])
153+
154+
# Build the state graph
155+
builder = StateGraph(input=GraphInput, output=GraphOutput)
156+
builder.add_node("invoke_researcher", invoke_researcher)
157+
builder.add_node("create_quiz", create_quiz)
158+
builder.add_node("return_quiz", return_quiz)
159+
builder.add_node("prepare_input", prepare_input)
160+
161+
builder.add_edge(START, "prepare_input")
162+
builder.add_edge("prepare_input", "create_quiz")
163+
builder.add_conditional_edges("create_quiz", check_quiz_creation)
164+
builder.add_edge("invoke_researcher", "create_quiz")
165+
builder.add_edge("return_quiz", END)
166+
167+
# Compile the graph
168+
graph = builder.compile()
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from typing import Optional
2+
import time
3+
from langchain_anthropic import ChatAnthropic
4+
from langchain_community.tools.tavily_search import TavilySearchResults
5+
from langgraph.graph import END, START, MessagesState, StateGraph
6+
from langgraph.prebuilt import create_react_agent
7+
from langgraph.types import Command
8+
from uipath import UiPath
9+
from langchain_core.messages import AIMessage, SystemMessage
10+
11+
uipath = UiPath()
12+
tavily_tool = TavilySearchResults(max_results=5)
13+
anthropic_model = "claude-3-5-sonnet-latest"
14+
15+
16+
llm = ChatAnthropic(model=anthropic_model)
17+
18+
research_agent = create_react_agent(
19+
llm, tools=[tavily_tool], prompt="You are a researcher. Search relevant information given the user topic. Don't do summarizations. Retrieve raw, unstructured data."
20+
)
21+
22+
class GraphInput(MessagesState):
23+
bucket_name: str
24+
bucket_folder: Optional[str]
25+
26+
class GraphState(MessagesState):
27+
web_results: str
28+
file_name: Optional[str]
29+
bucket_name: str
30+
bucket_folder: Optional[str]
31+
32+
def prepare_input(state: GraphInput) -> GraphState:
33+
return GraphState(
34+
messages=state["messages"],
35+
web_results="",
36+
bucket_name=state["bucket_name"],
37+
bucket_folder=state.get("bucket_folder",None),
38+
file_name=None,
39+
)
40+
41+
async def research_node(state: GraphState) -> Command:
42+
result = await research_agent.ainvoke(state)
43+
web_results = result["messages"][-1].content
44+
return Command(
45+
update={
46+
"web_results": web_results,
47+
"file_name": state["file_name"],
48+
})
49+
50+
async def create_file_name(state: GraphState) -> GraphState:
51+
file_name = await llm.ainvoke(
52+
[SystemMessage(
53+
"""
54+
You are a message summarizer.
55+
Generate a file name from the received message, replacing spaces with underscores,
56+
to create a succinct and descriptive identification.
57+
For instance, 'Need data about formula 1' should be converted to format like 'data_about_formula_1'.
58+
"""
59+
),
60+
state['messages'][-1]])
61+
return GraphState(
62+
messages=state["messages"],
63+
web_results="",
64+
bucket_name=state["bucket_name"],
65+
bucket_folder=state.get("bucket_folder", None),
66+
file_name=file_name.content,
67+
)
68+
69+
70+
def upload_to_bucket(state: GraphState) -> MessagesState:
71+
current_timestamp = int(time.time())
72+
file_name = state["file_name"]
73+
uipath.buckets.upload_from_memory(
74+
bucket_name=state["bucket_name"],
75+
blob_file_path=f"{file_name}-{current_timestamp}.txt",
76+
content_type="application/txt",
77+
content=state["web_results"],)
78+
return MessagesState(messages=[AIMessage("Relevant information uploaded to bucket.")])
79+
80+
81+
# Build the state graph
82+
builder = StateGraph(input=GraphInput, output=MessagesState)
83+
builder.add_node("researcher", research_node)
84+
builder.add_node("upload_to_bucket", upload_to_bucket)
85+
builder.add_node("prepare_input", prepare_input)
86+
builder.add_node("create_file_name", create_file_name)
87+
88+
builder.add_edge(START, "prepare_input")
89+
builder.add_edge("prepare_input", "create_file_name")
90+
builder.add_edge("create_file_name", "researcher")
91+
builder.add_edge("researcher", "upload_to_bucket")
92+
builder.add_edge("upload_to_bucket", END)
93+
94+
# Compile the graph
95+
graph = builder.compile()

0 commit comments

Comments
 (0)