diff --git a/application/agents/classic_agent.py b/application/agents/classic_agent.py index 6fe73de0..fbffe77c 100644 --- a/application/agents/classic_agent.py +++ b/application/agents/classic_agent.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) class ClassicAgent(BaseAgent): - """A simplified classic agent with clear execution flow. + """A simplified agent with clear execution flow. Usage: 1. Processes a query through retrieval diff --git a/application/agents/tools/brave.py b/application/agents/tools/brave.py index 1428bea4..33843ac0 100644 --- a/application/agents/tools/brave.py +++ b/application/agents/tools/brave.py @@ -25,27 +25,35 @@ class BraveSearchTool(Tool): else: raise ValueError(f"Unknown action: {action_name}") - def _web_search(self, query, country="ALL", search_lang="en", count=10, - offset=0, safesearch="off", freshness=None, - result_filter=None, extra_snippets=False, summary=False): + def _web_search( + self, + query, + country="ALL", + search_lang="en", + count=10, + offset=0, + safesearch="off", + freshness=None, + result_filter=None, + extra_snippets=False, + summary=False, + ): """ Performs a web search using the Brave Search API. """ print(f"Performing Brave web search for: {query}") - + url = f"{self.base_url}/web/search" - - # Build query parameters + params = { "q": query, "country": country, "search_lang": search_lang, "count": min(count, 20), "offset": min(offset, 9), - "safesearch": safesearch + "safesearch": safesearch, } - - # Add optional parameters only if they have values + if freshness: params["freshness"] = freshness if result_filter: @@ -54,68 +62,69 @@ class BraveSearchTool(Tool): params["extra_snippets"] = 1 if summary: params["summary"] = 1 - - # Set up headers headers = { "Accept": "application/json", "Accept-Encoding": "gzip", - "X-Subscription-Token": self.token + "X-Subscription-Token": self.token, } - - # Make the request + response = requests.get(url, params=params, headers=headers) - + if response.status_code == 200: return { "status_code": response.status_code, "results": response.json(), - "message": "Search completed successfully." + "message": "Search completed successfully.", } else: return { "status_code": response.status_code, - "message": f"Search failed with status code: {response.status_code}." + "message": f"Search failed with status code: {response.status_code}.", } - - def _image_search(self, query, country="ALL", search_lang="en", count=5, - safesearch="off", spellcheck=False): + + def _image_search( + self, + query, + country="ALL", + search_lang="en", + count=5, + safesearch="off", + spellcheck=False, + ): """ Performs an image search using the Brave Search API. """ print(f"Performing Brave image search for: {query}") - + url = f"{self.base_url}/images/search" - - # Build query parameters + params = { "q": query, "country": country, "search_lang": search_lang, "count": min(count, 100), # API max is 100 "safesearch": safesearch, - "spellcheck": 1 if spellcheck else 0 + "spellcheck": 1 if spellcheck else 0, } - - # Set up headers + headers = { "Accept": "application/json", "Accept-Encoding": "gzip", - "X-Subscription-Token": self.token + "X-Subscription-Token": self.token, } - - # Make the request + response = requests.get(url, params=params, headers=headers) - + if response.status_code == 200: return { "status_code": response.status_code, "results": response.json(), - "message": "Image search completed successfully." + "message": "Image search completed successfully.", } else: return { "status_code": response.status_code, - "message": f"Image search failed with status code: {response.status_code}." + "message": f"Image search failed with status code: {response.status_code}.", } def get_actions_metadata(self): @@ -130,42 +139,14 @@ class BraveSearchTool(Tool): "type": "string", "description": "The search query (max 400 characters, 50 words)", }, - # "country": { - # "type": "string", - # "description": "The 2-character country code (default: US)", - # }, "search_lang": { "type": "string", "description": "The search language preference (default: en)", }, - # "count": { - # "type": "integer", - # "description": "Number of results to return (max 20, default: 10)", - # }, - # "offset": { - # "type": "integer", - # "description": "Pagination offset (max 9, default: 0)", - # }, - # "safesearch": { - # "type": "string", - # "description": "Filter level for adult content (off, moderate, strict)", - # }, "freshness": { "type": "string", "description": "Time filter for results (pd: last 24h, pw: last week, pm: last month, py: last year)", }, - # "result_filter": { - # "type": "string", - # "description": "Comma-delimited list of result types to include", - # }, - # "extra_snippets": { - # "type": "boolean", - # "description": "Get additional excerpts from result pages", - # }, - # "summary": { - # "type": "boolean", - # "description": "Enable summary generation in search results", - # } }, "required": ["query"], "additionalProperties": False, @@ -181,37 +162,21 @@ class BraveSearchTool(Tool): "type": "string", "description": "The search query (max 400 characters, 50 words)", }, - # "country": { - # "type": "string", - # "description": "The 2-character country code (default: US)", - # }, - # "search_lang": { - # "type": "string", - # "description": "The search language preference (default: en)", - # }, "count": { "type": "integer", "description": "Number of results to return (max 100, default: 5)", }, - # "safesearch": { - # "type": "string", - # "description": "Filter level for adult content (off, strict). Default: strict", - # }, - # "spellcheck": { - # "type": "boolean", - # "description": "Whether to spellcheck provided query (default: true)", - # } }, "required": ["query"], "additionalProperties": False, }, - } + }, ] def get_config_requirements(self): return { "token": { - "type": "string", - "description": "Brave Search API key for authentication" + "type": "string", + "description": "Brave Search API key for authentication", }, - } \ No newline at end of file + } diff --git a/application/agents/tools/duckduckgo.py b/application/agents/tools/duckduckgo.py new file mode 100644 index 00000000..87c1bc7e --- /dev/null +++ b/application/agents/tools/duckduckgo.py @@ -0,0 +1,114 @@ +from application.agents.tools.base import Tool +from duckduckgo_search import DDGS + + +class DuckDuckGoSearchTool(Tool): + """ + DuckDuckGo Search + A tool for performing web and image searches using DuckDuckGo. + """ + + def __init__(self, config): + self.config = config + + def execute_action(self, action_name, **kwargs): + actions = { + "ddg_web_search": self._web_search, + "ddg_image_search": self._image_search, + } + + if action_name in actions: + return actions[action_name](**kwargs) + else: + raise ValueError(f"Unknown action: {action_name}") + + def _web_search( + self, + query, + max_results=5, + ): + print(f"Performing DuckDuckGo web search for: {query}") + + try: + results = DDGS().text( + query, + max_results=max_results, + ) + + return { + "status_code": 200, + "results": results, + "message": "Web search completed successfully.", + } + except Exception as e: + return { + "status_code": 500, + "message": f"Web search failed: {str(e)}", + } + + def _image_search( + self, + query, + max_results=5, + ): + print(f"Performing DuckDuckGo image search for: {query}") + + try: + results = DDGS().images( + keywords=query, + max_results=max_results, + ) + + return { + "status_code": 200, + "results": results, + "message": "Image search completed successfully.", + } + except Exception as e: + return { + "status_code": 500, + "message": f"Image search failed: {str(e)}", + } + + def get_actions_metadata(self): + return [ + { + "name": "ddg_web_search", + "description": "Perform a web search using DuckDuckGo.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query", + }, + "max_results": { + "type": "integer", + "description": "Number of results to return (default: 5)", + }, + }, + "required": ["query"], + }, + }, + { + "name": "ddg_image_search", + "description": "Perform an image search using DuckDuckGo.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query", + }, + "max_results": { + "type": "integer", + "description": "Number of results to return (default: 5, max: 50)", + }, + }, + "required": ["query"], + }, + }, + ] + + def get_config_requirements(self): + return {} diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py index 469ea98c..2b9783f7 100644 --- a/application/api/answer/routes.py +++ b/application/api/answer/routes.py @@ -614,7 +614,7 @@ class Answer(Resource): try: question = data["question"] history = limit_chat_history( - json.loads(data.get("history", [])), gpt_model=gpt_model + json.loads(data.get("history", "[]")), gpt_model=gpt_model ) conversation_id = data.get("conversation_id") prompt_id = data.get("prompt_id", "default") diff --git a/application/api/user/routes.py b/application/api/user/routes.py index b1058203..c2f89761 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -879,29 +879,6 @@ class CombinedJson(Resource): "syncFrequency": index.get("sync_frequency", ""), } ) - if "duckduck_search" in settings.RETRIEVERS_ENABLED: - data.append( - { - "name": "DuckDuckGo Search", - "date": "duckduck_search", - "model": settings.EMBEDDINGS_NAME, - "location": "custom", - "tokens": "", - "retriever": "duckduck_search", - } - ) - if "brave_search" in settings.RETRIEVERS_ENABLED: - data.append( - { - "name": "Brave Search", - "language": "en", - "date": "brave_search", - "model": settings.EMBEDDINGS_NAME, - "location": "custom", - "tokens": "", - "retriever": "brave_search", - } - ) except Exception as err: current_app.logger.error(f"Error retrieving sources: {err}", exc_info=True) return make_response(jsonify({"success": False}), 400) diff --git a/application/core/settings.py b/application/core/settings.py index 7030022a..35e1bb75 100644 --- a/application/core/settings.py +++ b/application/core/settings.py @@ -33,7 +33,7 @@ class Settings(BaseSettings): VECTOR_STORE: str = ( "faiss" # "faiss" or "elasticsearch" or "qdrant" or "milvus" or "lancedb" ) - RETRIEVERS_ENABLED: list = ["classic_rag", "duckduck_search"] # also brave_search + RETRIEVERS_ENABLED: list = ["classic_rag"] AGENT_NAME: str = "classic" FALLBACK_LLM_PROVIDER: Optional[str] = None # provider for fallback llm FALLBACK_LLM_NAME: Optional[str] = None # model name for fallback llm @@ -99,7 +99,6 @@ class Settings(BaseSettings): LANCEDB_TABLE_NAME: Optional[str] = ( "docsgpts" # Name of the table to use for storing vectors ) - BRAVE_SEARCH_API_KEY: Optional[str] = None FLASK_DEBUG_MODE: bool = False STORAGE_TYPE: str = "local" # local or s3 diff --git a/application/retriever/brave_search.py b/application/retriever/brave_search.py deleted file mode 100644 index 123000e4..00000000 --- a/application/retriever/brave_search.py +++ /dev/null @@ -1,112 +0,0 @@ -import json - -from langchain_community.tools import BraveSearch - -from application.core.settings import settings -from application.llm.llm_creator import LLMCreator -from application.retriever.base import BaseRetriever - - -class BraveRetSearch(BaseRetriever): - - def __init__( - self, - source, - chat_history, - prompt, - chunks=2, - token_limit=150, - gpt_model="docsgpt", - user_api_key=None, - decoded_token=None, - ): - self.question = "" - self.source = source - self.chat_history = chat_history - self.prompt = prompt - self.chunks = chunks - self.gpt_model = gpt_model - self.token_limit = ( - token_limit - if token_limit - < settings.LLM_TOKEN_LIMITS.get( - self.gpt_model, settings.DEFAULT_MAX_HISTORY - ) - else settings.LLM_TOKEN_LIMITS.get( - self.gpt_model, settings.DEFAULT_MAX_HISTORY - ) - ) - self.user_api_key = user_api_key - self.decoded_token = decoded_token - - def _get_data(self): - if self.chunks == 0: - docs = [] - else: - search = BraveSearch.from_api_key( - api_key=settings.BRAVE_SEARCH_API_KEY, - search_kwargs={"count": int(self.chunks)}, - ) - results = search.run(self.question) - results = json.loads(results) - - docs = [] - for i in results: - try: - title = i["title"] - link = i["link"] - snippet = i["snippet"] - docs.append({"text": snippet, "title": title, "link": link}) - except IndexError: - pass - if settings.LLM_PROVIDER == "llama.cpp": - docs = [docs[0]] - - return docs - - def gen(self): - docs = self._get_data() - - # join all page_content together with a newline - docs_together = "\n".join([doc["text"] for doc in docs]) - p_chat_combine = self.prompt.replace("{summaries}", docs_together) - messages_combine = [{"role": "system", "content": p_chat_combine}] - for doc in docs: - yield {"source": doc} - - if len(self.chat_history) > 0: - for i in self.chat_history: - if "prompt" in i and "response" in i: - messages_combine.append({"role": "user", "content": i["prompt"]}) - messages_combine.append( - {"role": "assistant", "content": i["response"]} - ) - messages_combine.append({"role": "user", "content": self.question}) - - llm = LLMCreator.create_llm( - settings.LLM_PROVIDER, - api_key=settings.API_KEY, - user_api_key=self.user_api_key, - decoded_token=self.decoded_token, - ) - - completion = llm.gen_stream(model=self.gpt_model, messages=messages_combine) - for line in completion: - yield {"answer": str(line)} - - def search(self, query: str = ""): - if query: - self.question = query - return self._get_data() - - def get_params(self): - return { - "question": self.question, - "source": self.source, - "chat_history": self.chat_history, - "prompt": self.prompt, - "chunks": self.chunks, - "token_limit": self.token_limit, - "gpt_model": self.gpt_model, - "user_api_key": self.user_api_key, - } diff --git a/application/retriever/duckduck_search.py b/application/retriever/duckduck_search.py deleted file mode 100644 index 5abe5edd..00000000 --- a/application/retriever/duckduck_search.py +++ /dev/null @@ -1,111 +0,0 @@ -from langchain_community.tools import DuckDuckGoSearchResults -from langchain_community.utilities import DuckDuckGoSearchAPIWrapper - -from application.core.settings import settings -from application.llm.llm_creator import LLMCreator -from application.retriever.base import BaseRetriever - - -class DuckDuckSearch(BaseRetriever): - - def __init__( - self, - source, - chat_history, - prompt, - chunks=2, - token_limit=150, - gpt_model="docsgpt", - user_api_key=None, - decoded_token=None, - ): - self.question = "" - self.source = source - self.chat_history = chat_history - self.prompt = prompt - self.chunks = chunks - self.gpt_model = gpt_model - self.token_limit = ( - token_limit - if token_limit - < settings.LLM_TOKEN_LIMITS.get( - self.gpt_model, settings.DEFAULT_MAX_HISTORY - ) - else settings.LLM_TOKEN_LIMITS.get( - self.gpt_model, settings.DEFAULT_MAX_HISTORY - ) - ) - self.user_api_key = user_api_key - self.decoded_token = decoded_token - - def _get_data(self): - if self.chunks == 0: - docs = [] - else: - wrapper = DuckDuckGoSearchAPIWrapper(max_results=self.chunks) - search = DuckDuckGoSearchResults(api_wrapper=wrapper, output_format="list") - results = search.run(self.question) - - docs = [] - for i in results: - try: - docs.append( - { - "text": i.get("snippet", "").strip(), - "title": i.get("title", "").strip(), - "link": i.get("link", "").strip(), - } - ) - except IndexError: - pass - if settings.LLM_PROVIDER == "llama.cpp": - docs = [docs[0]] - - return docs - - def gen(self): - docs = self._get_data() - - # join all page_content together with a newline - docs_together = "\n".join([doc["text"] for doc in docs]) - p_chat_combine = self.prompt.replace("{summaries}", docs_together) - messages_combine = [{"role": "system", "content": p_chat_combine}] - for doc in docs: - yield {"source": doc} - - if len(self.chat_history) > 0: - for i in self.chat_history: - if "prompt" in i and "response" in i: - messages_combine.append({"role": "user", "content": i["prompt"]}) - messages_combine.append( - {"role": "assistant", "content": i["response"]} - ) - messages_combine.append({"role": "user", "content": self.question}) - - llm = LLMCreator.create_llm( - settings.LLM_PROVIDER, - api_key=settings.API_KEY, - user_api_key=self.user_api_key, - decoded_token=self.decoded_token, - ) - - completion = llm.gen_stream(model=self.gpt_model, messages=messages_combine) - for line in completion: - yield {"answer": str(line)} - - def search(self, query: str = ""): - if query: - self.question = query - return self._get_data() - - def get_params(self): - return { - "question": self.question, - "source": self.source, - "chat_history": self.chat_history, - "prompt": self.prompt, - "chunks": self.chunks, - "token_limit": self.token_limit, - "gpt_model": self.gpt_model, - "user_api_key": self.user_api_key, - } diff --git a/application/retriever/retriever_creator.py b/application/retriever/retriever_creator.py index 26cb41ca..e51be42f 100644 --- a/application/retriever/retriever_creator.py +++ b/application/retriever/retriever_creator.py @@ -1,13 +1,9 @@ from application.retriever.classic_rag import ClassicRAG -from application.retriever.duckduck_search import DuckDuckSearch -from application.retriever.brave_search import BraveRetSearch class RetrieverCreator: retrievers = { "classic": ClassicRAG, - "duckduck_search": DuckDuckSearch, - "brave_search": BraveRetSearch, "default": ClassicRAG, } diff --git a/docs/pages/Agents/_meta.json b/docs/pages/Agents/_meta.json index f5d0fe6e..857a6c30 100644 --- a/docs/pages/Agents/_meta.json +++ b/docs/pages/Agents/_meta.json @@ -2,5 +2,13 @@ "basics": { "title": "🤖 Agent Basics", "href": "/Agents/basics" + }, + "api": { + "title": "🔌 Agent API", + "href": "/Agents/api" + }, + "webhooks": { + "title": "🪝 Agent Webhooks", + "href": "/Agents/webhooks" } -} \ No newline at end of file +} diff --git a/docs/pages/Agents/api.mdx b/docs/pages/Agents/api.mdx new file mode 100644 index 00000000..18d4e763 --- /dev/null +++ b/docs/pages/Agents/api.mdx @@ -0,0 +1,227 @@ +--- +title: Interacting with Agents via API +description: Learn how to programmatically interact with DocsGPT Agents using the streaming and non-streaming API endpoints. +--- + +import { Callout, Tabs } from 'nextra/components'; + +# Interacting with Agents via API + +DocsGPT Agents can be accessed programmatically through a dedicated API, allowing you to integrate their specialized capabilities into your own applications, scripts, and workflows. This guide covers the two primary methods for interacting with an agent: the streaming API for real-time responses and the non-streaming API for a single, consolidated answer. + +When you use an API key generated for a specific agent, you do not need to pass `prompt`, `tools` etc. The agent's configuration (including its prompt, selected tools, and knowledge sources) is already associated with its unique API key. + +### API Endpoints + +- **Non-Streaming:** `http://localhost:7091/api/answer` +- **Streaming:** `http://localhost:7091/stream` + + +For DocsGPT Cloud, use `https://gptcloud.arc53.com/` as the base URL. + + +For more technical details, you can explore the API swagger documentation available for the cloud version or your local instance. + +--- + +## Non-Streaming API (`/api/answer`) + +This is a standard synchronous endpoint. It waits for the agent to fully process the request and returns a single JSON object with the complete answer. This is the simplest method and is ideal for backend processes where a real-time feed is not required. + +### Request + +- **Endpoint:** `/api/answer` +- **Method:** `POST` +- **Payload:** + - `question` (string, required): The user's query or input for the agent. + - `api_key` (string, required): The unique API key for the agent you wish to interact with. + - `history` (string, optional): A JSON string representing the conversation history, e.g., `[{\"prompt\": \"first question\", \"answer\": \"first answer\"}]`. + +### Response + +A single JSON object containing: +- `answer`: The complete, final answer from the agent. +- `sources`: A list of sources the agent consulted. +- `conversation_id`: The unique ID for the interaction. + +### Examples + + + + ```bash + curl -X POST http://localhost:7091/api/answer \ + -H "Content-Type: application/json" \ + -d '{ + "question": "your question here", + "api_key": "your_agent_api_key" + }' + ``` + + + ```python + import requests + + API_URL = "http://localhost:7091/api/answer" + API_KEY = "your_agent_api_key" + QUESTION = "your question here" + + response = requests.post( + API_URL, + json={"question": QUESTION, "api_key": API_KEY} + ) + + if response.status_code == 200: + print(response.json()) + else: + print(f"Error: {response.status_code}") + print(response.text) + ``` + + + ```javascript + const apiUrl = 'http://localhost:7091/api/answer'; + const apiKey = 'your_agent_api_key'; + const question = 'your question here'; + + async function getAnswer() { + try { + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ question, api_key: apiKey }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + console.log(data); + } catch (error) { + console.error("Failed to fetch answer:", error); + } + } + + getAnswer(); + ``` + + + +--- + +## Streaming API (`/stream`) + +The `/stream` endpoint uses Server-Sent Events (SSE) to push data in real-time. This is ideal for applications where you want to display the response as it's being generated, such as in a live chatbot interface. + +### Request + +- **Endpoint:** `/stream` +- **Method:** `POST` +- **Payload:** Same as the non-streaming API. + +### Response (SSE Stream) + +The stream consists of multiple `data:` events, each containing a JSON object. Your client should listen for these events and process them based on their `type`. + +**Event Types:** +- `answer`: A chunk of the agent's final answer. +- `source`: A document or source used by the agent. +- `thought`: A reasoning step from the agent (for ReAct agents). +- `id`: The unique `conversation_id` for the interaction. +- `error`: An error message. +- `end`: A final message indicating the stream has concluded. + +### Examples + + + + ```bash + curl -X POST http://localhost:7091/stream \ + -H "Content-Type: application/json" \ + -H "Accept: text/event-stream" \ + -d '{ + "question": "your question here", + "api_key": "your_agent_api_key" + }' + ``` + + + ```python + import requests + import json + + API_URL = "http://localhost:7091/stream" + payload = { + "question": "your question here", + "api_key": "your_agent_api_key" + } + + with requests.post(API_URL, json=payload, stream=True) as r: + for line in r.iter_lines(): + if line: + decoded_line = line.decode('utf-8') + if decoded_line.startswith('data: '): + try: + data = json.loads(decoded_line[6:]) + print(data) + except json.JSONDecodeError: + pass + ``` + + + ```javascript + const apiUrl = 'http://localhost:7091/stream'; + const apiKey = 'your_agent_api_key'; + const question = 'your question here'; + + async function getStream() { + try { + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'text/event-stream' + }, + // Corrected line: 'apiKey' is changed to 'api_key' + body: JSON.stringify({ question, api_key: apiKey }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + // Note: This parsing method assumes each chunk contains whole lines. + // For a more robust production implementation, buffer the chunks + // and process them line by line. + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const data = JSON.parse(line.substring(6)); + console.log(data); + } catch (e) { + console.error("Failed to parse JSON from SSE event:", e); + } + } + } + } + } catch (error) { + console.error("Failed to fetch stream:", error); + } + } + + getStream(); + ``` + + diff --git a/docs/pages/Agents/webhooks.mdx b/docs/pages/Agents/webhooks.mdx new file mode 100644 index 00000000..814690b5 --- /dev/null +++ b/docs/pages/Agents/webhooks.mdx @@ -0,0 +1,152 @@ +--- +title: Triggering Agents with Webhooks +description: Learn how to automate and integrate DocsGPT Agents using webhooks for asynchronous task execution. +--- + +import { Callout, Tabs } from 'nextra/components'; + +# Triggering Agents with Webhooks + +Agent Webhooks provide a powerful mechanism to trigger an agent's execution from external systems. Unlike the direct API which provides an immediate response, webhooks are designed for **asynchronous** operations. When you call a webhook, DocsGPT enqueues the agent's task for background processing and immediately returns a `task_id`. You then use this ID to poll for the result. + +This workflow is ideal for integrating with services that expect a quick initial response (e.g., form submissions) or for triggering long-running tasks without tying up a client connection. + +Each agent has its own unique webhook URL, which can be generated from the agent's edit page in the DocsGPT UI. This URL includes a secure token for authentication. + +### API Endpoints + +- **Webhook URL:** `http://localhost:7091/api/webhooks/agents/{AGENT_WEBHOOK_TOKEN}` +- **Task Status URL:** `http://localhost:7091/api/task_status` + + +For DocsGPT Cloud, use `https://gptcloud.arc53.com/` as the base URL. + + +For more technical details, you can explore the API swagger documentation available for the cloud version or your local instance. + +--- + +## The Webhook Workflow + +The process involves two main steps: triggering the task and polling for the result. + +### Step 1: Trigger the Webhook + +Send an HTTP `POST` request to the agent's unique webhook URL with the required payload. The structure of this payload should match what the agent's prompt and tools are designed to handle. + +- **Method:** `POST` +- **Response:** A JSON object with a `task_id`. `{"task_id": "a1b2c3d4-e5f6-..."}` + + + + ```bash + curl -X POST \ + http://localhost:7091/api/webhooks/agents/your_webhook_token \ + -H "Content-Type: application/json" \ + -d '{"question": "Your message to agent"}' + ``` + + + ```python + import requests + + WEBHOOK_URL = "http://localhost:7091/api/webhooks/agents/your_webhook_token" + payload = {"question": "Your message to agent"} + + try: + response = requests.post(WEBHOOK_URL, json=payload) + response.raise_for_status() + task_id = response.json().get("task_id") + print(f"Task successfully created with ID: {task_id}") + except requests.exceptions.RequestException as e: + print(f"Error triggering webhook: {e}") + ``` + + + ```javascript + const webhookUrl = 'http://localhost:7091/api/webhooks/agents/your_webhook_token'; + const payload = { question: 'Your message to agent' }; + + async function triggerWebhook() { + try { + const response = await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (!response.ok) throw new Error(`HTTP error! ${response.status}`); + const data = await response.json(); + console.log(`Task successfully created with ID: ${data.task_id}`); + return data.task_id; + } catch (error) { + console.error('Error triggering webhook:', error); + } + } + + triggerWebhook(); + ``` + + + +### Step 2: Poll for the Result + +Once you have the `task_id`, periodically send a `GET` request to the `/api/task_status` endpoint until the task `status` is `SUCCESS` or `FAILURE`. + +- **`status`**: The current state of the task (`PENDING`, `STARTED`, `SUCCESS`, `FAILURE`). +- **`result`**: The final output from the agent, available when the status is `SUCCESS` or `FAILURE`. + + + + ```bash + # Replace the task_id with the one you received + curl http://localhost:7091/api/task_status?task_id=YOUR_TASK_ID + ``` + + + ```python + import requests + import time + + STATUS_URL = "http://localhost:7091/api/task_status" + task_id = "YOUR_TASK_ID" + + while True: + response = requests.get(STATUS_URL, params={"task_id": task_id}) + data = response.json() + status = data.get("status") + print(f"Current task status: {status}") + + if status in ["SUCCESS", "FAILURE"]: + print("Final Result:") + print(data.get("result")) + break + + time.sleep(2) + ``` + + + ```javascript + const statusUrl = 'http://localhost:7091/api/task_status'; + const taskId = 'YOUR_TASK_ID'; + + const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); + + async function pollForResult() { + while (true) { + const response = await fetch(`${statusUrl}?task_id=${taskId}`); + const data = await response.json(); + const status = data.status; + console.log(`Current task status: ${status}`); + + if (status === 'SUCCESS' || status === 'FAILURE') { + console.log('Final Result:', data.result); + break; + } + await sleep(2000); + } + } + + pollForResult(); + ``` + + diff --git a/frontend/public/toolIcons/tool_duckduckgo.svg b/frontend/public/toolIcons/tool_duckduckgo.svg new file mode 100644 index 00000000..8215a918 --- /dev/null +++ b/frontend/public/toolIcons/tool_duckduckgo.svg @@ -0,0 +1 @@ +duckduckgo \ No newline at end of file diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index d59726b0..6379eb0a 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -288,136 +288,101 @@ const ConversationBubble = forwardRef< {DisableSourceFE || type === 'ERROR' || sources?.length === 0 || - sources?.some((source) => source.link === 'None') ? null : !sources && - chunks !== '0' && - selectedDocs ? ( -
-
- source.link === 'None') + ? null + : sources && ( +
+
+ + } /> - } - /> -

- {t('conversation.sources.title')} -

-
-
- {Array.from({ length: 4 }).map((_, index) => ( -
- - - - - - +

+ {t('conversation.sources.title')} +

- ))} -
-
- ) : ( - sources && ( -
-
- - } - /> -

- {t('conversation.sources.title')} -

-
-
-
- {sources?.slice(0, 3)?.map((source, index) => ( -
-
setActiveTooltip(index)} - onMouseOut={() => setActiveTooltip(null)} - > -

- {source.text} -

+
+
+ {sources?.slice(0, 3)?.map((source, index) => ( +
- source.link && source.link !== 'local' - ? window.open( - source.link, - '_blank', - 'noopener, noreferrer', - ) - : null - } - > - Document -

- {source.link && source.link !== 'local' - ? source.link - : source.title} -

-
-
- {activeTooltip === index && ( -
setActiveTooltip(index)} onMouseOut={() => setActiveTooltip(null)} > -

+

{source.text}

+
+ source.link && source.link !== 'local' + ? window.open( + source.link, + '_blank', + 'noopener, noreferrer', + ) + : null + } + > + Document +

+ {source.link && source.link !== 'local' + ? source.link + : source.title} +

+
- )} -
- ))} - {(sources?.length ?? 0) > 3 && ( -
setIsSidebarOpen(true)} - > -

- {t('conversation.sources.view_more', { - count: sources?.length ? sources.length - 3 : 0, - })} -

-
- )} + {activeTooltip === index && ( +
setActiveTooltip(index)} + onMouseOut={() => setActiveTooltip(null)} + > +

+ {source.text} +

+
+ )} +
+ ))} + {(sources?.length ?? 0) > 3 && ( +
setIsSidebarOpen(true)} + > +

+ {t('conversation.sources.view_more', { + count: sources?.length ? sources.length - 3 : 0, + })} +

+
+ )} +
-
- ) - )} + )} {toolCalls && toolCalls.length > 0 && ( )} diff --git a/frontend/src/conversation/conversationSlice.ts b/frontend/src/conversation/conversationSlice.ts index 708119f3..b3eb92f9 100644 --- a/frontend/src/conversation/conversationSlice.ts +++ b/frontend/src/conversation/conversationSlice.ts @@ -284,14 +284,9 @@ export const conversationSlice = createSlice({ query: Partial; }>, ) { - const { conversationId, index, query } = action.payload; - if (state.conversationId !== conversationId) return; - - if (!state.queries[index].sources) { - state.queries[index].sources = query?.sources; - } else if (query.sources) { - state.queries[index].sources!.push(...query.sources); - } + const { index, query } = action.payload; + if (query.sources !== undefined) + state.queries[index].sources = query.sources; }, updateToolCall(state, action) { const { index, tool_call } = action.payload; diff --git a/frontend/src/modals/ChunkModal.tsx b/frontend/src/modals/ChunkModal.tsx index 00a1491f..e24fc5b9 100644 --- a/frontend/src/modals/ChunkModal.tsx +++ b/frontend/src/modals/ChunkModal.tsx @@ -33,6 +33,19 @@ export default function ChunkModal({ setChunkText(originalText || ''); }, [originalTitle, originalText]); + const resetForm = () => { + setTitle(''); + setChunkText(''); + }; + + const handleDeleteConfirmed = () => { + if (handleDelete) { + handleDelete(); + } + setDeleteModal('INACTIVE'); + setModalState('INACTIVE'); + }; + if (modalState !== 'ACTIVE') return null; const content = ( @@ -71,6 +84,7 @@ export default function ChunkModal({ onClick={() => { handleSubmit(title, chunkText); setModalState('INACTIVE'); + resetForm(); }} className="bg-purple-30 hover:bg-violets-are-blue rounded-3xl px-5 py-2 text-sm text-white transition-all" > @@ -79,6 +93,7 @@ export default function ChunkModal({ diff --git a/frontend/src/modals/WrapperModal.tsx b/frontend/src/modals/WrapperModal.tsx index d2ab8e4f..5c580d1c 100644 --- a/frontend/src/modals/WrapperModal.tsx +++ b/frontend/src/modals/WrapperModal.tsx @@ -3,38 +3,33 @@ import { createPortal } from 'react-dom'; import Exit from '../assets/exit.svg'; -interface WrapperModalPropsType { +type WrapperModalPropsType = { children: React.ReactNode; close: () => void; isPerformingTask?: boolean; className?: string; contentClassName?: string; -} +}; export default function WrapperModal({ children, close, isPerformingTask = false, - className = '', // Default width, but can be overridden - contentClassName = '', // Default padding, but can be overridden + className = '', + contentClassName = '', }: WrapperModalPropsType) { const modalRef = useRef(null); useEffect(() => { if (isPerformingTask) return; + const handleClickOutside = (event: MouseEvent) => { - if ( - modalRef.current && - !modalRef.current.contains(event.target as Node) - ) { + if (modalRef.current && !modalRef.current.contains(event.target as Node)) close(); - } }; const handleEscapePress = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - close(); - } + if (event.key === 'Escape') close(); }; document.addEventListener('mousedown', handleClickOutside); @@ -44,17 +39,17 @@ export default function WrapperModal({ document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleEscapePress); }; - }, [close]); + }, [close, isPerformingTask]); const modalContent = ( -
+
{!isPerformingTask && (
); }