Made agentic
Browse files- agent/__init__.py +9 -0
- agent/agent.py +162 -0
- agent/responses.py +29 -0
- agent/tools.py +49 -0
- app.py +53 -32
agent/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .agent import Agent
|
| 2 |
+
from .tools import Tool, fetch_webpage, FETCH_WEBPAGE_TOOL
|
| 3 |
+
|
| 4 |
+
__all__ = [
|
| 5 |
+
"Agent",
|
| 6 |
+
"Tool",
|
| 7 |
+
"fetch_webpage",
|
| 8 |
+
"FETCH_WEBPAGE_TOOL",
|
| 9 |
+
]
|
agent/agent.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from typing import Any, Generator
|
| 3 |
+
|
| 4 |
+
from openai import OpenAI
|
| 5 |
+
|
| 6 |
+
from .tools import Tool
|
| 7 |
+
|
| 8 |
+
# ---------------------------------------------------------------------------
|
| 9 |
+
# Streaming event contract
|
| 10 |
+
# ---------------------------------------------------------------------------
|
| 11 |
+
# Each yield from Agent.stream() is a dict with a "type" key:
|
| 12 |
+
#
|
| 13 |
+
# {"type": "text", "content": "partial text"}
|
| 14 |
+
# {"type": "reasoning","content": "model thinking"}
|
| 15 |
+
# {"type": "tool_call", "name": str, "arguments": '{"url":"..."}'}
|
| 16 |
+
# {"type": "tool_output","name": str, "content": "result"}
|
| 17 |
+
# {"type": "done", "content": "full assistant response"}
|
| 18 |
+
# {"type": "error", "content": "error message"}
|
| 19 |
+
# ---------------------------------------------------------------------------
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class Agent:
|
| 23 |
+
"""OpenAI-compatible tool-calling agent with streaming."""
|
| 24 |
+
|
| 25 |
+
def __init__(self, base_url: str, api_key: str, model: str) -> None:
|
| 26 |
+
self.client = OpenAI(base_url=base_url, api_key=api_key)
|
| 27 |
+
self.model = model
|
| 28 |
+
self._tools: list[Tool] = []
|
| 29 |
+
|
| 30 |
+
def register_tool(self, tool: Tool) -> None:
|
| 31 |
+
self._tools.append(tool)
|
| 32 |
+
|
| 33 |
+
def stream(self, messages: list[dict]) -> Generator[dict, None, None]:
|
| 34 |
+
"""Yield streaming events until the model produces a final response.
|
| 35 |
+
|
| 36 |
+
*messages* is mutated in-place — after the generator completes it
|
| 37 |
+
contains the full conversation history (including assistant replies,
|
| 38 |
+
tool calls, and tool outputs).
|
| 39 |
+
"""
|
| 40 |
+
while True:
|
| 41 |
+
specs = [t.to_openai_spec() for t in self._tools] if self._tools else None
|
| 42 |
+
|
| 43 |
+
collected_content = ""
|
| 44 |
+
collected_tool_calls: dict[int, dict] = {}
|
| 45 |
+
|
| 46 |
+
kwargs: dict[str, Any] = dict(
|
| 47 |
+
model=self.model,
|
| 48 |
+
messages=messages,
|
| 49 |
+
stream=True,
|
| 50 |
+
)
|
| 51 |
+
if specs:
|
| 52 |
+
kwargs["tools"] = specs
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
stream = self.client.chat.completions.create(**kwargs)
|
| 56 |
+
except Exception as exc:
|
| 57 |
+
yield {"type": "error", "content": str(exc)}
|
| 58 |
+
return
|
| 59 |
+
|
| 60 |
+
for chunk in stream:
|
| 61 |
+
cd = chunk.to_dict()
|
| 62 |
+
choices = cd.get("choices", [])
|
| 63 |
+
if not choices:
|
| 64 |
+
continue
|
| 65 |
+
delta = choices[0].get("delta", {})
|
| 66 |
+
|
| 67 |
+
# Reasoning (e.g. DeepSeek R1)
|
| 68 |
+
reasoning = delta.get("reasoning_content") or delta.get("reasoning")
|
| 69 |
+
if reasoning:
|
| 70 |
+
yield {"type": "reasoning", "content": reasoning}
|
| 71 |
+
|
| 72 |
+
# Text content
|
| 73 |
+
content = delta.get("content", "")
|
| 74 |
+
if content:
|
| 75 |
+
collected_content += content
|
| 76 |
+
yield {"type": "text", "content": content}
|
| 77 |
+
|
| 78 |
+
# Tool call fragments
|
| 79 |
+
for tc in delta.get("tool_calls", []):
|
| 80 |
+
idx = tc.get("index", 0)
|
| 81 |
+
if idx not in collected_tool_calls:
|
| 82 |
+
collected_tool_calls[idx] = {
|
| 83 |
+
"id": tc.get("id", ""),
|
| 84 |
+
"function": {"name": "", "arguments": ""},
|
| 85 |
+
}
|
| 86 |
+
if tc.get("id"):
|
| 87 |
+
collected_tool_calls[idx]["id"] = tc["id"]
|
| 88 |
+
fn = tc.get("function", {})
|
| 89 |
+
if fn.get("name"):
|
| 90 |
+
collected_tool_calls[idx]["function"]["name"] += fn["name"]
|
| 91 |
+
if fn.get("arguments"):
|
| 92 |
+
collected_tool_calls[idx]["function"]["arguments"] += fn[
|
| 93 |
+
"arguments"
|
| 94 |
+
]
|
| 95 |
+
|
| 96 |
+
# --- Handle tool calls ---
|
| 97 |
+
if collected_tool_calls:
|
| 98 |
+
tool_call_list = []
|
| 99 |
+
for idx in sorted(collected_tool_calls.keys()):
|
| 100 |
+
tc = collected_tool_calls[idx]
|
| 101 |
+
tool_call_list.append(
|
| 102 |
+
{
|
| 103 |
+
"id": tc["id"],
|
| 104 |
+
"type": "function",
|
| 105 |
+
"function": {
|
| 106 |
+
"name": tc["function"]["name"],
|
| 107 |
+
"arguments": tc["function"]["arguments"],
|
| 108 |
+
},
|
| 109 |
+
}
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
# Assistant message with tool_calls
|
| 113 |
+
assistant_msg: dict[str, Any] = {
|
| 114 |
+
"role": "assistant",
|
| 115 |
+
"content": collected_content or None,
|
| 116 |
+
}
|
| 117 |
+
assistant_msg["tool_calls"] = tool_call_list
|
| 118 |
+
messages.append(assistant_msg)
|
| 119 |
+
|
| 120 |
+
# Execute each tool and append results
|
| 121 |
+
for tc_spec in tool_call_list:
|
| 122 |
+
tname = tc_spec["function"]["name"]
|
| 123 |
+
try:
|
| 124 |
+
targs = json.loads(tc_spec["function"]["arguments"])
|
| 125 |
+
except json.JSONDecodeError:
|
| 126 |
+
targs = {}
|
| 127 |
+
|
| 128 |
+
yield {
|
| 129 |
+
"type": "tool_call",
|
| 130 |
+
"name": tname,
|
| 131 |
+
"arguments": tc_spec["function"]["arguments"],
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
tool_obj = next((t for t in self._tools if t.name == tname), None)
|
| 135 |
+
if tool_obj:
|
| 136 |
+
try:
|
| 137 |
+
result = tool_obj.run(**targs)
|
| 138 |
+
except Exception as e:
|
| 139 |
+
result = f"Error executing {tname}: {e}"
|
| 140 |
+
else:
|
| 141 |
+
result = f"Error: Tool '{tname}' not found"
|
| 142 |
+
|
| 143 |
+
result_str = str(result)
|
| 144 |
+
# if result is too long, truncate and indicate truncation
|
| 145 |
+
if len(result_str) > 5_000:
|
| 146 |
+
result_str = result_str[:5_000] + "\n...[truncated]"
|
| 147 |
+
yield {"type": "tool_output", "name": tname, "content": result_str}
|
| 148 |
+
|
| 149 |
+
messages.append(
|
| 150 |
+
{
|
| 151 |
+
"role": "tool",
|
| 152 |
+
"tool_call_id": tc_spec["id"],
|
| 153 |
+
"content": result_str,
|
| 154 |
+
}
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
continue # Loop back so the model can respond after tools
|
| 158 |
+
|
| 159 |
+
# --- No tool calls — final response ---
|
| 160 |
+
messages.append({"role": "assistant", "content": collected_content})
|
| 161 |
+
yield {"type": "done", "content": collected_content}
|
| 162 |
+
break
|
agent/responses.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Streaming event types yielded by Agent.stream().
|
| 3 |
+
|
| 4 |
+
Each event is a plain dict with at least a ``"type"`` key.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
# ── Text chunk ──────────────────────────────────────────────────────────
|
| 8 |
+
# {"type": "text", "content": "partial assistant message"}
|
| 9 |
+
TEXT = "text"
|
| 10 |
+
|
| 11 |
+
# ── Reasoning (e.g. DeepSeek R1 chain-of-thought) ──────────────────────
|
| 12 |
+
# {"type": "reasoning", "content": "model thinking"}
|
| 13 |
+
REASONING = "reasoning"
|
| 14 |
+
|
| 15 |
+
# ── Tool was called by the model ────────────────────────────────────────
|
| 16 |
+
# {"type": "tool_call", "name": "fetch_webpage", "arguments": '{"url":"..."}'}
|
| 17 |
+
TOOL_CALL = "tool_call"
|
| 18 |
+
|
| 19 |
+
# ── Tool execution output ───────────────────────────────────────────────
|
| 20 |
+
# {"type": "tool_output", "name": "fetch_webpage", "content": "<html>..."}
|
| 21 |
+
TOOL_OUTPUT = "tool_output"
|
| 22 |
+
|
| 23 |
+
# ── Final response (no more tool calls) ─────────────────────────────────
|
| 24 |
+
# {"type": "done", "content": "full assistant message"}
|
| 25 |
+
DONE = "done"
|
| 26 |
+
|
| 27 |
+
# ── Fatal error ─────────────────────────────────────────────────────────
|
| 28 |
+
# {"type": "error", "content": "API key invalid"}
|
| 29 |
+
ERROR = "error"
|
agent/tools.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from markitdown import MarkItDown
|
| 2 |
+
from typing import Any, Callable
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Tool:
|
| 6 |
+
"""A callable tool the agent can invoke."""
|
| 7 |
+
|
| 8 |
+
def __init__(
|
| 9 |
+
self, name: str, description: str, parameters: dict, handler: Callable[..., str]
|
| 10 |
+
) -> None:
|
| 11 |
+
self.name = name
|
| 12 |
+
self.description = description
|
| 13 |
+
self.parameters = parameters
|
| 14 |
+
self.handler = handler
|
| 15 |
+
|
| 16 |
+
def to_openai_spec(self) -> dict:
|
| 17 |
+
return {
|
| 18 |
+
"type": "function",
|
| 19 |
+
"function": {
|
| 20 |
+
"name": self.name,
|
| 21 |
+
"description": self.description,
|
| 22 |
+
"parameters": self.parameters,
|
| 23 |
+
},
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
def run(self, **kwargs: Any) -> str:
|
| 27 |
+
return self.handler(**kwargs)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def fetch_webpage(url: str) -> str:
|
| 31 |
+
"""Fetch a webpage and return its text content."""
|
| 32 |
+
md = MarkItDown()
|
| 33 |
+
return md.convert(url).text_content
|
| 34 |
+
|
| 35 |
+
FETCH_WEBPAGE_TOOL = Tool(
|
| 36 |
+
name="fetch_webpage",
|
| 37 |
+
description="Fetch the content of a webpage and return its text",
|
| 38 |
+
parameters={
|
| 39 |
+
"type": "object",
|
| 40 |
+
"properties": {
|
| 41 |
+
"url": {
|
| 42 |
+
"type": "string",
|
| 43 |
+
"description": "The URL to fetch",
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
"required": ["url"],
|
| 47 |
+
},
|
| 48 |
+
handler=fetch_webpage,
|
| 49 |
+
)
|
app.py
CHANGED
|
@@ -1,19 +1,20 @@
|
|
| 1 |
import time
|
| 2 |
import uuid
|
| 3 |
import gradio as gr
|
| 4 |
-
from openai import OpenAI
|
| 5 |
from dotenv import load_dotenv
|
|
|
|
| 6 |
import os
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
load_dotenv()
|
| 10 |
gr.set_static_paths("static/")
|
| 11 |
|
| 12 |
-
|
| 13 |
-
api_key=os.getenv("OPENAI_API_KEY"),
|
| 14 |
base_url=os.getenv("OPENAI_BASE_URL"),
|
|
|
|
|
|
|
| 15 |
)
|
| 16 |
-
|
| 17 |
|
| 18 |
# Load JS from external files
|
| 19 |
_js_dir = Path(__file__).parent / "static" / "js"
|
|
@@ -69,54 +70,74 @@ class GradioEvents:
|
|
| 69 |
|
| 70 |
yield { msg: gr.update(value=""), chatbot: gr.update(value=ctx["history"]), state: gr.update(value=state_value), conv_choice: _conv_choices(state_value), send_btn: gr.update(visible=False), stop_btn: gr.update(visible=True)}
|
| 71 |
|
| 72 |
-
|
|
|
|
| 73 |
is_reasoning = False
|
| 74 |
spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
| 75 |
spinner_idx = 0
|
| 76 |
-
temp = []
|
| 77 |
|
| 78 |
try:
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
messages=ctx["history"],
|
| 82 |
-
stream=True
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
for chunk in stream:
|
| 86 |
-
chunk = chunk.to_dict()
|
| 87 |
-
delta = chunk['choices'][0]['delta']
|
| 88 |
|
| 89 |
-
if
|
| 90 |
if not is_reasoning:
|
| 91 |
is_reasoning = True
|
| 92 |
spinner_idx += 1
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
"role": "assistant",
|
| 95 |
"content": f"<span class=\"thinking-indicator\">{spinner_frames[spinner_idx % len(spinner_frames)]} Thinking...</span>",
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
elif delta.get('content', ''):
|
| 100 |
if is_reasoning:
|
| 101 |
is_reasoning = False
|
| 102 |
-
|
| 103 |
-
temp = [{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
"role": "assistant",
|
| 105 |
-
"content":
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
yield {
|
| 111 |
chatbot: gr.update(value=ctx["history"] + temp),
|
| 112 |
state: gr.update(value=state_value),
|
| 113 |
}
|
| 114 |
|
| 115 |
-
if not is_reasoning and temp:
|
| 116 |
-
ctx["history"].extend(temp)
|
| 117 |
-
if ctx["history"][-1].get("role") == "assistant":
|
| 118 |
-
ctx["history"][-1]["is_final"] = True
|
| 119 |
-
|
| 120 |
yield {
|
| 121 |
chatbot: gr.update(value=ctx["history"]),
|
| 122 |
state: gr.update(value=state_value),
|
|
|
|
| 1 |
import time
|
| 2 |
import uuid
|
| 3 |
import gradio as gr
|
|
|
|
| 4 |
from dotenv import load_dotenv
|
| 5 |
+
from agent import Agent, FETCH_WEBPAGE_TOOL
|
| 6 |
import os
|
| 7 |
from pathlib import Path
|
| 8 |
|
| 9 |
load_dotenv()
|
| 10 |
gr.set_static_paths("static/")
|
| 11 |
|
| 12 |
+
agent = Agent(
|
|
|
|
| 13 |
base_url=os.getenv("OPENAI_BASE_URL"),
|
| 14 |
+
api_key=os.getenv("OPENAI_API_KEY"),
|
| 15 |
+
model=os.getenv("OPENAI_MODEL"),
|
| 16 |
)
|
| 17 |
+
agent.register_tool(FETCH_WEBPAGE_TOOL)
|
| 18 |
|
| 19 |
# Load JS from external files
|
| 20 |
_js_dir = Path(__file__).parent / "static" / "js"
|
|
|
|
| 70 |
|
| 71 |
yield { msg: gr.update(value=""), chatbot: gr.update(value=ctx["history"]), state: gr.update(value=state_value), conv_choice: _conv_choices(state_value), send_btn: gr.update(visible=False), stop_btn: gr.update(visible=True)}
|
| 72 |
|
| 73 |
+
# Agent will populate ctx["history"] with assistant + tool messages
|
| 74 |
+
display_buffer = ""
|
| 75 |
is_reasoning = False
|
| 76 |
spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
| 77 |
spinner_idx = 0
|
|
|
|
| 78 |
|
| 79 |
try:
|
| 80 |
+
for ev in agent.stream(ctx["history"]):
|
| 81 |
+
t = ev["type"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
+
if t == "reasoning":
|
| 84 |
if not is_reasoning:
|
| 85 |
is_reasoning = True
|
| 86 |
spinner_idx += 1
|
| 87 |
+
if display_buffer:
|
| 88 |
+
temp = [{"role": "assistant", "content": display_buffer}]
|
| 89 |
+
else:
|
| 90 |
+
temp = []
|
| 91 |
+
temp.append({
|
| 92 |
"role": "assistant",
|
| 93 |
"content": f"<span class=\"thinking-indicator\">{spinner_frames[spinner_idx % len(spinner_frames)]} Thinking...</span>",
|
| 94 |
+
})
|
| 95 |
+
|
| 96 |
+
elif t == "text":
|
|
|
|
| 97 |
if is_reasoning:
|
| 98 |
is_reasoning = False
|
| 99 |
+
display_buffer += ev["content"]
|
| 100 |
+
temp = [{"role": "assistant", "content": display_buffer}]
|
| 101 |
+
|
| 102 |
+
elif t == "tool_call":
|
| 103 |
+
display_buffer += (
|
| 104 |
+
f'\n<div class="tool-call">'
|
| 105 |
+
f'🔧 <strong>{ev["name"]}</strong>'
|
| 106 |
+
f'<pre style="display:inline;font-size:0.85em">({ev["arguments"]})</pre>'
|
| 107 |
+
f'</div>'
|
| 108 |
+
)
|
| 109 |
+
temp = [{"role": "assistant", "content": display_buffer}]
|
| 110 |
+
|
| 111 |
+
elif t == "tool_output":
|
| 112 |
+
display_buffer += (
|
| 113 |
+
f'\n<div class="tool-output">'
|
| 114 |
+
f'📄 <em>({len(ev["content"])} chars)</em>'
|
| 115 |
+
f'</div>'
|
| 116 |
+
)
|
| 117 |
+
temp = [{"role": "assistant", "content": display_buffer}]
|
| 118 |
+
|
| 119 |
+
elif t == "error":
|
| 120 |
+
ctx["history"].append({
|
| 121 |
"role": "assistant",
|
| 122 |
+
"content": f'<span style="color: var(--color-red-500)">{ev["content"]}</span>',
|
| 123 |
+
})
|
| 124 |
+
yield {
|
| 125 |
+
chatbot: gr.update(value=ctx["history"]),
|
| 126 |
+
state: gr.update(value=state_value),
|
| 127 |
+
send_btn: gr.update(visible=True),
|
| 128 |
+
stop_btn: gr.update(visible=False),
|
| 129 |
+
}
|
| 130 |
+
return
|
| 131 |
+
|
| 132 |
+
elif t == "done":
|
| 133 |
+
# Agent has fully populated ctx["history"]
|
| 134 |
+
break
|
| 135 |
|
| 136 |
yield {
|
| 137 |
chatbot: gr.update(value=ctx["history"] + temp),
|
| 138 |
state: gr.update(value=state_value),
|
| 139 |
}
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
yield {
|
| 142 |
chatbot: gr.update(value=ctx["history"]),
|
| 143 |
state: gr.update(value=state_value),
|