From cb99a3b7ecd49b275b450675c19456c7fa50642d Mon Sep 17 00:00:00 2001 From: "estevez.sebastian@gmail.com" Date: Fri, 18 Jul 2025 14:10:11 -0400 Subject: [PATCH] streaming and tool calling in ui --- frontend/src/app/chat/page.tsx | 561 ++++++++++++++++++++++++++++++--- pyproject.toml | 2 +- src/agent.py | 125 +++++++- src/app.py | 46 ++- uv.lock | 8 +- 5 files changed, 661 insertions(+), 81 deletions(-) diff --git a/frontend/src/app/chat/page.tsx b/frontend/src/app/chat/page.tsx index 20b47215..842e6867 100644 --- a/frontend/src/app/chat/page.tsx +++ b/frontend/src/app/chat/page.tsx @@ -4,12 +4,22 @@ import { useState, useRef, useEffect } from "react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" -import { MessageCircle, Send, Loader2, User, Bot } from "lucide-react" +import { MessageCircle, Send, Loader2, User, Bot, Zap, Settings, ChevronDown, ChevronRight } from "lucide-react" interface Message { role: "user" | "assistant" content: string timestamp: Date + functionCalls?: FunctionCall[] + isStreaming?: boolean +} + +interface FunctionCall { + name: string + arguments?: Record + result?: Record + status: "pending" | "completed" | "error" + argumentsString?: string } type EndpointType = "chat" | "langflow" @@ -19,6 +29,13 @@ export default function ChatPage() { const [input, setInput] = useState("") const [loading, setLoading] = useState(false) const [endpoint, setEndpoint] = useState("chat") + const [asyncMode, setAsyncMode] = useState(false) + const [streamingMessage, setStreamingMessage] = useState<{ + content: string + functionCalls: FunctionCall[] + timestamp: Date + } | null>(null) + const [expandedFunctionCalls, setExpandedFunctionCalls] = useState>(new Set()) const messagesEndRef = useRef(null) const scrollToBottom = () => { @@ -27,7 +44,310 @@ export default function ChatPage() { useEffect(() => { scrollToBottom() - }, [messages]) + }, [messages, streamingMessage]) + + const handleSSEStream = async (userMessage: Message) => { + const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow" + + try { + const response = await fetch(apiEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + prompt: userMessage.content, + stream: true + }), + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const reader = response.body?.getReader() + if (!reader) { + throw new Error("No reader available") + } + + const decoder = new TextDecoder() + let buffer = "" + let currentContent = "" + const currentFunctionCalls: FunctionCall[] = [] + + // Initialize streaming message + setStreamingMessage({ + content: "", + functionCalls: [], + timestamp: new Date() + }) + + try { + while (true) { + const { done, value } = await reader.read() + + if (done) break + + buffer += decoder.decode(value, { stream: true }) + + // Process complete lines (JSON objects) + const lines = buffer.split('\n') + buffer = lines.pop() || "" // Keep incomplete line in buffer + + for (const line of lines) { + if (line.trim()) { + try { + const chunk = JSON.parse(line) + console.log("Received chunk:", chunk.type || chunk.object, chunk) + + // Handle OpenAI Chat Completions streaming format + if (chunk.object === "response.chunk" && chunk.delta) { + // Handle function calls in delta + if (chunk.delta.function_call) { + console.log("Function call in delta:", chunk.delta.function_call) + + // Check if this is a new function call + if (chunk.delta.function_call.name) { + console.log("New function call:", chunk.delta.function_call.name) + const functionCall: FunctionCall = { + name: chunk.delta.function_call.name, + arguments: undefined, + status: "pending", + argumentsString: chunk.delta.function_call.arguments || "" + } + currentFunctionCalls.push(functionCall) + console.log("Added function call:", functionCall) + } + // Or if this is arguments continuation + else if (chunk.delta.function_call.arguments) { + console.log("Function call arguments delta:", chunk.delta.function_call.arguments) + const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1] + if (lastFunctionCall) { + if (!lastFunctionCall.argumentsString) { + lastFunctionCall.argumentsString = "" + } + lastFunctionCall.argumentsString += chunk.delta.function_call.arguments + console.log("Accumulated arguments:", lastFunctionCall.argumentsString) + + // Try to parse arguments if they look complete + if (lastFunctionCall.argumentsString.includes("}")) { + try { + const parsed = JSON.parse(lastFunctionCall.argumentsString) + lastFunctionCall.arguments = parsed + lastFunctionCall.status = "completed" + console.log("Parsed function arguments:", parsed) + } catch (e) { + console.log("Arguments not yet complete or invalid JSON:", e) + } + } + } + } + } + + // Handle tool calls in delta + else if (chunk.delta.tool_calls && Array.isArray(chunk.delta.tool_calls)) { + console.log("Tool calls in delta:", chunk.delta.tool_calls) + + for (const toolCall of chunk.delta.tool_calls) { + if (toolCall.function) { + // Check if this is a new tool call + if (toolCall.function.name) { + console.log("New tool call:", toolCall.function.name) + const functionCall: FunctionCall = { + name: toolCall.function.name, + arguments: undefined, + status: "pending", + argumentsString: toolCall.function.arguments || "" + } + currentFunctionCalls.push(functionCall) + console.log("Added tool call:", functionCall) + } + // Or if this is arguments continuation + else if (toolCall.function.arguments) { + console.log("Tool call arguments delta:", toolCall.function.arguments) + const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1] + if (lastFunctionCall) { + if (!lastFunctionCall.argumentsString) { + lastFunctionCall.argumentsString = "" + } + lastFunctionCall.argumentsString += toolCall.function.arguments + console.log("Accumulated tool arguments:", lastFunctionCall.argumentsString) + + // Try to parse arguments if they look complete + if (lastFunctionCall.argumentsString.includes("}")) { + try { + const parsed = JSON.parse(lastFunctionCall.argumentsString) + lastFunctionCall.arguments = parsed + lastFunctionCall.status = "completed" + console.log("Parsed tool arguments:", parsed) + } catch (e) { + console.log("Tool arguments not yet complete or invalid JSON:", e) + } + } + } + } + } + } + } + + // Handle content/text in delta + else if (chunk.delta.content) { + console.log("Content delta:", chunk.delta.content) + currentContent += chunk.delta.content + } + + // Handle finish reason + if (chunk.delta.finish_reason) { + console.log("Finish reason:", chunk.delta.finish_reason) + // Mark any pending function calls as completed + currentFunctionCalls.forEach(fc => { + if (fc.status === "pending" && fc.argumentsString) { + try { + fc.arguments = JSON.parse(fc.argumentsString) + fc.status = "completed" + console.log("Completed function call on finish:", fc) + } catch (e) { + fc.arguments = { raw: fc.argumentsString } + fc.status = "error" + console.log("Error parsing function call on finish:", fc, e) + } + } + }) + } + } + + // Handle Realtime API format (this is what you're actually getting!) + else if (chunk.type === "response.output_item.added" && chunk.item?.type === "function_call") { + console.log("Function call started (Realtime API):", chunk.item.name) + const functionCall: FunctionCall = { + name: chunk.item.name || "unknown", + arguments: undefined, + status: "pending", + argumentsString: "" + } + currentFunctionCalls.push(functionCall) + } + + // Handle function call arguments streaming (Realtime API) + else if (chunk.type === "response.function_call_arguments.delta") { + console.log("Function args delta (Realtime API):", chunk.delta) + const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1] + if (lastFunctionCall) { + if (!lastFunctionCall.argumentsString) { + lastFunctionCall.argumentsString = "" + } + lastFunctionCall.argumentsString += chunk.delta || "" + console.log("Accumulated arguments (Realtime API):", lastFunctionCall.argumentsString) + } + } + + // Handle function call arguments completion (Realtime API) + else if (chunk.type === "response.function_call_arguments.done") { + console.log("Function args done (Realtime API):", chunk.arguments) + const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1] + if (lastFunctionCall) { + try { + lastFunctionCall.arguments = JSON.parse(chunk.arguments || "{}") + lastFunctionCall.status = "completed" + console.log("Parsed function arguments (Realtime API):", lastFunctionCall.arguments) + } catch (e) { + lastFunctionCall.arguments = { raw: chunk.arguments } + lastFunctionCall.status = "error" + console.log("Error parsing function arguments (Realtime API):", e) + } + } + } + + // Handle function call completion (Realtime API) + else if (chunk.type === "response.output_item.done" && chunk.item?.type === "function_call") { + console.log("Function call done (Realtime API):", chunk.item.status) + const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1] + if (lastFunctionCall) { + lastFunctionCall.status = chunk.item.status === "completed" ? "completed" : "error" + } + } + + // Handle function call results + else if (chunk.type === "response.function_call.result" || chunk.type === "function_call_result") { + console.log("Function call result:", chunk.result || chunk) + const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1] + if (lastFunctionCall) { + lastFunctionCall.result = chunk.result || chunk.output || chunk.response + lastFunctionCall.status = "completed" + } + } + + // Handle tool call results + else if (chunk.type === "response.tool_call.result" || chunk.type === "tool_call_result") { + console.log("Tool call result:", chunk.result || chunk) + const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1] + if (lastFunctionCall) { + lastFunctionCall.result = chunk.result || chunk.output || chunk.response + lastFunctionCall.status = "completed" + } + } + + // Handle generic results that might be in different formats + else if ((chunk.type && chunk.type.includes("result")) || chunk.result) { + console.log("Generic result:", chunk) + const lastFunctionCall = currentFunctionCalls[currentFunctionCalls.length - 1] + if (lastFunctionCall && !lastFunctionCall.result) { + lastFunctionCall.result = chunk.result || chunk.output || chunk.response || chunk + lastFunctionCall.status = "completed" + } + } + + // Handle text output streaming (Realtime API) + else if (chunk.type === "response.output_text.delta") { + console.log("Text delta (Realtime API):", chunk.delta) + currentContent += chunk.delta || "" + } + + // Log unhandled chunks + else if (chunk.type !== null && chunk.object !== "response.chunk") { + console.log("Unhandled chunk format:", chunk) + } + + // Update streaming message + setStreamingMessage({ + content: currentContent, + functionCalls: [...currentFunctionCalls], + timestamp: new Date() + }) + + } catch (parseError) { + console.warn("Failed to parse chunk:", line, parseError) + } + } + } + } + } finally { + reader.releaseLock() + } + + // Finalize the message + const finalMessage: Message = { + role: "assistant", + content: currentContent, + functionCalls: currentFunctionCalls, + timestamp: new Date() + } + + setMessages(prev => [...prev, finalMessage]) + setStreamingMessage(null) + + } catch (error) { + console.error("SSE Stream error:", error) + setStreamingMessage(null) + + const errorMessage: Message = { + role: "assistant", + content: "Sorry, I couldn't connect to the chat service. Please try again.", + timestamp: new Date() + } + setMessages(prev => [...prev, errorMessage]) + } + } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -43,45 +363,127 @@ export default function ChatPage() { setInput("") setLoading(true) - try { - const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow" - const response = await fetch(apiEndpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ prompt: userMessage.content }), - }) + if (asyncMode) { + await handleSSEStream(userMessage) + } else { + // Original non-streaming logic + try { + const apiEndpoint = endpoint === "chat" ? "/api/chat" : "/api/langflow" + const response = await fetch(apiEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ prompt: userMessage.content }), + }) - const result = await response.json() - - if (response.ok) { - const assistantMessage: Message = { - role: "assistant", - content: result.response, - timestamp: new Date() + const result = await response.json() + + if (response.ok) { + const assistantMessage: Message = { + role: "assistant", + content: result.response, + timestamp: new Date() + } + setMessages(prev => [...prev, assistantMessage]) + } else { + console.error("Chat failed:", result.error) + const errorMessage: Message = { + role: "assistant", + content: "Sorry, I encountered an error. Please try again.", + timestamp: new Date() + } + setMessages(prev => [...prev, errorMessage]) } - setMessages(prev => [...prev, assistantMessage]) - } else { - console.error("Chat failed:", result.error) + } catch (error) { + console.error("Chat error:", error) const errorMessage: Message = { role: "assistant", - content: "Sorry, I encountered an error. Please try again.", + content: "Sorry, I couldn't connect to the chat service. Please try again.", timestamp: new Date() } setMessages(prev => [...prev, errorMessage]) } - } catch (error) { - console.error("Chat error:", error) - const errorMessage: Message = { - role: "assistant", - content: "Sorry, I couldn't connect to the chat service. Please try again.", - timestamp: new Date() - } - setMessages(prev => [...prev, errorMessage]) - } finally { - setLoading(false) } + + setLoading(false) + } + + const toggleFunctionCall = (functionCallId: string) => { + setExpandedFunctionCalls(prev => { + const newSet = new Set(prev) + if (newSet.has(functionCallId)) { + newSet.delete(functionCallId) + } else { + newSet.add(functionCallId) + } + return newSet + }) + } + + const renderFunctionCalls = (functionCalls: FunctionCall[], messageIndex?: number) => { + if (!functionCalls || functionCalls.length === 0) return null + + return ( +
+ {functionCalls.map((fc, index) => { + const functionCallId = `${messageIndex || 'streaming'}-${index}` + const isExpanded = expandedFunctionCalls.has(functionCallId) + + return ( +
+
toggleFunctionCall(functionCallId)} + > + + + Function Call: {fc.name} + +
+ {fc.status} +
+ {isExpanded ? ( + + ) : ( + + )} +
+ + {isExpanded && ( +
+ {/* Show arguments - either completed or streaming */} + {(fc.arguments || fc.argumentsString) && ( +
+ Arguments: +
+                        {fc.arguments 
+                          ? JSON.stringify(fc.arguments, null, 2)
+                          : fc.argumentsString || "..."
+                        }
+                      
+
+ )} + + {fc.result && ( +
+ Result: +
+                        {JSON.stringify(fc.result, null, 2)}
+                      
+
+ )} +
+ )} +
+ ) + })} +
+ ) } return ( @@ -98,33 +500,57 @@ export default function ChatPage() { Chat -
- - +
+ {/* Async Mode Toggle */} +
+ + +
+ {/* Endpoint Toggle */} +
+ + +
- Chat with AI about your indexed documents using {endpoint === "chat" ? "Chat" : "Langflow"} endpoint + Chat with AI about your indexed documents using {endpoint === "chat" ? "Chat" : "Langflow"} endpoint + {asyncMode ? " with real-time streaming" : ""} {/* Messages Area */}
- {messages.length === 0 ? ( + {messages.length === 0 && !streamingMessage ? (
@@ -168,6 +594,7 @@ export default function ChatPage() { {message.timestamp.toLocaleTimeString()}
+ {renderFunctionCalls(message.functionCalls || [], index)}

{message.content}

@@ -175,7 +602,37 @@ export default function ChatPage() { )} ))} - {loading && ( + + {/* Streaming Message Display */} + {streamingMessage && ( +
+
+
+ +
+ AI + gpt-4.1 +
+
+
+
+ + Streaming... + + {streamingMessage.timestamp.toLocaleTimeString()} + +
+ {renderFunctionCalls(streamingMessage.functionCalls, messages.length)} +

+ {streamingMessage.content} + +

+
+
+
+ )} + + {loading && !asyncMode && (
diff --git a/pyproject.toml b/pyproject.toml index 74c5e3b4..ceca68e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ - "agentd>=0.2.0.post3", + "agentd>=0.2.1", "aiofiles>=24.1.0", "docling>=2.41.0", "opensearch-py[async]>=3.0.0", diff --git a/src/agent.py b/src/agent.py index 92b60bd7..b947e237 100644 --- a/src/agent.py +++ b/src/agent.py @@ -1,17 +1,118 @@ messages = [{"role": "system", "content": "You are a helpful assistant. Always use the search_tools to answer questions."}] -# Async version for web server -async def async_chat(async_client, prompt: str, model: str = "gpt-4.1-mini", previous_response_id: str = None) -> str: +# Generic async response function for streaming +async def async_response_stream(client, prompt: str, model: str, previous_response_id: str = None, log_prefix: str = "response"): + print(f"user ==> {prompt}") + + try: + # Build request parameters + request_params = { + "model": model, + "input": prompt, + "stream": True + } + if previous_response_id is not None: + request_params["previous_response_id"] = previous_response_id + + response = await client.responses.create(**request_params) + + full_response = "" + chunk_count = 0 + async for chunk in response: + chunk_count += 1 + print(f"[DEBUG] Chunk {chunk_count}: {chunk}") + + # Yield the raw event as JSON for the UI to process + import json + + # Also extract text content for logging + if hasattr(chunk, 'output_text') and chunk.output_text: + full_response += chunk.output_text + elif hasattr(chunk, 'delta') and chunk.delta: + # Handle delta properly - it might be a dict or string + if isinstance(chunk.delta, dict): + delta_text = chunk.delta.get('content', '') or chunk.delta.get('text', '') or str(chunk.delta) + else: + delta_text = str(chunk.delta) + full_response += delta_text + + # Send the raw event as JSON followed by newline for easy parsing + try: + # Try to serialize the chunk object + if hasattr(chunk, 'model_dump'): + # Pydantic model + chunk_data = chunk.model_dump() + elif hasattr(chunk, '__dict__'): + chunk_data = chunk.__dict__ + else: + chunk_data = str(chunk) + + yield (json.dumps(chunk_data, default=str) + '\n').encode('utf-8') + except Exception as e: + # Fallback to string representation + print(f"[DEBUG] JSON serialization failed: {e}") + yield (json.dumps({"error": f"Serialization failed: {e}", "raw": str(chunk)}) + '\n').encode('utf-8') + + print(f"[DEBUG] Stream complete. Total chunks: {chunk_count}") + print(f"{log_prefix} ==> {full_response}") + + except Exception as e: + print(f"[ERROR] Exception in streaming: {e}") + import traceback + traceback.print_exc() + raise + +# Generic async response function for non-streaming +async def async_response(client, prompt: str, model: str, previous_response_id: str = None, log_prefix: str = "response"): + print(f"user ==> {prompt}") + + # Build request parameters + request_params = { + "model": model, + "input": prompt, + "stream": False + } + if previous_response_id is not None: + request_params["previous_response_id"] = previous_response_id + + response = await client.responses.create(**request_params) + + response_text = response.output_text + print(f"{log_prefix} ==> {response_text}") + return response_text + +# Unified streaming function for both chat and langflow +async def async_stream(client, prompt: str, model: str, previous_response_id: str = None, log_prefix: str = "response"): + async for chunk in async_response_stream(client, prompt, model, previous_response_id=previous_response_id, log_prefix=log_prefix): + yield chunk + +# Async langflow function (non-streaming only) +async def async_langflow(langflow_client, flow_id: str, prompt: str): + return await async_response(langflow_client, prompt, flow_id, log_prefix="langflow") + +# Async langflow function for streaming (alias for compatibility) +async def async_langflow_stream(langflow_client, flow_id: str, prompt: str): + print(f"[DEBUG] Starting langflow stream for prompt: {prompt}") + try: + async for chunk in async_stream(langflow_client, prompt, flow_id, log_prefix="langflow"): + print(f"[DEBUG] Yielding chunk from langflow_stream: {chunk[:100]}...") + yield chunk + print(f"[DEBUG] Langflow stream completed") + except Exception as e: + print(f"[ERROR] Exception in langflow_stream: {e}") + import traceback + traceback.print_exc() + raise + +# Async chat function (non-streaming only) +async def async_chat(async_client, prompt: str, model: str = "gpt-4.1-mini", previous_response_id: str = None): global messages messages += [{"role": "user", "content": prompt}] - response = await async_client.responses.create( - model=model, - input=prompt, - previous_response_id=previous_response_id, - ) + return await async_response(async_client, prompt, model, previous_response_id=previous_response_id, log_prefix="agent") - response_id = response.id - response_text = response.output_text - print(f"user ==> {prompt}") - print(f"agent ==> {response_text}") - return response_text \ No newline at end of file +# Async chat function for streaming (alias for compatibility) +async def async_chat_stream(async_client, prompt: str, model: str = "gpt-4.1-mini", previous_response_id: str = None): + global messages + messages += [{"role": "user", "content": prompt}] + async for chunk in async_stream(async_client, prompt, model, previous_response_id=previous_response_id, log_prefix="agent"): + yield chunk \ No newline at end of file diff --git a/src/app.py b/src/app.py index 927a7c3b..573b3ba5 100644 --- a/src/app.py +++ b/src/app.py @@ -4,7 +4,7 @@ import os from collections import defaultdict from typing import Any -from agent import async_chat +from agent import async_chat, async_langflow os.environ['USE_CPU_ONLY'] = 'true' @@ -14,7 +14,7 @@ import asyncio from starlette.applications import Starlette from starlette.requests import Request -from starlette.responses import JSONResponse +from starlette.responses import JSONResponse, StreamingResponse from starlette.routing import Route import aiofiles @@ -305,16 +305,31 @@ async def search_tool(query: str)-> dict[str, Any]: async def chat_endpoint(request): data = await request.json() prompt = data.get("prompt", "") + stream = data.get("stream", False) if not prompt: return JSONResponse({"error": "Prompt is required"}, status_code=400) - response = await async_chat(patched_async_client, prompt) - return JSONResponse({"response": response}) + if stream: + from agent import async_chat_stream + return StreamingResponse( + async_chat_stream(patched_async_client, prompt), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Cache-Control" + } + ) + else: + response = await async_chat(patched_async_client, prompt) + return JSONResponse({"response": response}) async def langflow_endpoint(request): data = await request.json() prompt = data.get("prompt", "") + stream = data.get("stream", False) if not prompt: return JSONResponse({"error": "Prompt is required"}, status_code=400) @@ -323,14 +338,21 @@ async def langflow_endpoint(request): return JSONResponse({"error": "LANGFLOW_URL, FLOW_ID, and LANGFLOW_KEY environment variables are required"}, status_code=500) try: - response = await langflow_client.responses.create( - model=flow_id, - input=prompt - ) - - response_text = response.output_text - - return JSONResponse({"response": response_text}) + if stream: + from agent import async_langflow_stream + return StreamingResponse( + async_langflow_stream(langflow_client, flow_id, prompt), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "Cache-Control" + } + ) + else: + response = await async_langflow(langflow_client, flow_id, prompt) + return JSONResponse({"response": response}) except Exception as e: return JSONResponse({"error": f"Langflow request failed: {str(e)}"}, status_code=500) diff --git a/uv.lock b/uv.lock index 1b782d26..7750d8ff 100644 --- a/uv.lock +++ b/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ [[package]] name = "agentd" -version = "0.2.0.post3" +version = "0.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "litellm" }, @@ -18,9 +18,9 @@ dependencies = [ { name = "openai-agents" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/89/2bc397c80764d6acfeb6de7ac6d3ce8914f6f37f57307d1e2b6f7e8b0923/agentd-0.2.0.post3.tar.gz", hash = "sha256:765cb51798791eed32687b44305b20dd4130990471f0f1914afa2b292d09cb5e", size = 114530, upload-time = "2025-07-16T06:13:11.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/ef/8c96f9699ec99b2e60b4f2fbbc8ae3693ff583eda2835b3244d3aaaf68b3/agentd-0.2.1.tar.gz", hash = "sha256:96c4a5efc1a4ee3ee0a1ce68ade05e8b51e6a0171b0d64fadac369744a847240", size = 118826, upload-time = "2025-07-17T19:47:58.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/f8/ced474722557f11e0a2f7e691371cf25c6a823507cc297971baa71bfbaac/agentd-0.2.0.post3-py3-none-any.whl", hash = "sha256:d05c6123a33d9f0b466fba4f7b378618c352ca367c65cd2c5a54f867af7b3cff", size = 13299, upload-time = "2025-07-16T06:13:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9b/1e695c083227550eb85a9544a6a7c28e11d1dfb893e9d22035a0ad1b7fa3/agentd-0.2.1-py3-none-any.whl", hash = "sha256:b35da9c3557b5784c301c327c2831c6dfcfc74bf84fa8428585f2dffecdb2904", size = 15833, upload-time = "2025-07-17T19:47:57.749Z" }, ] [[package]] @@ -434,7 +434,7 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "agentd", specifier = ">=0.2.0.post3" }, + { name = "agentd", specifier = ">=0.2.1" }, { name = "aiofiles", specifier = ">=24.1.0" }, { name = "docling", specifier = ">=2.41.0" }, { name = "opensearch-py", extras = ["async"], specifier = ">=3.0.0" },