Spaces:
Running
Running
fahmiaziz98
commited on
Commit
·
2a51e7d
1
Parent(s):
dd38ce5
init
Browse files- app.py +90 -0
- apps/agent/__pycache__/constant.cpython-310.pyc +0 -0
- apps/agent/__pycache__/graph.cpython-310.pyc +0 -0
- apps/agent/__pycache__/state.cpython-310.pyc +0 -0
- apps/agent/__pycache__/tools.cpython-310.pyc +0 -0
- apps/agent/constant.py +26 -0
- apps/agent/graph.py +88 -0
- apps/agent/state.py +16 -0
- apps/agent/tools.py +37 -0
- apps/models.py +4 -0
- apps/service.py +53 -0
- run_api.py +5 -0
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)
|