CalebMaresca commited on
Commit
64dd0b5
ยท
1 Parent(s): 5ecb42a

initial commit

Browse files
Files changed (6) hide show
  1. .gitignore +6 -0
  2. Dockerfile +0 -0
  3. chainlit.md +14 -0
  4. chainlit_app.py +124 -0
  5. rag.py +105 -0
  6. 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