fahmiaziz98 commited on
Commit
2a51e7d
·
1 Parent(s): dd38ce5
app.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from langchain_groq import ChatGroq
3
+ from apps.agent.constant import GROQ_API_KEY, MODEL_GROQ, CONFIG
4
+ from apps.agent.graph import Agent
5
+
6
+
7
+ llm = ChatGroq(model=MODEL_GROQ, api_key=GROQ_API_KEY, temperature=0.1)
8
+
9
+
10
+ agent = Agent(llm=llm)
11
+
12
+
13
+ def get_response(query: str):
14
+ response = agent.graph.invoke({"messages": ("user", query)}, CONFIG)
15
+ return response["messages"][-1].content
16
+
17
+ with st.sidebar:
18
+ st.header("Prof of Concept")
19
+ st.markdown(
20
+ """
21
+ This is just a prototype chatbot, the data taken is based on the following sites:
22
+ Xano Documentation
23
+ - https://docs.xano.com/about
24
+ - https://releases.xano.com/?_gl=1*sifgtw*_ga*MTI5NTY3MTk5NS4xNzMwNjMzNjY3*_ga_EJWDZRK3CG*MTczMDgwNjg3Mi43LjEuMTczMDgwNjkyMy45LjAuODUyNzA5OTA4
25
+ - https://docs.xano.com/onboarding-tutorial-reference
26
+ - https://docs.xano.com/faq
27
+ - https://docs.xano.com/about
28
+ - https://docs.xano.com/what-xano-includes
29
+ - https://docs.xano.com/what-xano-includes/instance
30
+ - https://docs.xano.com/what-xano-includes/workspace
31
+ - https://docs.xano.com/database/triggers
32
+ - https://docs.xano.com/fundamentals/the-development-life-cycle
33
+
34
+ WeWeb Documentation
35
+ - https://docs.weweb.io/start-here/welcome.html
36
+ - https://docs.weweb.io/start-here/frequently-asked-questions.html
37
+ - https://docs.weweb.io/editor/intro-to-the-editor.html
38
+ - https://docs.weweb.io/editor/intro-to-html-css.html
39
+ - https://docs.weweb.io/editor/how-to-use-the-add-panel.html
40
+ - https://docs.weweb.io/editor/logs.html
41
+ - https://docs.weweb.io/editor/copilot/import-figma-designs.html
42
+ - https://docs.weweb.io/editor/app-settings/app-settings.html
43
+ - https://docs.weweb.io/editor/app-settings/pwa.html
44
+ """
45
+ )
46
+
47
+ st.header("Example Question")
48
+ st.markdown(
49
+ """
50
+ Note: When asking a question, always add the word **xeno** or **weweb** so that the agent can easily find an accurate answer.
51
+
52
+ - What is PWA? and how enabling mobile app features in Weweb?
53
+ - How installing a PWA on a phone in WeWeb?
54
+ - Will the Marketplace have templates that I can use to start my backend with?
55
+ - Can I scale my backend with Xano?
56
+ """
57
+ )
58
+
59
+ st.title("AI Agent Assistance")
60
+
61
+ if "messages" not in st.session_state:
62
+ st.session_state.messages = []
63
+
64
+ for message in st.session_state.messages:
65
+ role = message.get("role", "assistant")
66
+ with st.chat_message(role):
67
+ if "output" in message:
68
+ st.markdown(message["output"])
69
+
70
+
71
+ if prompt := st.chat_input("What do you want to know?"):
72
+ st.chat_message("user").markdown(prompt)
73
+ st.session_state.messages.append({"role": "user", "output": prompt})
74
+
75
+ with st.spinner("Searching for an answer..."):
76
+ output_text = get_response(prompt)
77
+ print("Output", output_text)
78
+
79
+ # Display assistant response and SQL query
80
+ st.chat_message("assistant").markdown(output_text) # Kenapa ini tidak muncul di UI?
81
+
82
+
83
+ # Append assistant response to session state
84
+ st.session_state.messages.append(
85
+ {
86
+ "role": "assistant",
87
+ "output": output_text,
88
+ }
89
+ )
90
+
apps/agent/__pycache__/constant.cpython-310.pyc ADDED
Binary file (927 Bytes). View file
 
apps/agent/__pycache__/graph.cpython-310.pyc ADDED
Binary file (2.92 kB). View file
 
apps/agent/__pycache__/state.cpython-310.pyc ADDED
Binary file (1.1 kB). View file
 
apps/agent/__pycache__/tools.cpython-310.pyc ADDED
Binary file (1.88 kB). View file
 
apps/agent/constant.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from langchain_core.prompts import ChatPromptTemplate
3
+
4
+
5
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
6
+ MODEL_GROQ = "llama-3.1-8b-instant"
7
+ INDEX_NAME_XANO = "xano-index"
8
+ INDEX_NAME_WEWEB = "weweb-index"
9
+
10
+ CONFIG = {
11
+ "configurable" : {
12
+ "thread_id": "1234"
13
+ }
14
+ }
15
+
16
+ PROMPT = ChatPromptTemplate.from_messages(
17
+ [
18
+ (
19
+ "system",
20
+ "You are a knowledgeable instructor. Your job is to help students learn a tool, the data for which is retrieved from a documentation site"
21
+ "Answer questions directly and clearly, as if you were explaining to a student who needs precise and structured guidance."
22
+ "If the answer doesn't fit the given context, just say I don't have the information for that."
23
+ ),
24
+ ("placeholder", "{messages}")
25
+ ]
26
+ )
apps/agent/graph.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.checkpoint.memory import MemorySaver
2
+ from langgraph.store.memory import InMemoryStore
3
+ from langgraph.graph import StateGraph, START, END
4
+ from langgraph.prebuilt import ToolNode
5
+ from langchain_core.runnables import Runnable
6
+ from langchain_core.messages import AIMessage, ToolMessage
7
+ from langgraph.prebuilt import tools_condition
8
+
9
+ from langchain_groq import ChatGroq
10
+ from apps.agent.tools import tool_weweb, tool_xano # kalo run api pake ini -> app.agent.tools
11
+ from apps.agent.state import State, RequestAssistance
12
+ from apps.agent.constant import PROMPT
13
+
14
+
15
+ class Agent:
16
+ def __init__(self, llm: ChatGroq, memory=MemorySaver(), store=InMemoryStore() , prompt=PROMPT):
17
+ self.llm = llm
18
+ self.memory = memory
19
+ self.store = store
20
+ self.tools = [tool_xano, tool_weweb]
21
+ llm_with_tools = prompt | self.llm.bind_tools(self.tools + [RequestAssistance])
22
+
23
+ builder = StateGraph(State)
24
+ builder.add_node("chatbot", Assistant(llm_with_tools))
25
+ builder.add_node("tools", ToolNode(self.tools))
26
+ builder.add_node("human", self._human_node)
27
+ builder.add_conditional_edges(
28
+ "chatbot",
29
+ tools_condition,
30
+ {"human": "human", "tools": "tools", END: END},
31
+ )
32
+
33
+ builder.add_edge("tools", "chatbot")
34
+ builder.add_edge("human", "chatbot")
35
+ builder.add_edge(START, "chatbot")
36
+
37
+ self.graph = builder.compile(
38
+ checkpointer=self.memory,
39
+ store=self.store,
40
+ interrupt_after=["human"]
41
+ )
42
+
43
+ def _create_response(self, response: str, ai_message: AIMessage):
44
+ return ToolMessage(
45
+ content=response,
46
+ tool_call_id=ai_message.tool_calls[0]["id"],
47
+ )
48
+
49
+ def _human_node(self, state: State):
50
+ new_messages = []
51
+ if not isinstance(state["messages"][-1], ToolMessage):
52
+ # Typically, the user will have updated the state during the interrupt.
53
+ # If they choose not to, we will include a placeholder ToolMessage to
54
+ # let the LLM continue.
55
+ new_messages.append(
56
+ self._create_response("No response from human.", state["messages"][-1])
57
+ )
58
+ return {
59
+ # Append the new messages
60
+ "messages": new_messages,
61
+ # Unset the flag
62
+ "ask_human": False,
63
+ }
64
+
65
+
66
+ def _select_next_node(self, state: State):
67
+ if state["ask_human"]:
68
+ return "human"
69
+ # Otherwise, we can route as before
70
+ return tools_condition(state)
71
+
72
+
73
+ class Assistant:
74
+ def __init__(self, runnable: Runnable):
75
+ self.runnable = runnable
76
+
77
+ def __call__(self, state):
78
+ while True:
79
+ response = self.runnable.invoke(state)
80
+ # If the LLM happens to return an empty response, we will re-prompt it
81
+ # for an actual response.
82
+ ask_human = False
83
+
84
+ if (
85
+ response.tool_calls and response.tool_calls[0]["name"] == RequestAssistance.__name__
86
+ ):
87
+ ask_human = True
88
+ return {"messages": [response], "ask_human": ask_human}
apps/agent/state.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing_extensions import TypedDict
3
+ from langgraph.graph.message import add_messages, AnyMessage
4
+ from typing import Annotated, List
5
+
6
+ class State(TypedDict):
7
+ messages: Annotated[List[AnyMessage], add_messages]
8
+ ask_human: bool
9
+
10
+ class RequestAssistance(BaseModel):
11
+ """
12
+ Escalate the conversation to an expert. Use this if you are unable to assist directly or if the user requires support beyond your permissions.
13
+ To use this function, relay the user's 'request' so the expert can provide the right guidance.
14
+ """
15
+ request: str
16
+
apps/agent/tools.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from langchain_community.vectorstores.pinecone import Pinecone
3
+ from langchain_community.embeddings.fastembed import FastEmbedEmbeddings
4
+ from langchain.retrievers import ContextualCompressionRetriever
5
+ from langchain.retrievers.document_compressors import FlashrankRerank
6
+ from langchain_core.tools import tool
7
+
8
+ from apps.agent.constant import INDEX_NAME_WEWEB, INDEX_NAME_XANO
9
+
10
+ # os.environ["PINECONE_API_KEY"] = "a526d62f-ccca-40d6-859b-3d878c8d288b"
11
+
12
+ embeddings = FastEmbedEmbeddings(model_name="BAAI/bge-small-en-v1.5")
13
+ compressor = FlashrankRerank()
14
+
15
+ def create_compressed_retriever(index_name: str, embeddings, compressor) -> ContextualCompressionRetriever:
16
+ vectorstore = Pinecone.from_existing_index(embedding=embeddings, index_name=index_name)
17
+ retriever = vectorstore.as_retriever()
18
+ return ContextualCompressionRetriever(base_compressor=compressor, base_retriever=retriever)
19
+
20
+ reranker_xano = create_compressed_retriever(INDEX_NAME_XANO, embeddings, compressor)
21
+ reranker_weweb = create_compressed_retriever(INDEX_NAME_WEWEB, embeddings, compressor)
22
+
23
+ @tool
24
+ def tool_xano(query: str):
25
+ """
26
+ Searches and returns excerpts from the Xano documentation
27
+ """
28
+ docs = reranker_xano.invoke(query)
29
+ return "\n\n".join([doc["page_content"] for doc in docs])
30
+
31
+ @tool
32
+ def tool_weweb(query: str):
33
+ """
34
+ Searches and returns excerpts from the Weweb documentation
35
+ """
36
+ docs = reranker_weweb.invoke(query)
37
+ return "\n\n".join([doc["page_content"] for doc in docs])
apps/models.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+ class QueryInput(BaseModel):
4
+ query: str
apps/service.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from fastapi import FastAPI, HTTPException
3
+ from fastapi.responses import JSONResponse
4
+
5
+ from langgraph.errors import GraphRecursionError
6
+ from langchain_groq import ChatGroq
7
+ from apps.models import QueryInput
8
+ from apps.agent.graph import Agent
9
+ from apps.agent.constant import GROQ_API_KEY, MODEL_GROQ, CONFIG
10
+
11
+ logging.basicConfig(level=logging.INFO)
12
+ logger = logging.getLogger(__name__)
13
+
14
+ llm = ChatGroq(model=MODEL_GROQ, api_key=GROQ_API_KEY, temperature=0.1)
15
+ agent = Agent(llm=llm)
16
+
17
+ app = FastAPI(
18
+ title="Agent API",
19
+ description="API to interact with the RAG agent.",
20
+ version="0.1.0",
21
+ docs_url="/docs",
22
+ redoc_url="/redoc",
23
+ openapi_url="/openapi.json",
24
+ )
25
+
26
+ @app.get("/", summary="API Health Check", tags=["Health"])
27
+ async def health_check():
28
+ """Endpoint for checking the API status."""
29
+ return {"status": "API is running"}
30
+
31
+
32
+ @app.post("/query-agent", summary="Query the RAG Agent", tags=["Agent"])
33
+ async def query_rag_agent(query: QueryInput):
34
+ """ """
35
+ try:
36
+ output = agent.graph.invoke({"messages": ("user", query.query)}, CONFIG)
37
+
38
+ response = output["messages"][-1].content
39
+
40
+ logger.info(f"Processed query successfully: {query.query}")
41
+
42
+ return JSONResponse(
43
+ content={"response": response},
44
+ media_type="application/json",
45
+ status_code=200
46
+ )
47
+
48
+ except GraphRecursionError:
49
+ logger.error("Graph recursion limit reached; query processing failed.")
50
+ raise HTTPException(
51
+ status_code=500,
52
+ detail="Recursion limit reached. Could not generate response despite 25 attempts."
53
+ )
run_api.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import uvicorn
2
+ from apps.service import app
3
+
4
+ if __name__ == "__main__":
5
+ uvicorn.run(app, host="0.0.0.0", port=8000)