Spaces:
Sleeping
Sleeping
Add app files
Browse files- .gitignore +6 -0
- Dockerfile +14 -0
- README.md +19 -10
- __init__.py +0 -0
- app.py +116 -0
- chainlit.md +0 -0
- requirements.txt +9 -0
- searches.py +61 -0
- utils.py +5 -0
.gitignore
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.env
|
2 |
+
__pycache__/
|
3 |
+
.chainlit
|
4 |
+
*.faiss
|
5 |
+
*.pkl
|
6 |
+
.files
|
Dockerfile
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM python:3.9
|
2 |
+
RUN useradd -m -u 1000 user
|
3 |
+
ENV HOME=/home/user \
|
4 |
+
PATH=/home/user/.local/bin:$PATH
|
5 |
+
|
6 |
+
COPY ./requirements.txt ~/app/requirements.txt
|
7 |
+
WORKDIR $HOME/app
|
8 |
+
RUN chown -R user:user $HOME/app/
|
9 |
+
COPY --chown=user . $HOME/app/
|
10 |
+
USER user
|
11 |
+
RUN pip install -r requirements.txt
|
12 |
+
RUN pip install langchain-openai
|
13 |
+
COPY --chown=user . .
|
14 |
+
CMD ["chainlit", "run", "app.py", "--port", "7860"]
|
README.md
CHANGED
@@ -1,10 +1,19 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Welcome to the LibRAGentic Demo!
|
2 |
+
|
3 |
+
This is a helpful AI bot that analyzes novel reviews written by the general public. Currently, only Reddit and Goodreads.com reviews are supported
|
4 |
+
|
5 |
+
To use this app, prompt the AI with questions/statements structured similar to the following formats:
|
6 |
+
|
7 |
+
* What does [Reddit/Goodreads] think about "X" book?
|
8 |
+
* What is the overall sentiment of "X" from [Reddit/Goodreads]?
|
9 |
+
* Summarize the [praises/criticisms] about "X" from [Reddit/Goodreads]
|
10 |
+
* Highlight some [positive/negative] reviews about "X" from [Reddit/Goodreads]
|
11 |
+
|
12 |
+
Note that:
|
13 |
+
- You can simultaneously request reviews from both sites (replace the '/' with 'and')
|
14 |
+
- You can request specific subreddit searches, e.g., "What does the books subreddit think about "X" book
|
15 |
+
|
16 |
+
Future plans:
|
17 |
+
* Implement additional website support
|
18 |
+
* Analyze review comments
|
19 |
+
* Book recommendations
|
__init__.py
ADDED
File without changes
|
app.py
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import json
|
3 |
+
import chainlit as cl
|
4 |
+
|
5 |
+
from operator import itemgetter
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
|
8 |
+
from langchain_community.tools.ddg_search import DuckDuckGoSearchRun
|
9 |
+
from langchain_community.tools.reddit_search.tool import RedditSearchRun
|
10 |
+
from langchain_community.utilities.reddit_search import RedditSearchAPIWrapper
|
11 |
+
from langchain_openai import ChatOpenAI
|
12 |
+
from langchain_core.utils.function_calling import convert_to_openai_function
|
13 |
+
from langchain_core.messages import FunctionMessage, HumanMessage
|
14 |
+
from langchain.schema.runnable.config import RunnableConfig
|
15 |
+
from langchain.schema import StrOutputParser
|
16 |
+
|
17 |
+
from langgraph.prebuilt import ToolExecutor
|
18 |
+
from langgraph.prebuilt import ToolInvocation
|
19 |
+
from langgraph.graph import StateGraph, END
|
20 |
+
|
21 |
+
from searches import GoodReadsSearch
|
22 |
+
from utils import AgentState
|
23 |
+
|
24 |
+
async def call_model(state: AgentState, config: RunnableConfig):
|
25 |
+
messages = state["messages"]
|
26 |
+
response = await model.ainvoke(messages, config)
|
27 |
+
return {"messages" : [response]}
|
28 |
+
|
29 |
+
def call_tool(state):
|
30 |
+
last_message = state["messages"][-1]
|
31 |
+
|
32 |
+
action = ToolInvocation(
|
33 |
+
tool=last_message.additional_kwargs["function_call"]["name"],
|
34 |
+
tool_input=json.loads(
|
35 |
+
last_message.additional_kwargs["function_call"]["arguments"]
|
36 |
+
)
|
37 |
+
)
|
38 |
+
|
39 |
+
response = tool_executor.invoke(action)
|
40 |
+
|
41 |
+
function_message = FunctionMessage(content=str(response), name=action.tool)
|
42 |
+
|
43 |
+
return {"messages" : [function_message]}
|
44 |
+
|
45 |
+
def should_continue(state):
|
46 |
+
last_message = state["messages"][-1]
|
47 |
+
|
48 |
+
if "function_call" not in last_message.additional_kwargs:
|
49 |
+
return "end"
|
50 |
+
|
51 |
+
return "continue"
|
52 |
+
|
53 |
+
load_dotenv()
|
54 |
+
|
55 |
+
REDDIT_CLIENT_ID = os.environ["REDDIT_CLIENT_ID"]
|
56 |
+
REDDIT_CLIENT_SECRET = os.environ["REDDIT_CLIENT_SECRET"]
|
57 |
+
REDDIT_USER_AGENT = os.environ["REDDIT_USER_AGENT"]
|
58 |
+
OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]
|
59 |
+
|
60 |
+
tool_belt = [
|
61 |
+
DuckDuckGoSearchRun(),
|
62 |
+
RedditSearchRun(
|
63 |
+
api_wrapper=RedditSearchAPIWrapper(
|
64 |
+
reddit_client_id=REDDIT_CLIENT_ID,
|
65 |
+
reddit_client_secret=REDDIT_CLIENT_SECRET,
|
66 |
+
reddit_user_agent=REDDIT_USER_AGENT,
|
67 |
+
)
|
68 |
+
),
|
69 |
+
GoodReadsSearch()
|
70 |
+
]
|
71 |
+
|
72 |
+
tool_executor = ToolExecutor(tool_belt)
|
73 |
+
model = ChatOpenAI(model="gpt-4o-mini", temperature=0, streaming=True)
|
74 |
+
functions = [convert_to_openai_function(t) for t in tool_belt]
|
75 |
+
model = model.bind_functions(functions)
|
76 |
+
|
77 |
+
workflow = StateGraph(AgentState)
|
78 |
+
workflow.add_node("agent", call_model)
|
79 |
+
workflow.add_node("action", call_tool)
|
80 |
+
workflow.set_entry_point("agent")
|
81 |
+
workflow.add_conditional_edges(
|
82 |
+
"agent",
|
83 |
+
should_continue,
|
84 |
+
{
|
85 |
+
"continue" : "action",
|
86 |
+
"end" : END
|
87 |
+
}
|
88 |
+
)
|
89 |
+
workflow.add_edge("action", "agent")
|
90 |
+
|
91 |
+
app = workflow.compile()
|
92 |
+
|
93 |
+
@cl.on_chat_start
|
94 |
+
async def start_chat():
|
95 |
+
"""
|
96 |
+
"""
|
97 |
+
cl.user_session.set("agent", app)
|
98 |
+
|
99 |
+
@cl.on_message
|
100 |
+
async def main(message: cl.Message):
|
101 |
+
"""
|
102 |
+
"""
|
103 |
+
agent = cl.user_session.get("agent")
|
104 |
+
inputs = {"messages" : [HumanMessage(content=str(message.content))]}
|
105 |
+
cb = cl.LangchainCallbackHandler(stream_final_answer=True)
|
106 |
+
config = RunnableConfig(callbacks=[cb])
|
107 |
+
|
108 |
+
msg = cl.Message(content="")
|
109 |
+
await msg.send()
|
110 |
+
|
111 |
+
async for event in agent.astream_events(inputs, config=config, version="v1"):
|
112 |
+
kind = event["event"]
|
113 |
+
if kind == "on_chat_model_stream":
|
114 |
+
await msg.stream_token(event["data"]["chunk"].content)
|
115 |
+
|
116 |
+
await msg.update()
|
chainlit.md
ADDED
File without changes
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
chainlit==0.7.700
|
2 |
+
langchain_core==0.2.21
|
3 |
+
langchain==0.2.9
|
4 |
+
langchain_community==0.2.7
|
5 |
+
langgraph==0.1.8
|
6 |
+
beautifulsoup4==4.12.3
|
7 |
+
duckduckgo_search==6.2.1
|
8 |
+
praw==7.7.1
|
9 |
+
python-dotenv==1.0.1
|
searches.py
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Optional, Type
|
2 |
+
|
3 |
+
from langchain_core.callbacks import CallbackManagerForToolRun
|
4 |
+
from langchain_core.pydantic_v1 import BaseModel, Field
|
5 |
+
from langchain_core.tools import BaseTool
|
6 |
+
|
7 |
+
from urllib import request
|
8 |
+
from bs4 import BeautifulSoup
|
9 |
+
|
10 |
+
class GoodReadsSearch(BaseTool):
|
11 |
+
name: str = "GoodReads"
|
12 |
+
description: str = """
|
13 |
+
Used to search for user reviews on Goodreads
|
14 |
+
using a rating system of 1 to 5 stars,
|
15 |
+
where 1 and 2 stars are considered as negative reviews,
|
16 |
+
3 stars is considered as neutral reviews,
|
17 |
+
and 4 and 5 starts are considered as positive reviews.
|
18 |
+
"""
|
19 |
+
|
20 |
+
def fetchReviews(self, search_url: str) -> str:
|
21 |
+
response = request.urlopen(search_url).read().decode("utf-8")
|
22 |
+
soup = BeautifulSoup(response, 'html.parser')
|
23 |
+
|
24 |
+
content_url = ""
|
25 |
+
for attrs in soup.find_all('a'):
|
26 |
+
link = str(attrs.get("href"))
|
27 |
+
if link.startswith("/book/show"):
|
28 |
+
content_url = link
|
29 |
+
break
|
30 |
+
|
31 |
+
book_url = "https://www.goodreads.com"+content_url
|
32 |
+
book_response = request.urlopen(book_url).read().decode("utf-8")
|
33 |
+
book_soup = BeautifulSoup(book_response, 'html.parser')
|
34 |
+
|
35 |
+
rating_count = 0
|
36 |
+
is_review = False
|
37 |
+
reviews = ""
|
38 |
+
for attrs in book_soup.find_all('span'):
|
39 |
+
if is_review:
|
40 |
+
review = str(attrs.get("class"))
|
41 |
+
if "Formatted" in review:
|
42 |
+
reviews += attrs.get_text() + "\n\n"
|
43 |
+
is_review = False
|
44 |
+
else:
|
45 |
+
rating = str(attrs.get("aria-label"))
|
46 |
+
if rating.startswith("Rating") and int(rating[7]) > 0 and rating[8]==" ":
|
47 |
+
reviews += rating +"\n"
|
48 |
+
rating_count+=1
|
49 |
+
is_review = True
|
50 |
+
return reviews
|
51 |
+
|
52 |
+
def _run(
|
53 |
+
self,
|
54 |
+
query: str,
|
55 |
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
56 |
+
) -> str:
|
57 |
+
"""Use the Goodreads tool."""
|
58 |
+
base_url = "https://www.goodreads.com/search?q="
|
59 |
+
book_title = query.replace(" ","+")
|
60 |
+
response = self.fetchReviews((base_url+book_title))
|
61 |
+
return response
|
utils.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import TypedDict, Annotated
|
2 |
+
from langgraph.graph.message import add_messages
|
3 |
+
|
4 |
+
class AgentState(TypedDict):
|
5 |
+
messages: Annotated[list, add_messages]
|