Spaces:
Sleeping
Sleeping
Commit
ยท
64dd0b5
1
Parent(s):
5ecb42a
initial commit
Browse files- .gitignore +6 -0
- Dockerfile +0 -0
- chainlit.md +14 -0
- chainlit_app.py +124 -0
- rag.py +105 -0
- rag_test.ipynb +0 -0
.gitignore
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Byte-compiled / optimized / DLL files
|
| 2 |
__pycache__/
|
| 3 |
*.py[cod]
|
|
|
|
| 1 |
+
data/
|
| 2 |
+
docs/
|
| 3 |
+
.cursor/
|
| 4 |
+
.chainlit/
|
| 5 |
+
.files/
|
| 6 |
+
|
| 7 |
# Byte-compiled / optimized / DLL files
|
| 8 |
__pycache__/
|
| 9 |
*.py[cod]
|
Dockerfile
ADDED
|
File without changes
|
chainlit.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Welcome to Chainlit! ๐๐ค
|
| 2 |
+
|
| 3 |
+
Hi there, Developer! ๐ We're excited to have you on board. Chainlit is a powerful tool designed to help you prototype, debug and share applications built on top of LLMs.
|
| 4 |
+
|
| 5 |
+
## Useful Links ๐
|
| 6 |
+
|
| 7 |
+
- **Documentation:** Get started with our comprehensive [Chainlit Documentation](https://docs.chainlit.io) ๐
|
| 8 |
+
- **Discord Community:** Join our friendly [Chainlit Discord](https://discord.gg/k73SQ3FyUh) to ask questions, share your projects, and connect with other developers! ๐ฌ
|
| 9 |
+
|
| 10 |
+
We can't wait to see what you create with Chainlit! Happy coding! ๐ป๐
|
| 11 |
+
|
| 12 |
+
## Welcome screen
|
| 13 |
+
|
| 14 |
+
To modify the welcome screen, edit the `chainlit.md` file at the root of your project. If you do not want a welcome screen, just leave this file empty.
|
chainlit_app.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import chainlit as cl
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
|
| 4 |
+
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
| 5 |
+
from langchain_qdrant import QdrantVectorStore
|
| 6 |
+
from langchain_core.messages import HumanMessage, AIMessage
|
| 7 |
+
from rag import make_react_agent_graph
|
| 8 |
+
import tiktoken
|
| 9 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 10 |
+
from langchain.document_loaders import PyMuPDFLoader
|
| 11 |
+
|
| 12 |
+
# load_dotenv()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@cl.on_chat_start
|
| 16 |
+
async def start_chat():
|
| 17 |
+
settings = { # TODO: These settings might need to be passed to the Langchain model differently
|
| 18 |
+
"model": "gpt-4o-mini",
|
| 19 |
+
"temperature": 0.5,
|
| 20 |
+
"max_tokens": 2000,
|
| 21 |
+
"frequency_penalty": 0,
|
| 22 |
+
"presence_penalty": 0,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
# Initialize Langchain components
|
| 26 |
+
model = ChatOpenAI(
|
| 27 |
+
model=settings["model"],
|
| 28 |
+
temperature=settings["temperature"],
|
| 29 |
+
# max_tokens=settings["max_tokens"] # ChatOpenAI might not take max_tokens directly here
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
def tiktoken_len(text):
|
| 33 |
+
tokens = tiktoken.encoding_for_model("gpt-4o-mini").encode(
|
| 34 |
+
text,
|
| 35 |
+
)
|
| 36 |
+
return len(tokens)
|
| 37 |
+
|
| 38 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
| 39 |
+
chunk_size = 300,
|
| 40 |
+
chunk_overlap = 0,
|
| 41 |
+
length_function = tiktoken_len,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
loader = PyMuPDFLoader("data/PracticalAdviceOnMatrixGames.pdf")
|
| 45 |
+
docs = loader.load()
|
| 46 |
+
|
| 47 |
+
split_chunks = text_splitter.split_documents(docs)
|
| 48 |
+
embedding_function = OpenAIEmbeddings(model="text-embedding-3-small")
|
| 49 |
+
# Create a dummy collection. You'll need to populate this with actual documents for RAG to work.
|
| 50 |
+
vector_store = QdrantVectorStore.from_documents(
|
| 51 |
+
split_chunks,
|
| 52 |
+
embedding_function,
|
| 53 |
+
location=":memory:",
|
| 54 |
+
collection_name="matrix_game_docs",
|
| 55 |
+
)
|
| 56 |
+
# You might want to add some documents here if you have any, e.g.:
|
| 57 |
+
# vector_store.add_texts(["Some initial context for the agent"])
|
| 58 |
+
|
| 59 |
+
# Create the ReAct agent graph
|
| 60 |
+
# The search_kwargs for the vector store can be customized if needed
|
| 61 |
+
agent_graph = make_react_agent_graph(model=model, vector_store=vector_store, search_kwargs={"k": 3})
|
| 62 |
+
|
| 63 |
+
cl.user_session.set("agent_graph", agent_graph)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@cl.on_message
|
| 67 |
+
async def main(message: cl.Message):
|
| 68 |
+
agent_graph = cl.user_session.get("agent_graph")
|
| 69 |
+
|
| 70 |
+
if not agent_graph:
|
| 71 |
+
await cl.Message(content="The agent is not initialized. Please restart the chat.").send()
|
| 72 |
+
return
|
| 73 |
+
|
| 74 |
+
conversation_history = cl.chat_context.to_openai()
|
| 75 |
+
|
| 76 |
+
msg = cl.Message(content="")
|
| 77 |
+
# msg.content will be built by streaming tokens.
|
| 78 |
+
# stream_token will call send() on the first token.
|
| 79 |
+
async for token, metadata in agent_graph.astream(
|
| 80 |
+
{'messages': conversation_history},
|
| 81 |
+
stream_mode="messages"
|
| 82 |
+
):
|
| 83 |
+
|
| 84 |
+
if metadata['langgraph_node'] == 'agent':
|
| 85 |
+
await msg.stream_token(token.content)
|
| 86 |
+
|
| 87 |
+
# try:
|
| 88 |
+
# # Use stream_mode="messages" to get LLM tokens as MessageChunk objects
|
| 89 |
+
# async for chunk in agent_graph.astream(
|
| 90 |
+
# agent_input, {"stream_mode": "messages"}
|
| 91 |
+
# ):
|
| 92 |
+
# # chunk is expected to be a MessageChunk (e.g., AIMessageChunk)
|
| 93 |
+
# if hasattr(chunk, 'content'):
|
| 94 |
+
# token = chunk.content
|
| 95 |
+
# if token: # Ensure there's content in the chunk
|
| 96 |
+
# # msg.stream_token will handle sending the message shell on the first call
|
| 97 |
+
# await msg.stream_token(token)
|
| 98 |
+
# # else:
|
| 99 |
+
# # Handle cases where the chunk might not be a MessageChunk as expected,
|
| 100 |
+
# # or if other types of events are streamed in this mode (though less likely for "messages" mode).
|
| 101 |
+
# # print(f"Received chunk without content: {chunk}")
|
| 102 |
+
# except Exception as e:
|
| 103 |
+
# print(f"Error during agent stream: {e}")
|
| 104 |
+
# await cl.Message(content=f"An error occurred: {str(e)}").send()
|
| 105 |
+
# return
|
| 106 |
+
|
| 107 |
+
# After the loop, if tokens were streamed, the message content is populated.
|
| 108 |
+
# A final update might be necessary if other properties of msg need to be set,
|
| 109 |
+
# or to ensure the stream is properly closed from Chainlit's perspective.
|
| 110 |
+
if msg.streaming: # msg.streaming is True if stream_token was called
|
| 111 |
+
await msg.update()
|
| 112 |
+
elif not msg.content: # If no tokens were streamed and message is still empty
|
| 113 |
+
# This case might occur if the agent produces no response or an error happened before streaming
|
| 114 |
+
# (though the try-except should catch errors in astream itself).
|
| 115 |
+
# Send a default message or handle as an error.
|
| 116 |
+
# For now, if no content, we don't send an empty message unless explicitly handled.
|
| 117 |
+
# If msg.send() was never called (because no tokens came), we might need to send something.
|
| 118 |
+
# However, if there was genuinely no response, sending nothing might be intended.
|
| 119 |
+
# Let's ensure an empty message isn't sent if no tokens were ever produced.
|
| 120 |
+
# If an error occurred, it's handled by the except block.
|
| 121 |
+
# If the agent genuinely returns no content, this will result in no message being sent
|
| 122 |
+
# beyond the initial empty shell (if it were sent prior to token checks).
|
| 123 |
+
# Since msg.send() is implicitly called by the first stream_token, if no tokens, no send.
|
| 124 |
+
pass
|
rag.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langgraph.graph import END, StateGraph
|
| 2 |
+
from langgraph.prebuilt import create_react_agent
|
| 3 |
+
from langgraph.checkpoint.memory import InMemorySaver
|
| 4 |
+
|
| 5 |
+
from langchain_core.vectorstores import VectorStore
|
| 6 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 7 |
+
from langchain_core.language_models.chat_models import BaseChatModel
|
| 8 |
+
from langchain_core.tools import tool
|
| 9 |
+
from langchain_core.messages import HumanMessage
|
| 10 |
+
from typing import Callable, List, Sequence, Annotated
|
| 11 |
+
from langchain_core.documents import Document
|
| 12 |
+
from typing import Annotated
|
| 13 |
+
from typing_extensions import TypedDict
|
| 14 |
+
from langgraph.graph.message import add_messages
|
| 15 |
+
from langchain_core.documents import Document
|
| 16 |
+
|
| 17 |
+
class RagState(TypedDict):
|
| 18 |
+
messages: Annotated[list, add_messages]
|
| 19 |
+
context: list[Document]
|
| 20 |
+
|
| 21 |
+
RAG_PROMPT = """\
|
| 22 |
+
Given a provided context and a question, you must answer the question. If you do not know the answer, you must state that you do not know.
|
| 23 |
+
|
| 24 |
+
Context:
|
| 25 |
+
{context}
|
| 26 |
+
|
| 27 |
+
Question:
|
| 28 |
+
{question}
|
| 29 |
+
|
| 30 |
+
Answer:
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
rag_prompt_template = ChatPromptTemplate.from_template(RAG_PROMPT)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def create_retriever_node(vector_store: VectorStore, search_kwargs: dict = {"k": 5}) -> Callable:
|
| 37 |
+
def retriever_node(state: RagState) -> RagState:
|
| 38 |
+
retriever = vector_store.as_retriever(search_kwargs=search_kwargs)
|
| 39 |
+
retrieved_docs = retriever.invoke(state["messages"][-1].content)
|
| 40 |
+
return {"context" : retrieved_docs}
|
| 41 |
+
return retriever_node
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def create_generator_node(model: BaseChatModel, template: ChatPromptTemplate = rag_prompt_template) -> Callable:
|
| 45 |
+
generation_chain = template | model
|
| 46 |
+
def generator_node(state: RagState) -> RagState:
|
| 47 |
+
response = generation_chain.invoke({"query" : state["messages"][-1].content, "context" : state["context"]})
|
| 48 |
+
return {"messages" : response}
|
| 49 |
+
return generator_node
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def make_rag_graph(model: BaseChatModel, vector_store: VectorStore, template: ChatPromptTemplate = rag_prompt_template, search_kwargs: dict = {"k": 5}) -> StateGraph:
|
| 53 |
+
retriever_node = create_retriever_node(vector_store, search_kwargs)
|
| 54 |
+
generator_node = create_generator_node(model, template)
|
| 55 |
+
|
| 56 |
+
rag_graph = StateGraph(RagState)
|
| 57 |
+
|
| 58 |
+
rag_graph.add_node("retriever", retriever_node)
|
| 59 |
+
rag_graph.add_node("generator", generator_node)
|
| 60 |
+
|
| 61 |
+
rag_graph.set_entry_point("retriever")
|
| 62 |
+
|
| 63 |
+
rag_graph.add_edge("retriever", "generator")
|
| 64 |
+
rag_graph.add_edge("generator", END)
|
| 65 |
+
|
| 66 |
+
return rag_graph.compile()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# For the ReAct agent, the state is typically managed by the prebuilt agent itself,
|
| 70 |
+
# focusing on the 'messages' list. If a specific state object like RagState is needed
|
| 71 |
+
# for integration, the graph's input/output would need to be adapted.
|
| 72 |
+
# For now, we assume the agent operates on a message-based state.
|
| 73 |
+
|
| 74 |
+
def create_vector_search_tool(vector_store: VectorStore, search_kwargs: dict) -> Callable:
|
| 75 |
+
@tool("vector-search")
|
| 76 |
+
def vector_search_tool(query: str) -> List[str]:
|
| 77 |
+
"""Searches a vector database for the given query and returns relevant document contents."""
|
| 78 |
+
retriever = vector_store.as_retriever(search_kwargs=search_kwargs)
|
| 79 |
+
retrieved_docs = retriever.invoke(query)
|
| 80 |
+
return [doc.page_content for doc in retrieved_docs]
|
| 81 |
+
return vector_search_tool
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def make_react_agent_graph(model: BaseChatModel, vector_store: VectorStore, search_kwargs: dict = {"k": 5}):
|
| 85 |
+
"""
|
| 86 |
+
Creates a StateGraph for a RAG agent that uses the ReAct framework.
|
| 87 |
+
The agent can formulate its own queries to the vector database.
|
| 88 |
+
"""
|
| 89 |
+
search_tool = create_vector_search_tool(vector_store, search_kwargs)
|
| 90 |
+
|
| 91 |
+
# A checkpointer is highly recommended for agents to allow them to save state
|
| 92 |
+
# across multiple steps of thought and action.
|
| 93 |
+
# checkpointer = InMemorySaver()
|
| 94 |
+
|
| 95 |
+
# The prebuilt create_react_agent handles the agent logic and graph compilation.
|
| 96 |
+
# It uses a default ReAct prompt unless a custom one is provided.
|
| 97 |
+
# The agent's state revolves around messages.
|
| 98 |
+
# Input to this agent_graph.invoke should be like: {"messages": [HumanMessage(content="your question")]}
|
| 99 |
+
agent_graph = create_react_agent(
|
| 100 |
+
model=model,
|
| 101 |
+
tools=[search_tool]#,
|
| 102 |
+
#checkpointer=checkpointer
|
| 103 |
+
)
|
| 104 |
+
return agent_graph
|
| 105 |
+
|
rag_test.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|