Skip to content

Commit d00f63c

Browse files
committed
chore: add RAG sample
1 parent 4d0ee52 commit d00f63c

File tree

8 files changed

+6651
-0
lines changed

8 files changed

+6651
-0
lines changed

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: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
from typing import Optional, List, Literal, Union
2+
from langgraph.graph import END, START, MessagesState, StateGraph
3+
from langgraph.types import Command, interrupt
4+
from pydantic import BaseModel, Field, validator
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, SystemMessage
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+
@validator("quiz", always=True)
42+
def check_quiz(cls, v, values):
43+
if values.get("additional_info") == "false" and v is None:
44+
raise ValueError("Quiz should be None when additional_info is not 'false'")
45+
return v
46+
47+
output_parser = PydanticOutputParser(pydantic_object=QuizOrInsufficientInfo)
48+
49+
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 knowledge.
50+
If the documents do not provide enough info, respond with additional_info=<information that is required>.
51+
If they provide enough info, create the quiz and set additional_info='false'
52+
53+
{context}
54+
55+
{format_instructions}
56+
57+
Respond with the classification in the requested JSON format."""
58+
59+
uipath = UiPath()
60+
61+
62+
class GraphOutput(BaseModel):
63+
quiz: Quiz
64+
65+
class GraphInput(BaseModel):
66+
general_category: str
67+
quiz_topic: str
68+
bucket_name: str
69+
index_name: str
70+
bucket_folder: Optional[str]
71+
72+
class GraphState(MessagesState):
73+
general_category: str
74+
quiz_topic: str
75+
bucket_name: str
76+
bucket_folder: Optional[str]
77+
index_name: str
78+
additional_info: Optional[bool]
79+
quiz: Optional[Quiz]
80+
81+
def prepare_input(state: GraphInput) -> GraphState:
82+
return GraphState(
83+
quiz_topic=state.quiz_topic,
84+
bucket_name=state.bucket_name,
85+
index_name=state.index_name,
86+
general_category=state.general_category,
87+
additional_info="false",
88+
)
89+
90+
async def invoke_researcher(state: GraphState) -> Command:
91+
print("INVOKE RESEARCHER")
92+
if state.get("additional_info", None) != "false":
93+
state["messages"].append(HumanMessage(f"{state['additional_info']}")),
94+
else:
95+
state["messages"].append(HumanMessage(f"Fetch data about {state['general_category']}")),
96+
input_args_json = {
97+
"messages": state["messages"],
98+
"bucket_name": state["bucket_name"],
99+
"bucket_folder": state.get("bucket_folder", None),
100+
}
101+
agent_response = interrupt(InvokeProcess(
102+
name = "researcher-and-uploader-agent",
103+
input_arguments = input_args_json,
104+
))
105+
quiz_topic = state["quiz_topic"]
106+
return Command(
107+
update={
108+
"messages": [agent_response["messages"][-1], ("user", f"create a quiz about {quiz_topic}")],
109+
})
110+
111+
async def create_quiz(state: GraphState) -> Command:
112+
print("CREATE QUIZ")
113+
no_of_retries = 5
114+
context_data = None
115+
index = uipath.context_grounding.get_or_create_index(state["index_name"],storage_bucket_name=state["bucket_name"])
116+
uipath.context_grounding.ingest_data(index)
117+
while no_of_retries != 0:
118+
try:
119+
context_data = ContextGroundingRetriever(
120+
index_name=state["index_name"],
121+
uipath_sdk=uipath,
122+
number_of_results=10
123+
).invoke(state["quiz_topic"])
124+
break
125+
except IngestionInProgressException as ex:
126+
logger.info(ex.message)
127+
no_of_retries -= 1
128+
logger.info(f"{no_of_retries} retries left")
129+
time.sleep(5)
130+
if not context_data:
131+
raise Exception("Ingestion is taking too long!")
132+
133+
# state["messages"].append(SystemMessage(system_message.format(format_instructions=output_parser.get_format_instructions(),
134+
# context= context_data)))
135+
print("INVOKE LLM")
136+
message= system_message.format(format_instructions=output_parser.get_format_instructions(),
137+
context= context_data,
138+
quiz_topic=state["quiz_topic"])
139+
print(message)
140+
result = llm.invoke(message)
141+
try:
142+
llm_response = output_parser.parse(result.content)
143+
print("LLM RESPONSE")
144+
print(llm_response)
145+
print("CONTEXT DATA")
146+
print(context_data)
147+
return Command(
148+
update={
149+
"quiz": llm_response.quiz if llm_response.additional_info == "false" else None,
150+
"additional_info": llm_response.additional_info,
151+
}
152+
)
153+
except Exception as e:
154+
print(f"Failed to parse {e}")
155+
return Command(goto=END)
156+
157+
def check_quiz_creation(state: GraphState) -> Literal["invoke_researcher", "return_quiz"]:
158+
print("CHECK QUIZ CREATION")
159+
print(state["additional_info"])
160+
if state["additional_info"] != "false":
161+
return "invoke_researcher"
162+
return "return_quiz"
163+
164+
def return_quiz(state: GraphState) -> GraphOutput:
165+
# print("RETURN QUIZ")
166+
# print(state["quiz"])
167+
return GraphOutput( quiz=state["quiz"])
168+
169+
# Build the state graph
170+
builder = StateGraph(input=GraphInput, output=GraphOutput)
171+
builder.add_node("invoke_researcher", invoke_researcher)
172+
builder.add_node("create_quiz", create_quiz)
173+
builder.add_node("return_quiz", return_quiz)
174+
builder.add_node("prepare_input", prepare_input)
175+
176+
builder.add_edge(START, "prepare_input")
177+
builder.add_edge("prepare_input", "invoke_researcher")
178+
builder.add_edge("invoke_researcher", "create_quiz")
179+
builder.add_conditional_edges("create_quiz", check_quiz_creation)
180+
builder.add_edge("return_quiz", END)
181+
182+
# Compile the graph
183+
graph = builder.compile()
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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 HumanMessage, AIMessage
10+
11+
12+
tavily_tool = TavilySearchResults(max_results=5)
13+
14+
llm = ChatAnthropic(model="claude-3-5-sonnet-latest")
15+
16+
uipath = UiPath()
17+
research_agent = create_react_agent(
18+
llm, tools=[tavily_tool], prompt="You are a researcher. Search relevant information given the user topic. Don't do summarizations. Retrieve raw data."
19+
)
20+
21+
class GraphInput(MessagesState):
22+
bucket_name: str
23+
bucket_folder: Optional[str]
24+
25+
class GraphState(MessagesState):
26+
web_results: str
27+
bucket_name: str
28+
bucket_folder: Optional[str]
29+
30+
def prepare_input(state: GraphInput) -> GraphState:
31+
return GraphState(
32+
messages=state["messages"],
33+
web_results="",
34+
bucket_name=state["bucket_name"],
35+
bucket_folder=state.get("bucket_folder",None),
36+
)
37+
38+
def research_node(state: GraphState) -> Command:
39+
result = research_agent.invoke(state)
40+
web_results = result["messages"][-1].content
41+
return Command(
42+
update={
43+
"web_results": web_results,
44+
})
45+
46+
def upload_to_bucket(state: GraphState) -> MessagesState:
47+
#TODO: also include a question summarization as document name
48+
current_timestamp = int(time.time())
49+
uipath.buckets.upload_from_memory(
50+
bucket_name=state["bucket_name"],
51+
blob_file_path=f"{current_timestamp}.txt",
52+
content_type="application/txt",
53+
content=state["web_results"],)
54+
return MessagesState(messages=[AIMessage("Relevant information uploaded to bucket.")])
55+
56+
57+
# Build the state graph
58+
builder = StateGraph(input=GraphInput, output=MessagesState)
59+
builder.add_node("researcher", research_node)
60+
builder.add_node("upload_to_bucket", upload_to_bucket)
61+
builder.add_node("prepare_input", prepare_input)
62+
63+
builder.add_edge(START, "prepare_input")
64+
builder.add_edge("prepare_input", "researcher")
65+
builder.add_edge("researcher", "upload_to_bucket")
66+
builder.add_edge("upload_to_bucket", END)
67+
68+
# Compile the graph
69+
graph = builder.compile()

0 commit comments

Comments
 (0)