acastelan commited on
Commit
de651ab
·
1 Parent(s): a58185a

Add app files

Browse files
Files changed (9) hide show
  1. .gitignore +6 -0
  2. Dockerfile +14 -0
  3. README.md +19 -10
  4. __init__.py +0 -0
  5. app.py +116 -0
  6. chainlit.md +0 -0
  7. requirements.txt +9 -0
  8. searches.py +61 -0
  9. 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
- title: LibRAGentic Demo
3
- emoji: 👁
4
- colorFrom: indigo
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
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]