ernestobs7 commited on
Commit
236fcc8
·
1 Parent(s): 80733cd

Fix: Install system dependencies as root before switching user

Browse files
Files changed (3) hide show
  1. Dockerfile +5 -9
  2. app.py +162 -108
  3. pyproject.toml +24 -23
Dockerfile CHANGED
@@ -1,10 +1,8 @@
1
  # Get a distribution that has uv already installed
2
  FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
3
 
4
- # Install required system dependencies for building packages (as root)
5
- RUN apt-get update && apt-get install -y gcc g++ python3-dev
6
-
7
  # Add user - this is the user that will run the app
 
8
  RUN useradd -m -u 1000 user
9
  USER user
10
 
@@ -20,14 +18,12 @@ WORKDIR $HOME/app
20
  # Copy the app to the container
21
  COPY --chown=user . $HOME/app
22
 
23
- # Copy dependencies separately
24
- COPY pyproject.toml .
25
-
26
- # Install dependencies
27
- RUN uv pip install --system --requirements pyproject.toml
28
 
29
  # Expose the port
30
  EXPOSE 7860
31
 
32
  # Run the app
33
- CMD ["chainlit", "run", "app.py", "--host", "0.0.0.0", "--port", "7860"]
 
1
  # Get a distribution that has uv already installed
2
  FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
3
 
 
 
 
4
  # Add user - this is the user that will run the app
5
+ # If you do not set user, the app will run as root (undesirable)
6
  RUN useradd -m -u 1000 user
7
  USER user
8
 
 
18
  # Copy the app to the container
19
  COPY --chown=user . $HOME/app
20
 
21
+ # Install the dependencies
22
+ # RUN uv sync --frozen
23
+ RUN uv sync
 
 
24
 
25
  # Expose the port
26
  EXPOSE 7860
27
 
28
  # Run the app
29
+ CMD ["uv", "run", "chainlit", "run", "app.py", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -1,139 +1,193 @@
1
- import os
2
- from typing import List
3
- from chainlit.types import AskFileResponse
4
- from aimakerspace.text_utils import CharacterTextSplitter, TextFileLoader, PDFLoader
5
- from aimakerspace.openai_utils.prompts import (
6
- UserRolePrompt,
7
- SystemRolePrompt,
8
- AssistantRolePrompt,
9
- )
10
- from aimakerspace.openai_utils.embedding import EmbeddingModel
11
- from aimakerspace.vectordatabase import VectorDatabase
12
- from aimakerspace.openai_utils.chatmodel import ChatOpenAI
13
  import chainlit as cl
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- system_template = """\
16
- Use the following context to answer a users question. If you cannot find the answer in the context, say you don't know the answer."""
17
- system_role_prompt = SystemRolePrompt(system_template)
18
 
19
- user_prompt_template = """\
20
- Context:
 
 
 
 
 
21
  {context}
22
 
23
- Question:
24
- {question}
25
- """
26
- user_role_prompt = UserRolePrompt(user_prompt_template)
27
 
28
- class RetrievalAugmentedQAPipeline:
29
- def __init__(self, llm: ChatOpenAI(), vector_db_retriever: VectorDatabase) -> None:
30
- self.llm = llm
31
- self.vector_db_retriever = vector_db_retriever
32
 
33
- async def arun_pipeline(self, user_query: str):
34
- context_list = self.vector_db_retriever.search_by_text(user_query, k=4)
35
 
36
- context_prompt = ""
37
- for context in context_list:
38
- context_prompt += context[0] + "\n"
 
 
39
 
40
- formatted_system_prompt = system_role_prompt.create_message()
 
 
 
41
 
42
- formatted_user_prompt = user_role_prompt.create_message(question=user_query, context=context_prompt)
 
 
43
 
44
- async def generate_response():
45
- async for chunk in self.llm.astream([formatted_system_prompt, formatted_user_prompt]):
46
- yield chunk
 
 
 
 
 
47
 
48
- return {"response": generate_response(), "context": context_list}
49
 
50
- text_splitter = CharacterTextSplitter()
 
 
 
51
 
 
52
 
53
- def process_file(file: AskFileResponse):
54
- import tempfile
55
- import shutil
56
-
57
- print(f"Processing file: {file.name}")
58
-
59
- # Create a temporary file with the correct extension
60
- suffix = f".{file.name.split('.')[-1]}"
61
- with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
62
- # Copy the uploaded file content to the temporary file
63
- shutil.copyfile(file.path, temp_file.name)
64
- print(f"Created temporary file at: {temp_file.name}")
65
-
66
- # Create appropriate loader
67
- if file.name.lower().endswith('.pdf'):
68
- loader = PDFLoader(temp_file.name)
69
- else:
70
- loader = TextFileLoader(temp_file.name)
71
-
72
- try:
73
- # Load and process the documents
74
- documents = loader.load_documents()
75
- texts = text_splitter.split_texts(documents)
76
- return texts
77
- finally:
78
- # Clean up the temporary file
79
- try:
80
- os.unlink(temp_file.name)
81
- except Exception as e:
82
- print(f"Error cleaning up temporary file: {e}")
83
 
 
 
84
 
85
- @cl.on_chat_start
86
- async def on_chat_start():
87
- files = None
 
88
 
89
- # Wait for the user to upload a file
90
- while files == None:
91
- files = await cl.AskFileMessage(
92
- content="Please upload a Text or PDF file to begin!",
93
- accept=["text/plain", "application/pdf"],
94
- max_size_mb=2,
95
- timeout=180,
96
- ).send()
97
 
98
- file = files[0]
99
 
100
- msg = cl.Message(
101
- content=f"Processing `{file.name}`..."
102
- )
103
- await msg.send()
104
 
105
- # load the file
106
- texts = process_file(file)
107
 
108
- print(f"Processing {len(texts)} text chunks")
 
 
109
 
110
- # Create a dict vector store
111
- vector_db = VectorDatabase()
112
- vector_db = await vector_db.abuild_from_list(texts)
113
-
114
- chat_openai = ChatOpenAI()
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- # Create a chain
117
- retrieval_augmented_qa_pipeline = RetrievalAugmentedQAPipeline(
118
- vector_db_retriever=vector_db,
119
- llm=chat_openai
120
- )
121
-
122
- # Let the user know that the system is ready
123
- msg.content = f"Processing `{file.name}` done. You can now ask questions!"
124
- await msg.update()
125
 
126
- cl.user_session.set("chain", retrieval_augmented_qa_pipeline)
127
 
128
 
 
 
 
 
129
  @cl.on_message
130
- async def main(message):
131
- chain = cl.user_session.get("chain")
 
 
 
 
 
132
 
133
- msg = cl.Message(content="")
134
- result = await chain.arun_pipeline(message.content)
135
 
136
- async for stream_resp in result["response"]:
137
- await msg.stream_token(stream_resp)
138
 
139
- await msg.send()
 
1
+ from typing import TypedDict, Annotated, List
2
+ from typing_extensions import List, TypedDict
3
+
4
+ from dotenv import load_dotenv
 
 
 
 
 
 
 
 
5
  import chainlit as cl
6
+ import operator
7
+
8
+ from langchain.prompts import ChatPromptTemplate
9
+ from langchain.retrievers.contextual_compression import ContextualCompressionRetriever
10
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
11
+ from langchain_community.document_loaders import DirectoryLoader, BSHTMLLoader, PyPDFLoader
12
+ from sentence_transformers import SentenceTransformer
13
+ from langchain_community.tools.arxiv.tool import ArxivQueryRun
14
+ from langchain_community.tools.tavily_search import TavilySearchResults
15
+ from langchain_core.documents import Document
16
+ from langchain_core.messages import BaseMessage, HumanMessage
17
+ from langchain_core.tools import tool
18
+ from langchain_openai import ChatOpenAI, OpenAIEmbeddings
19
+ from langchain_qdrant import QdrantVectorStore
20
+ from langgraph.graph import START, StateGraph, END
21
+ from langgraph.graph.message import add_messages
22
+ from langgraph.prebuilt import ToolNode
23
+ from qdrant_client import QdrantClient
24
+ from qdrant_client.http.models import Distance, VectorParams
25
+ from langchain_core.prompts import PromptTemplate
26
+ from langchain.embeddings import SentenceTransformerEmbeddings
27
+ from langchain_huggingface import HuggingFaceEmbeddings
28
+
29
+ load_dotenv()
30
+
31
+ openai_chat_model = ChatOpenAI(model="gpt-4o-mini")
32
+
33
+ path = "data/"
34
+
35
+ # Load HTML files
36
+ html_loader = DirectoryLoader(path, glob="*.html", loader_cls=BSHTMLLoader)
37
+ html_docs = html_loader.load()
38
+
39
+ # Load PDF files
40
+ pdf_loader = DirectoryLoader(path, glob="*.pdf", loader_cls=PyPDFLoader)
41
+ pdf_docs = pdf_loader.load()
42
+
43
+ # Combine both document lists
44
+ docs = html_docs + pdf_docs
45
+
46
+ # Split documents into chunks
47
+ text_splitter = RecursiveCharacterTextSplitter(
48
+ chunk_size = 850,
49
+ chunk_overlap = 50,
50
+ length_function = len
51
+ )
52
+ split_documents = text_splitter.split_documents(docs)
53
+
54
+
55
+ # Load your fine-tuned model
56
+
57
+ finetune_embeddings = HuggingFaceEmbeddings(model_name="finetuned_caregiver_ft")
58
+
59
+ client = QdrantClient(":memory:")
60
+
61
+ client.create_collection(
62
+ collection_name="ai_across_years2",
63
+ vectors_config=VectorParams(size=1024, distance=Distance.COSINE),
64
+ )
65
+
66
+ vector_store = QdrantVectorStore(
67
+ client=client,
68
+ collection_name="ai_across_years2",
69
+ embedding=finetune_embeddings,
70
+ )
71
+
72
+ _ = vector_store.add_documents(documents=split_documents)
73
+
74
+
75
+ retriever = vector_store.as_retriever(search_kwargs={"k": 5})
76
+
77
+
78
+ def retrieve(state):
79
+ retrieved_docs = retriever.invoke(state["question"])
80
+ return {"context": retrieved_docs}
81
 
 
 
 
82
 
83
+ #####
84
+
85
+ RAG_template = """
86
+ Use the following pieces of context to answer the question at the end.
87
+ If you don't know the answer, just say "I don't know, would you like to talk to a care coach?", don't try to make up an answer.
88
+ Use three sentences maximum and keep the answer as concise as possible.
89
+
90
  {context}
91
 
92
+ Question: {question}
 
 
 
93
 
94
+ Helpful Answer:"""
 
 
 
95
 
96
+ rag_prompt = PromptTemplate.from_template(RAG_template)
 
97
 
98
+ def generate(state):
99
+ docs_content = "\n\n".join([doc.page_content for doc in state["context"]])
100
+ messages = rag_prompt.format_prompt(context=docs_content, question=state["question"])
101
+ respose = openai_chat_model.invoke(messages)
102
+ return {"response": respose.content}
103
 
104
+ class State(TypedDict):
105
+ question: str
106
+ context: List[Document]
107
+ response: str
108
 
109
+ graph_builder = StateGraph(State).add_sequence([retrieve, generate])
110
+ graph_builder.add_edge(START, "retrieve");
111
+ graph = graph_builder.compile();
112
 
113
+ @tool
114
+ def ai_rag_tool(question: str) -> str:
115
+ """Answer questions about ALS based on the retrieved documents. Input should be a fully formed question."""
116
+ response = graph.invoke({"question": question})
117
+ return{
118
+ "messages": [HumanMessage(content=response["response"])],
119
+ "context": response["context"],
120
+ }
121
 
122
+ tavily_tool = TavilySearchResults(max_results=5)
123
 
124
+ tool_belt = [
125
+ tavily_tool,
126
+ ai_rag_tool,
127
+ ]
128
 
129
+ model = ChatOpenAI(model="gpt-4o", temperature=0)
130
 
131
+ model = model.bind_tools(tool_belt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
+ class AgentState(TypedDict):
134
+ messages: Annotated[list, add_messages]
135
 
136
+ def call_model(state):
137
+ messages = state["messages"]
138
+ response = model.invoke(messages, config={"tool_choice": "auto"}) # Ensure it knows it can use tools
139
+ return {"messages": [response]}
140
 
141
+ tool_node = ToolNode(tool_belt)
 
 
 
 
 
 
 
142
 
143
+ uncompiled_graph = StateGraph(AgentState)
144
 
145
+ uncompiled_graph.add_node("agent", call_model)
146
+ uncompiled_graph.add_node("action", tool_node)
 
 
147
 
148
+ uncompiled_graph.set_entry_point("agent")
 
149
 
150
+ def should_continue(state):
151
+ last_message = state["messages"][-1]
152
+ print(f"Checking if model wants to call a tool: {last_message}") # Debugging
153
 
154
+ if hasattr(last_message, "tool_calls") and last_message.tool_calls:
155
+ print(f"Model wants to call a tool: {last_message.tool_calls}")
156
+ return "action"
157
+
158
+ print("No tool calls detected, ending execution.")
159
+ return END
160
+
161
+
162
+ ### Add conditional edges to the graph
163
+ uncompiled_graph.add_conditional_edges(
164
+ "agent",
165
+ should_continue
166
+ )
167
+
168
+ uncompiled_graph.add_edge("action", "agent")
169
+ compiled_graph = uncompiled_graph.compile()
170
 
 
 
 
 
 
 
 
 
 
171
 
 
172
 
173
 
174
+ @cl.on_chat_start
175
+ async def start():
176
+ cl.user_session.set("compiled_graph", compiled_graph)
177
+
178
  @cl.on_message
179
+ async def handle(message: cl.Message):
180
+ """Handle user messages, invoke the agent graph, and send responses."""
181
+
182
+ compiled_graph = cl.user_session.get("compiled_graph") # Retrieve the stored graph
183
+
184
+ # Initialize the state for the agent graph
185
+ state = {"messages": [HumanMessage(content=message.content)]}
186
 
187
+ # Invoke the agent graph asynchronously
188
+ response = await compiled_graph.ainvoke(state)
189
 
190
+ # Extract the model's response and send it to the user
191
+ final_message = response["messages"][-1].content # Extract last message content
192
 
193
+ await cl.Message(content=final_message).send()
pyproject.toml CHANGED
@@ -1,30 +1,31 @@
1
  [project]
2
  name = "midterm"
3
  version = "0.1.0"
4
- description = "midterm"
5
  readme = "README.md"
6
  requires-python = ">=3.13"
7
  dependencies = [
8
- "langchain>=0.2.0",
9
- "langchain_openai",
10
- "langchain_huggingface",
11
- "langchain_core",
12
- "langchain_community",
13
- "langchain-text-splitters",
14
- "faiss-cpu",
 
 
 
 
 
 
 
 
 
 
 
15
  "python-pptx==1.0.2",
16
- "nltk==3.9.1",
17
- "pymupdf==1.25.2",
18
- "beautifulsoup4",
19
- "lxml",
20
- "pypdf",
21
- "wandb",
22
- "accelerate>=0.26.0",
23
- "langgraph==0.1.6",
24
- "arxiv",
25
- "langchain-qdrant==0.1.4",
26
- "tiktoken",
27
- "xmltodict",
28
- "rapidfuzz",
29
- "chainlit>=0.7.0"
30
- ]
 
1
  [project]
2
  name = "midterm"
3
  version = "0.1.0"
4
+ description = "Add your description here"
5
  readme = "README.md"
6
  requires-python = ">=3.13"
7
  dependencies = [
8
+ "arxiv==2.1.3",
9
+ "beautifulsoup4==4.13.3",
10
+ "chainlit==2.2.1",
11
+ "cohere==5.13.12",
12
+ "datasets==3.3.1",
13
+ "faiss-cpu==1.10.0",
14
+ "langchain-cohere==0.4.2",
15
+ "langchain-community==0.3.14",
16
+ "langchain-huggingface==0.1.2",
17
+ "langchain-openai==0.2.14",
18
+ "langchain-qdrant==0.2.0",
19
+ "langgraph==0.2.61",
20
+ "lxml==5.3.1",
21
+ "nltk==3.8.1",
22
+ "numpy==2.2.3",
23
+ "pyarrow==19.0.1",
24
+ "pymupdf==1.25.3",
25
+ "python-dotenv>=1.0.1",
26
  "python-pptx==1.0.2",
27
+ "ragas==0.2.10",
28
+ "sentence-transformers==3.4.1",
29
+ "unstructured==0.14.8",
30
+ "websockets>=15.0",
31
+ ]