├── .vscode └── settings.json ├── LICENSE ├── README.md ├── agent ├── .env.example ├── .gitignore ├── .vscode │ ├── cspell.json │ └── settings.json ├── langgraph.json ├── poetry.lock ├── pyproject.toml └── rag_agent │ ├── __init__.py │ ├── agent.py │ ├── demo.py │ ├── edges │ ├── __init__.py │ ├── answer_grader.py │ └── hallucination_grader.py │ ├── nodes │ ├── __init__.py │ ├── generate.py │ ├── question_rewriter.py │ ├── retrieval_grader.py │ └── retriever.py │ └── state.py └── ui ├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── api │ └── copilotkit │ │ └── route.ts ├── globals.css ├── layout.tsx └── page.tsx ├── bun.lockb ├── components.json ├── components ├── chat-interface.tsx ├── header.tsx ├── theme │ ├── mode-toggle.tsx │ └── theme-provider.tsx ├── ui │ ├── avatar.tsx │ ├── button.tsx │ ├── card.tsx │ ├── input.tsx │ ├── matrix-rain.tsx │ ├── retro-grid.tsx │ ├── textarea.tsx │ ├── toast.tsx │ └── toaster.tsx ├── url-input.tsx └── wrapper.tsx ├── hooks ├── use-speech-to-text.ts ├── use-text-to-speech.ts └── use-toast.ts ├── lib └── utils.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── tailwind.config.ts └── tsconfig.json /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "copilotkit", 4 | "FAISS", 5 | "fastembed", 6 | "groq", 7 | "vectorstore", 8 | "vectorstores" 9 | ] 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![talk-to-page](https://socialify.git.ci/kom-senapati/talk-to-page/image?font=Source+Code+Pro&language=1&name=1&owner=1&pattern=Solid&stargazers=1&theme=Dark) 2 | 3 |

4 | 5 |

6 | 7 | ### ⭐ About 8 | 9 | - Open-source web app for chatting with web pages. 10 | - Built with Next.js, CopilotKit. 11 | - Uses a self-RAG LangGraph agent to interact with URLs. 12 | - Simplifies web content extraction and conversation. 13 | 14 | | Demo Video | Blog Post | 15 | |----------------------------------------------------------------------------|--------------------------------------------------------------------------| 16 | | [![YouTube](http://i.ytimg.com/vi/O0Y2WEqkros/hqdefault.jpg)](https://www.youtube.com/watch?v=O0Y2WEqkros) | [![Blog](https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffr5p7w1bobvfjpqw29gn.png)](https://dev.to/komsenapati/building-talk-to-page-chat-or-talk-with-any-website-g0h) | 17 | 18 | 19 | ### :hammer_and_wrench: Tech Stack 20 | 21 |

22 | Next.js 23 | CopilotKit 24 | Tailwind CSS 25 | ShadCN UI 26 | LangGraph 27 | FastAPI 28 |

29 | 30 | ### :outbox_tray: Set up 31 | 32 | #### **Setting Up the Agent and UI** 33 | 34 | ##### **1. Get an API Key** 35 | - Obtain a **GROQ_API_KEY**. 36 | 37 | ##### **2. Clone the Repository** 38 | - Clone the repository to your local machine: 39 | ```sh 40 | git clone https://github.com/kom-senapati/talk-to-page.git 41 | ``` 42 | 43 | ##### **3. Set Up the Agent** 44 | - Navigate to the agent directory: 45 | ```sh 46 | cd agent 47 | ``` 48 | - Install dependencies using Poetry: 49 | ```sh 50 | poetry install 51 | ``` 52 | - Create a `.env` file inside the `./agent` directory with your **GROQ_API_KEY**: 53 | ``` 54 | GROQ_API_KEY=YOUR_API_KEY_HERE 55 | ``` 56 | - Run the agent demo: 57 | ```sh 58 | poetry run demo 59 | ``` 60 | 61 | ##### **4. Set Up the UI** 62 | - Navigate to the UI directory: 63 | ```sh 64 | cd ./ui 65 | ``` 66 | - Install dependencies using Bun: 67 | ```sh 68 | bun i 69 | ``` 70 | - Create a `.env` file inside the `./ui` directory with your **GROQ_API_KEY**: 71 | ``` 72 | GROQ_API_KEY=YOUR_API_KEY_HERE 73 | ``` 74 | - Run the Next.js project: 75 | ```sh 76 | bun dev 77 | ``` 78 | 79 | #### **Troubleshooting** 80 | 1. Ensure no other local application is running on port **8000**. 81 | 2. In the file `/agent/rag_agent/demo.py`, change the address from `0.0.0.0` to `127.0.0.1` or `localhost` if needed. 82 | 83 | ### :email: Contact 84 | Hi, I'm K Om Senapati! 👋 85 | Connect with me on [LinkedIn](https://www.linkedin.com/in/kom-senapati/), [X](https://x.com/kom_senapati) and check out my other projects on [GitHub](https://github.com/kom-senapati). 86 | -------------------------------------------------------------------------------- /agent/.env.example: -------------------------------------------------------------------------------- 1 | GROQ_API_KEY=your_api_key_here -------------------------------------------------------------------------------- /agent/.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | __pycache__/ 3 | *.pyc 4 | .env 5 | .vercel 6 | -------------------------------------------------------------------------------- /agent/.vscode/cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en", 4 | "words": [ 5 | "langgraph", 6 | "langchain", 7 | "perplexity", 8 | "openai", 9 | "ainvoke", 10 | "pydantic", 11 | "tavily", 12 | "copilotkit", 13 | "fastapi", 14 | "uvicorn", 15 | "checkpointer", 16 | "dotenv" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /agent/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "basic" 3 | } 4 | -------------------------------------------------------------------------------- /agent/langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "python_version": "3.12", 3 | "dockerfile_lines": [], 4 | "dependencies": ["."], 5 | "graphs": { 6 | "rag_agent": "./rag_agent/agent.py:graph" 7 | }, 8 | "env": ".env" 9 | } 10 | -------------------------------------------------------------------------------- /agent/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "rag_agent" 3 | version = "0.1.0" 4 | description = "Starter" 5 | authors = ["K Om Senapati "] 6 | license = "MIT" 7 | 8 | [project] 9 | name = "rag_agent" 10 | version = "0.0.1" 11 | 12 | [build-system] 13 | requires = ["setuptools >= 61.0"] 14 | build-backend = "setuptools.build_meta" 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.12" 18 | langchain-anthropic = "^0.2.1" 19 | langchain = "^0.3.1" 20 | openai = "^1.51.0" 21 | langchain-community = "^0.3.1" 22 | copilotkit = "0.1.30" 23 | uvicorn = "^0.31.0" 24 | python-dotenv = "^1.0.1" 25 | langchain-core = "^0.3.25" 26 | langgraph-cli = {extras = ["inmem"], version = "^0.1.64"} 27 | langchain-groq = "^0.2.2" 28 | chroma = "^0.2.0" 29 | fastembed = "^0.5.0" 30 | bs4 = "^0.0.2" 31 | faiss-cpu = "^1.9.0.post1" 32 | ipykernel = "^6.29.5" 33 | 34 | [tool.poetry.scripts] 35 | demo = "rag_agent.demo:main" 36 | -------------------------------------------------------------------------------- /agent/rag_agent/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0msenapati/talk-to-page/a66183a812b4c8db2a25bc7634ef3efa83c98bc7/agent/rag_agent/__init__.py -------------------------------------------------------------------------------- /agent/rag_agent/agent.py: -------------------------------------------------------------------------------- 1 | from langgraph.graph import END, StateGraph, START 2 | from rag_agent.state import AgentState 3 | from rag_agent.nodes import ( 4 | generate, 5 | grade_documents, 6 | retrieve, 7 | transform_query, 8 | update_url, 9 | no_context 10 | ) 11 | from rag_agent.edges import ( 12 | decide_to_generate, 13 | grade_generation_v_documents_and_question, 14 | new_url, 15 | ) 16 | from langgraph.checkpoint.memory import MemorySaver 17 | 18 | 19 | workflow = StateGraph(AgentState) 20 | 21 | workflow.add_node("update_url", update_url) 22 | workflow.add_node("retrieve", retrieve) 23 | workflow.add_node("grade_documents", grade_documents) 24 | workflow.add_node("generate", generate) 25 | workflow.add_node("transform_query", transform_query) 26 | workflow.add_node("no_context", no_context) 27 | 28 | workflow.add_conditional_edges( 29 | START, 30 | new_url, 31 | { 32 | "update_url": "update_url", 33 | "retrieve": "retrieve", 34 | }, 35 | ) 36 | workflow.add_edge("retrieve", "grade_documents") 37 | workflow.add_conditional_edges( 38 | "grade_documents", 39 | decide_to_generate, 40 | { 41 | "transform_query": "transform_query", 42 | "generate": "generate", 43 | }, 44 | ) 45 | workflow.add_edge("transform_query", "retrieve") 46 | workflow.add_conditional_edges( 47 | "generate", 48 | grade_generation_v_documents_and_question, 49 | { 50 | "not supported": "generate", 51 | "useful": END, 52 | "not useful": "transform_query", 53 | "no_context": "no_context", 54 | }, 55 | ) 56 | workflow.add_edge("update_url", END) 57 | workflow.add_edge("no_context", END) 58 | 59 | graph = workflow.compile(checkpointer=MemorySaver()) 60 | -------------------------------------------------------------------------------- /agent/rag_agent/demo.py: -------------------------------------------------------------------------------- 1 | """Demo""" 2 | 3 | from copilotkit import CopilotKitSDK, LangGraphAgent 4 | from copilotkit.integrations.fastapi import add_fastapi_endpoint 5 | from dotenv import load_dotenv 6 | from fastapi import FastAPI 7 | from rag_agent.agent import graph 8 | import os 9 | import uvicorn 10 | 11 | load_dotenv() 12 | 13 | app = FastAPI() 14 | sdk = CopilotKitSDK( 15 | agents=[ 16 | LangGraphAgent( 17 | name="rag_agent", 18 | description="A rag_agent that's used for chatting with an url", 19 | graph=graph, 20 | ) 21 | ], 22 | ) 23 | 24 | add_fastapi_endpoint(app, sdk, "/copilotkit") 25 | 26 | 27 | def main(): 28 | """Run the uvicorn server.""" 29 | port = int(os.getenv("PORT", "8000")) 30 | uvicorn.run("rag_agent.demo:app", host="0.0.0.0", port=port, reload=True) 31 | -------------------------------------------------------------------------------- /agent/rag_agent/edges/__init__.py: -------------------------------------------------------------------------------- 1 | from rag_agent.edges.hallucination_grader import hallucination_grader 2 | from rag_agent.edges.answer_grader import answer_grader 3 | from langchain_core.messages import SystemMessage, HumanMessage 4 | 5 | 6 | def new_url(state): 7 | messages = state["messages"] 8 | last_message = messages[-1] 9 | 10 | if ( 11 | isinstance(last_message, SystemMessage) 12 | and "URL UPDATED" in last_message.content 13 | ): 14 | return "update_url" 15 | 16 | return "retrieve" 17 | 18 | 19 | def decide_to_generate(state): 20 | """ 21 | Determines whether to generate an answer, or re-generate a question. 22 | 23 | Args: 24 | state (dict): The current graph state 25 | 26 | Returns: 27 | str: Binary decision for next node to call 28 | """ 29 | 30 | print("---ASSESS GRADED DOCUMENTS---") 31 | state["question"] 32 | filtered_documents = state["documents"] 33 | 34 | if not filtered_documents: 35 | # print(f"max_retries_rewrite: {state['max_retries_rewrite']}") 36 | # if state["max_retries_rewrite"] < 2: 37 | # state.update({"max_retries_rewrite": state["max_retries_rewrite"] + 1}) 38 | 39 | print( 40 | "---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, TRANSFORM QUERY---" 41 | ) 42 | return "transform_query" 43 | # else: 44 | # state.update({"max_retries_rewrite": 0}) 45 | 46 | # print("---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, STOP---") 47 | # return "no_context" 48 | else: 49 | print("---DECISION: GENERATE---") 50 | return "generate" 51 | 52 | 53 | def grade_generation_v_documents_and_question(state): 54 | """ 55 | Determines whether the generation is grounded in the document and answers question. 56 | 57 | Args: 58 | state (dict): The current graph state 59 | 60 | Returns: 61 | str: Decision for next node to call 62 | """ 63 | 64 | print("---CHECK HALLUCINATIONS---") 65 | question = state["question"] 66 | documents = state["documents"] 67 | generation = state["generation"] 68 | 69 | score = hallucination_grader.invoke( 70 | {"documents": documents, "generation": generation} 71 | ) 72 | grade = score.binary_score 73 | 74 | if grade == "yes": 75 | print("---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---") 76 | 77 | print("---GRADE GENERATION vs QUESTION---") 78 | score = answer_grader.invoke({"question": question, "generation": generation}) 79 | grade = score.binary_score 80 | 81 | if grade == "yes": 82 | messages = state["messages"] 83 | messages.append(HumanMessage(generation)) 84 | 85 | print("---DECISION: GENERATION ADDRESSES QUESTION---") 86 | return "useful" 87 | else: 88 | # print(f"max_retries_rewrite: {state['max_retries_rewrite']}") 89 | # if state["max_retries_rewrite"] < 2: 90 | # state["max_retries_rewrite"] += 1 91 | 92 | print("---DECISION: GENERATION DOES NOT ADDRESS QUESTION---") 93 | return "not useful" 94 | # else: 95 | # state["max_retries_rewrite"] = 0 96 | 97 | # print("---DECISION: GENERATION DOES NOT ADDRESS QUESTION, STOP---") 98 | # return "no_context" 99 | else: 100 | # print(f"max_generations: {state['max_generations']}") 101 | # if state["max_generations"] < 2: 102 | # state["max_generations"] += 1 103 | 104 | print("---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---") 105 | return "not supported" 106 | # else: 107 | # state["max_generations"] = 0 108 | 109 | # print("---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, STOP---") 110 | # return "no_context" 111 | -------------------------------------------------------------------------------- /agent/rag_agent/edges/answer_grader.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from langchain_groq import ChatGroq 3 | from langchain_core.prompts import ChatPromptTemplate 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | class GradeAnswer(BaseModel): 9 | """Binary score to assess answer addresses question.""" 10 | 11 | binary_score: str = Field( 12 | description="Answer addresses the question, 'yes' or 'no'" 13 | ) 14 | 15 | 16 | llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0) 17 | structured_llm_grader = llm.with_structured_output(GradeAnswer) 18 | 19 | system = """You are a grader assessing whether an answer addresses / resolves a question \n 20 | Give a binary score 'yes' or 'no'. Yes' means that the answer resolves the question.""" 21 | 22 | answer_prompt = ChatPromptTemplate.from_messages( 23 | [ 24 | ("system", system), 25 | ("human", "User question: \n\n {question} \n\n LLM generation: {generation}"), 26 | ] 27 | ) 28 | 29 | answer_grader = answer_prompt | structured_llm_grader 30 | -------------------------------------------------------------------------------- /agent/rag_agent/edges/hallucination_grader.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from langchain_groq import ChatGroq 3 | from langchain_core.prompts import ChatPromptTemplate 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | class GradeHallucinations(BaseModel): 9 | """Binary score for hallucination present in generation answer.""" 10 | 11 | binary_score: str = Field( 12 | description="Answer is grounded in the facts, 'yes' or 'no'" 13 | ) 14 | 15 | 16 | llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0) 17 | structured_llm_grader = llm.with_structured_output(GradeHallucinations) 18 | 19 | system = """You are a grader assessing whether an LLM generation is grounded in / supported by a set of retrieved facts. \n 20 | Give a binary score 'yes' or 'no'. 'Yes' means that the answer is grounded in / supported by the set of facts.""" 21 | 22 | hallucination_prompt = ChatPromptTemplate.from_messages( 23 | [ 24 | ("system", system), 25 | ("human", "Set of facts: \n\n {documents} \n\n LLM generation: {generation}"), 26 | ] 27 | ) 28 | 29 | hallucination_grader = hallucination_prompt | structured_llm_grader 30 | -------------------------------------------------------------------------------- /agent/rag_agent/nodes/__init__.py: -------------------------------------------------------------------------------- 1 | from rag_agent.nodes.retriever import retriever_instance 2 | from rag_agent.nodes.generate import rag_chain 3 | from rag_agent.nodes.retrieval_grader import retrieval_grader 4 | from rag_agent.nodes.question_rewriter import question_rewriter 5 | from langchain_core.messages import HumanMessage 6 | 7 | 8 | def update_url(state): 9 | print("---UPDATE URL---") 10 | 11 | new_url = state["url"] 12 | print(f"---NEW URL DETECTED: {new_url}---") 13 | 14 | retriever_instance.update_retriever(new_url) 15 | 16 | print("---RETRIEVER TOOL UPDATED---") 17 | 18 | return {**state, "url": new_url, "messages": []} 19 | 20 | 21 | def retrieve(state): 22 | """ 23 | Retrieve documents 24 | 25 | Args: 26 | state (dict): The current graph state 27 | 28 | Returns: 29 | state (dict): New key added to state, documents, that contains retrieved documents 30 | """ 31 | print("---RETRIEVE---") 32 | messages = state["messages"] 33 | question = messages[-1].content 34 | 35 | retriever = retriever_instance.retriever 36 | documents = retriever.invoke(question) 37 | return { 38 | **state, 39 | "documents": documents, 40 | "question": question, 41 | "max_retries_rewrite": state.get("max_retries_rewrite", 0), 42 | "max_generations": state.get("max_generations", 0), 43 | } 44 | 45 | 46 | def generate(state): 47 | """ 48 | Generate answer 49 | 50 | Args: 51 | state (dict): The current graph state 52 | 53 | Returns: 54 | state (dict): New key added to state, generation, that contains LLM generation 55 | """ 56 | print("---GENERATE---") 57 | question = state["question"] 58 | documents = state["documents"] 59 | 60 | generation = rag_chain.invoke({"context": documents, "question": question}) 61 | return { 62 | **state, 63 | "documents": documents, 64 | "question": question, 65 | "generation": generation, 66 | } 67 | 68 | 69 | def grade_documents(state): 70 | """ 71 | Determines whether the retrieved documents are relevant to the question. 72 | 73 | Args: 74 | state (dict): The current graph state 75 | 76 | Returns: 77 | state (dict): Updates documents key with only filtered relevant documents 78 | """ 79 | 80 | print("---CHECK DOCUMENT RELEVANCE TO QUESTION---") 81 | question = state["question"] 82 | documents = state["documents"] 83 | 84 | filtered_docs = [] 85 | for d in documents: 86 | score = retrieval_grader.invoke( 87 | {"question": question, "document": d.page_content} 88 | ) 89 | grade = score.binary_score 90 | if grade == "yes": 91 | print("---GRADE: DOCUMENT RELEVANT---") 92 | filtered_docs.append(d) 93 | else: 94 | print("---GRADE: DOCUMENT NOT RELEVANT---") 95 | continue 96 | return {**state, "documents": filtered_docs, "question": question} 97 | 98 | 99 | def transform_query(state): 100 | """ 101 | Transform the query to produce a better question. 102 | 103 | Args: 104 | state (dict): The current graph state 105 | 106 | Returns: 107 | state (dict): Updates question key with a re-phrased question 108 | """ 109 | 110 | print("---TRANSFORM QUERY---") 111 | question = state["question"] 112 | documents = state["documents"] 113 | 114 | better_question = question_rewriter.invoke({"question": question}) 115 | return {**state, "documents": documents, "question": better_question} 116 | 117 | 118 | def no_context(state): 119 | print("---NO CONTEXT---") 120 | 121 | messages = state["messages"] 122 | messages.append(HumanMessage("I'm sorry, I can't find any relevant information.")) 123 | 124 | return state 125 | -------------------------------------------------------------------------------- /agent/rag_agent/nodes/generate.py: -------------------------------------------------------------------------------- 1 | from langchain import hub 2 | from langchain_core.output_parsers import StrOutputParser 3 | from langchain_groq import ChatGroq 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | prompt = hub.pull("rlm/rag-prompt") 9 | 10 | llm = ChatGroq(model_name="llama-3.3-70b-versatile", temperature=0) 11 | 12 | 13 | def format_docs(docs): 14 | return "\n\n".join(doc.page_content for doc in docs) 15 | 16 | 17 | rag_chain = prompt | llm | StrOutputParser() 18 | -------------------------------------------------------------------------------- /agent/rag_agent/nodes/question_rewriter.py: -------------------------------------------------------------------------------- 1 | from langchain_groq import ChatGroq 2 | from langchain_core.output_parsers import StrOutputParser 3 | from langchain_core.prompts import ChatPromptTemplate 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0) 9 | 10 | system = """You a question re-writer that converts an input question to a better version that is optimized \n 11 | for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning.""" 12 | re_write_prompt = ChatPromptTemplate.from_messages( 13 | [ 14 | ("system", system), 15 | ( 16 | "human", 17 | "Here is the initial question: \n\n {question} \n Formulate an improved question.", 18 | ), 19 | ] 20 | ) 21 | 22 | question_rewriter = re_write_prompt | llm | StrOutputParser() 23 | -------------------------------------------------------------------------------- /agent/rag_agent/nodes/retrieval_grader.py: -------------------------------------------------------------------------------- 1 | from langchain_core.prompts import ChatPromptTemplate 2 | from langchain_groq import ChatGroq 3 | from pydantic import BaseModel, Field 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | class GradeDocuments(BaseModel): 9 | """Binary score for relevance check on retrieved documents.""" 10 | 11 | binary_score: str = Field( 12 | description="Documents are relevant to the question, 'yes' or 'no'" 13 | ) 14 | 15 | 16 | llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0) 17 | structured_llm_grader = llm.with_structured_output(GradeDocuments) 18 | 19 | system = """You are a grader assessing relevance of a retrieved document to a user question. \n 20 | It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \n 21 | If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \n 22 | Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question.""" 23 | 24 | grade_prompt = ChatPromptTemplate.from_messages( 25 | [ 26 | ("system", system), 27 | ("human", "Retrieved document: \n\n {document} \n\n User question: {question}"), 28 | ] 29 | ) 30 | 31 | retrieval_grader = grade_prompt | structured_llm_grader 32 | -------------------------------------------------------------------------------- /agent/rag_agent/nodes/retriever.py: -------------------------------------------------------------------------------- 1 | from langchain_community.document_loaders import WebBaseLoader 2 | from langchain_community.embeddings.fastembed import FastEmbedEmbeddings 3 | from langchain_community.vectorstores import FAISS 4 | from langchain.text_splitter import RecursiveCharacterTextSplitter 5 | 6 | class Retriever: 7 | def __init__(self, url): 8 | self.url = url 9 | self.retriever = self._get_retriever(url) 10 | 11 | def _get_retriever(self, url): 12 | docs = WebBaseLoader(url).load() 13 | 14 | text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( 15 | chunk_size=100, chunk_overlap=50 16 | ) 17 | doc_splits = text_splitter.split_documents(docs) 18 | 19 | vectorstore = FAISS.from_documents( 20 | documents=doc_splits, 21 | embedding=FastEmbedEmbeddings(), 22 | ) 23 | retriever = vectorstore.as_retriever() 24 | return retriever 25 | 26 | def update_retriever(self, url): 27 | self.url = url 28 | self.retriever = self._get_retriever(url) 29 | 30 | 31 | url = "https://blog.langchain.dev/langgraph/" 32 | retriever_instance = Retriever(url) 33 | -------------------------------------------------------------------------------- /agent/rag_agent/state.py: -------------------------------------------------------------------------------- 1 | from typing import List, Annotated, Sequence 2 | from typing_extensions import TypedDict 3 | from langchain_core.messages import BaseMessage 4 | from langgraph.graph.message import add_messages 5 | 6 | 7 | class AgentState(TypedDict): 8 | """ 9 | Represents the state of our graph. 10 | 11 | Attributes: 12 | url: current url 13 | question: question 14 | generation: LLM generation 15 | documents: list of documents 16 | messages: list of messages 17 | """ 18 | 19 | url: str 20 | question: str 21 | generation: str 22 | documents: List[str] 23 | messages: Annotated[Sequence[BaseMessage], add_messages] 24 | max_retries_rewrite: int 25 | max_generations: int 26 | -------------------------------------------------------------------------------- /ui/.env.example: -------------------------------------------------------------------------------- 1 | GROQ_API_KEY=your_api_key_here -------------------------------------------------------------------------------- /ui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | .env 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /ui/app/api/copilotkit/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import { 3 | CopilotRuntime, 4 | GroqAdapter, 5 | copilotRuntimeNextJSAppRouterEndpoint, 6 | } from "@copilotkit/runtime"; 7 | 8 | const serviceAdapter = new GroqAdapter({ model: "llama-3.3-70b-versatile" }); 9 | 10 | const runtime = new CopilotRuntime({ 11 | remoteEndpoints: [ 12 | { 13 | url: process.env.REMOTE_ACTION_URL || "http://localhost:8000/copilotkit", 14 | }, 15 | ], 16 | }); 17 | 18 | export const POST = async (req: NextRequest) => { 19 | const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({ 20 | runtime, 21 | serviceAdapter, 22 | endpoint: "/api/copilotkit", 23 | }); 24 | 25 | return handleRequest(req); 26 | }; 27 | -------------------------------------------------------------------------------- /ui/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | @layer base { 7 | :root { 8 | --background: 0 0% 100%; 9 | --foreground: 240 10% 3.9%; 10 | --card: 0 0% 100%; 11 | --card-foreground: 240 10% 3.9%; 12 | --popover: 0 0% 100%; 13 | --popover-foreground: 240 10% 3.9%; 14 | --primary: 142.1 76.2% 36.3%; 15 | --primary-foreground: 355.7 100% 97.3%; 16 | --secondary: 240 4.8% 95.9%; 17 | --secondary-foreground: 240 5.9% 10%; 18 | --muted: 240 4.8% 95.9%; 19 | --muted-foreground: 240 3.8% 46.1%; 20 | --accent: 240 4.8% 95.9%; 21 | --accent-foreground: 240 5.9% 10%; 22 | --destructive: 0 84.2% 60.2%; 23 | --destructive-foreground: 0 0% 98%; 24 | --border: 240 5.9% 90%; 25 | --input: 240 5.9% 90%; 26 | --ring: 142.1 76.2% 36.3%; 27 | --radius: 0.5rem; 28 | --chart-1: 12 76% 61%; 29 | --chart-2: 173 58% 39%; 30 | --chart-3: 197 37% 24%; 31 | --chart-4: 43 74% 66%; 32 | --chart-5: 27 87% 67%; 33 | } 34 | 35 | .dark { 36 | --background: 20 14.3% 4.1%; 37 | --foreground: 0 0% 95%; 38 | --card: 24 9.8% 10%; 39 | --card-foreground: 0 0% 95%; 40 | --popover: 0 0% 9%; 41 | --popover-foreground: 0 0% 95%; 42 | --primary: 142.1 70.6% 45.3%; 43 | --primary-foreground: 144.9 80.4% 10%; 44 | --secondary: 240 3.7% 15.9%; 45 | --secondary-foreground: 0 0% 98%; 46 | --muted: 0 0% 15%; 47 | --muted-foreground: 240 5% 64.9%; 48 | --accent: 12 6.5% 15.1%; 49 | --accent-foreground: 0 0% 98%; 50 | --destructive: 0 62.8% 30.6%; 51 | --destructive-foreground: 0 85.7% 97.3%; 52 | --border: 240 3.7% 15.9%; 53 | --input: 240 3.7% 15.9%; 54 | --ring: 142.4 71.8% 29.2%; 55 | --chart-1: 220 70% 50%; 56 | --chart-2: 160 60% 45%; 57 | --chart-3: 30 80% 55%; 58 | --chart-4: 280 65% 60%; 59 | --chart-5: 340 75% 55%; 60 | } 61 | } 62 | 63 | 64 | @layer base { 65 | * { 66 | @apply border-border; 67 | } 68 | 69 | body { 70 | @apply bg-background text-foreground; 71 | } 72 | } 73 | 74 | @keyframes fadeOut { 75 | from { 76 | opacity: 1; 77 | } 78 | 79 | to { 80 | opacity: 0; 81 | } 82 | } 83 | 84 | .animate-fade-out { 85 | animation: fadeOut 3s ease-out; 86 | } -------------------------------------------------------------------------------- /ui/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import { CopilotKit } from "@copilotkit/react-core"; 4 | import { Toaster } from "@/components/ui/toaster"; 5 | import { ThemeProvider } from "@/components/theme/theme-provider"; 6 | import "./globals.css"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "TalkToPage", 12 | description: "Explore web content like never before. TalkToPage allows you to interact with any URL, enabling real-time discussions, insights, and exploration of webpage content. Transform static browsing into a dynamic and engaging experience.", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | {children} 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /ui/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import ChatInterface from "@/components/chat-interface"; 4 | import Header from "@/components/header"; 5 | import MatrixRainBackground from "@/components/ui/matrix-rain"; 6 | import RetroGrid from "@/components/ui/retro-grid"; 7 | import UrlInput from "@/components/url-input"; 8 | import Wrapper from "@/components/wrapper"; 9 | import { useToast } from "@/hooks/use-toast"; 10 | import { useCoAgent, useCopilotChat } from "@copilotkit/react-core"; 11 | import { Role, TextMessage } from "@copilotkit/runtime-client-gql"; 12 | import { useState, useEffect } from "react"; 13 | 14 | interface AgentState { 15 | url: string 16 | } 17 | 18 | export default function Home() { 19 | const { state, setState, run } = useCoAgent({ 20 | name: "rag_agent", 21 | initialState: { 22 | url: "https://blog.langchain.dev/langgraph/" 23 | } 24 | }) 25 | const { isLoading, appendMessage, visibleMessages } = useCopilotChat() 26 | const { toast } = useToast() 27 | const [isUrlUpdated, setIsUrlUpdated] = useState(false) 28 | 29 | useEffect(() => { 30 | if (isUrlUpdated) { 31 | const timer = setTimeout(() => { 32 | setIsUrlUpdated(false) 33 | }, 5000) 34 | return () => clearTimeout(timer) 35 | } 36 | }, [isUrlUpdated]) 37 | 38 | const handleSave = () => { 39 | if (!state.url) return; 40 | run(() => new TextMessage({ role: Role.System, content: "URL UPDATED" })) 41 | setIsUrlUpdated(true) 42 | toast({ 43 | title: "URL UPDATED", 44 | description: "The URL has been updated successfully.", 45 | }) 46 | } 47 | 48 | return ( 49 | 50 |
51 | 52 | 57 | 58 | { 59 | isUrlUpdated ? : 60 | } 61 | 62 | ) 63 | } -------------------------------------------------------------------------------- /ui/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0msenapati/talk-to-page/a66183a812b4c8db2a25bc7634ef3efa83c98bc7/ui/bun.lockb -------------------------------------------------------------------------------- /ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /ui/components/chat-interface.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" 4 | import { Button } from "@/components/ui/button" 5 | import { Card, CardContent, CardFooter } from "@/components/ui/card" 6 | import { useSpeechToText } from "@/hooks/use-speech-to-text" 7 | import { useTextToSpeech } from "@/hooks/use-text-to-speech" 8 | import { cn } from "@/lib/utils" 9 | import { Role, TextMessage } from "@copilotkit/runtime-client-gql" 10 | import { Loader2, Mic, MicOff, Pause, Play, Send, Square, Volume2 } from 'lucide-react' 11 | import { useEffect, useRef, useState } from "react" 12 | import { Textarea } from "./ui/textarea" 13 | 14 | export default function ChatInterface({ 15 | isLoading, 16 | appendMessage, 17 | visibleMessages 18 | }: { 19 | isLoading: boolean 20 | appendMessage: (message: TextMessage) => void 21 | visibleMessages: TextMessage[] 22 | }) { 23 | const messagesEndRef = useRef(null) 24 | const [inputValue, setInputValue] = useState("") 25 | const { transcript, isListening, startListening, stopListening, error: speechToTextError } = useSpeechToText() 26 | const { speak, pause, resume, cancel, isSpeaking, isPaused, getSpeakingState } = useTextToSpeech() 27 | const [speakingMessageId, setSpeakingMessageId] = useState(null); 28 | 29 | const scrollToBottom = () => { 30 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) 31 | } 32 | 33 | useEffect(() => { 34 | setInputValue(transcript) 35 | }, [transcript]) 36 | 37 | useEffect(() => { 38 | if (speechToTextError) { 39 | console.error("Speech-to-text error:", speechToTextError) 40 | } 41 | }, [speechToTextError]) 42 | 43 | useEffect(() => { 44 | const textarea = document.querySelector('textarea'); 45 | if (textarea) { 46 | textarea.style.height = 'auto'; 47 | textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`; 48 | } 49 | }, [inputValue]); 50 | 51 | useEffect(() => { 52 | scrollToBottom(); 53 | }, [visibleMessages]); 54 | 55 | const handleSendMessage = () => { 56 | if (inputValue.trim()) { 57 | appendMessage(new TextMessage({ 58 | content: inputValue, 59 | role: Role.User 60 | })) 61 | setInputValue("") 62 | } 63 | } 64 | 65 | const toggleListening = () => { 66 | if (isListening) { 67 | stopListening() 68 | } else { 69 | startListening() 70 | } 71 | } 72 | 73 | const handleSpeak = (messageId: string, content: string) => { 74 | if (speakingMessageId && speakingMessageId !== messageId) { 75 | cancel(); 76 | } 77 | speak(content); 78 | setSpeakingMessageId(messageId); 79 | } 80 | 81 | const handlePauseResume = () => { 82 | const { isPaused } = getSpeakingState(); 83 | if (isPaused) { 84 | resume(); 85 | } else { 86 | pause(); 87 | } 88 | } 89 | 90 | const handleStop = () => { 91 | cancel(); 92 | setSpeakingMessageId(null); 93 | } 94 | 95 | return ( 96 | 97 | 98 | {visibleMessages.map((message) => message.content && ( 99 |
105 | {(message.role === "assistant") && ( 106 | 107 | 108 | AI 109 | 110 | )} 111 |
117 | {message.role === "assistant" && ( 118 |
119 | 127 | {isSpeaking && speakingMessageId === message.id && ( 128 | <> 129 | 137 | 145 | 146 | )} 147 |
148 | )} 149 | {message.role !== "system" && message.content} 150 |
151 | {(message.role === "user") && ( 152 | 153 | 154 | ME 155 | 156 | )} 157 |
158 | ))} 159 | {isLoading && ( 160 |
161 | 162 | 163 | AI 164 | 165 |
166 | 167 |
168 |
169 | )} 170 |
171 | 172 | 173 |
174 |