vibecore 0.2.0__py3-none-any.whl → 0.3.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of vibecore might be problematic. Click here for more details.
- vibecore/agents/default.py +6 -11
- vibecore/agents/{task_agent.py → task.py} +2 -6
- vibecore/flow.py +105 -0
- vibecore/main.py +28 -6
- vibecore/settings.py +28 -3
- vibecore/tools/task/executor.py +1 -1
- vibecore/tools/webfetch/__init__.py +7 -0
- vibecore/tools/webfetch/executor.py +127 -0
- vibecore/tools/webfetch/models.py +22 -0
- vibecore/tools/webfetch/tools.py +46 -0
- vibecore/tools/websearch/__init__.py +5 -0
- vibecore/tools/websearch/base.py +27 -0
- vibecore/tools/websearch/ddgs/__init__.py +5 -0
- vibecore/tools/websearch/ddgs/backend.py +64 -0
- vibecore/tools/websearch/executor.py +43 -0
- vibecore/tools/websearch/models.py +20 -0
- vibecore/tools/websearch/tools.py +49 -0
- vibecore/widgets/tool_message_factory.py +16 -0
- vibecore/widgets/tool_messages.py +117 -0
- vibecore/widgets/tool_messages.tcss +48 -0
- {vibecore-0.2.0.dist-info → vibecore-0.3.0b1.dist-info}/METADATA +106 -1
- {vibecore-0.2.0.dist-info → vibecore-0.3.0b1.dist-info}/RECORD +25 -13
- {vibecore-0.2.0.dist-info → vibecore-0.3.0b1.dist-info}/WHEEL +0 -0
- {vibecore-0.2.0.dist-info → vibecore-0.3.0b1.dist-info}/entry_points.txt +0 -0
- {vibecore-0.2.0.dist-info → vibecore-0.3.0b1.dist-info}/licenses/LICENSE +0 -0
vibecore/agents/default.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING
|
|
2
2
|
|
|
3
|
-
from agents import Agent
|
|
3
|
+
from agents import Agent
|
|
4
4
|
from agents.extensions.handoff_prompt import prompt_with_handoff_instructions
|
|
5
|
-
from openai.types import Reasoning
|
|
6
5
|
|
|
7
6
|
from vibecore.context import VibecoreContext
|
|
8
7
|
from vibecore.settings import settings
|
|
@@ -11,6 +10,8 @@ from vibecore.tools.python.tools import execute_python
|
|
|
11
10
|
from vibecore.tools.shell.tools import bash, glob, grep, ls
|
|
12
11
|
from vibecore.tools.task.tools import task
|
|
13
12
|
from vibecore.tools.todo.tools import todo_read, todo_write
|
|
13
|
+
from vibecore.tools.webfetch.tools import webfetch
|
|
14
|
+
from vibecore.tools.websearch.tools import websearch
|
|
14
15
|
|
|
15
16
|
from .prompts import COMMON_PROMPT
|
|
16
17
|
|
|
@@ -50,26 +51,20 @@ def create_default_agent(mcp_servers: list["MCPServer"] | None = None) -> Agent[
|
|
|
50
51
|
grep,
|
|
51
52
|
ls,
|
|
52
53
|
task,
|
|
54
|
+
websearch,
|
|
55
|
+
webfetch,
|
|
53
56
|
]
|
|
54
57
|
instructions = INSTRUCTIONS
|
|
55
58
|
|
|
56
59
|
instructions = prompt_with_handoff_instructions(instructions)
|
|
57
60
|
|
|
58
|
-
# Configure reasoning based on settings
|
|
59
|
-
reasoning_config = Reasoning(summary="auto")
|
|
60
|
-
if settings.reasoning_effort is not None:
|
|
61
|
-
reasoning_config = Reasoning(effort=settings.reasoning_effort, summary="auto")
|
|
62
|
-
|
|
63
61
|
return Agent[VibecoreContext](
|
|
64
62
|
name="Vibecore Agent",
|
|
65
63
|
handoff_description="A versatile general-purpose assistant",
|
|
66
64
|
instructions=instructions,
|
|
67
65
|
tools=tools,
|
|
68
66
|
model=settings.model,
|
|
69
|
-
model_settings=
|
|
70
|
-
include_usage=True, # Ensure token usage is tracked in streaming mode
|
|
71
|
-
reasoning=reasoning_config,
|
|
72
|
-
),
|
|
67
|
+
model_settings=settings.default_model_settings,
|
|
73
68
|
handoffs=[],
|
|
74
69
|
mcp_servers=mcp_servers or [],
|
|
75
70
|
)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Task-specific agent configuration for executing delegated tasks."""
|
|
2
2
|
|
|
3
|
-
from agents import Agent
|
|
3
|
+
from agents import Agent
|
|
4
4
|
from agents.extensions.handoff_prompt import prompt_with_handoff_instructions
|
|
5
|
-
from openai.types import Reasoning
|
|
6
5
|
|
|
7
6
|
from vibecore.context import VibecoreContext
|
|
8
7
|
from vibecore.settings import settings
|
|
@@ -58,9 +57,6 @@ def create_task_agent(prompt: str) -> Agent[VibecoreContext]:
|
|
|
58
57
|
instructions=instructions,
|
|
59
58
|
tools=tools,
|
|
60
59
|
model=settings.model,
|
|
61
|
-
model_settings=
|
|
62
|
-
include_usage=True, # Ensure token usage is tracked in streaming mode
|
|
63
|
-
reasoning=Reasoning(summary="auto"),
|
|
64
|
-
),
|
|
60
|
+
model_settings=settings.default_model_settings,
|
|
65
61
|
handoffs=[],
|
|
66
62
|
)
|
vibecore/flow.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import threading
|
|
3
|
+
from collections.abc import Callable, Coroutine
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from agents import Agent
|
|
7
|
+
from textual.pilot import Pilot
|
|
8
|
+
|
|
9
|
+
from vibecore.context import VibecoreContext
|
|
10
|
+
from vibecore.main import AppIsExiting, VibecoreApp
|
|
11
|
+
from vibecore.widgets.core import MyTextArea
|
|
12
|
+
from vibecore.widgets.messages import SystemMessage
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UserInputFunc(Protocol):
|
|
16
|
+
"""Protocol for user input function with optional prompt parameter."""
|
|
17
|
+
|
|
18
|
+
async def __call__(self, prompt: str = "") -> str:
|
|
19
|
+
"""Get user input with optional prompt message.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
prompt: Optional prompt to display before getting input.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The user's input string.
|
|
26
|
+
"""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def flow(
|
|
31
|
+
agent: Agent,
|
|
32
|
+
logic: Callable[[VibecoreApp, VibecoreContext, UserInputFunc], Coroutine],
|
|
33
|
+
headless: bool = False,
|
|
34
|
+
shutdown: bool = False,
|
|
35
|
+
disable_user_input: bool = True,
|
|
36
|
+
):
|
|
37
|
+
ctx = VibecoreContext()
|
|
38
|
+
app = VibecoreApp(ctx, agent, show_welcome=False)
|
|
39
|
+
|
|
40
|
+
app_ready_event = asyncio.Event()
|
|
41
|
+
|
|
42
|
+
def on_app_ready() -> None:
|
|
43
|
+
"""Called when app is ready to process events."""
|
|
44
|
+
app_ready_event.set()
|
|
45
|
+
|
|
46
|
+
async def run_app(app: VibecoreApp) -> None:
|
|
47
|
+
"""Run the apps message loop.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
app: App to run.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
with app._context():
|
|
54
|
+
try:
|
|
55
|
+
app._loop = asyncio.get_running_loop()
|
|
56
|
+
app._thread_id = threading.get_ident()
|
|
57
|
+
await app._process_messages(
|
|
58
|
+
ready_callback=on_app_ready,
|
|
59
|
+
headless=headless,
|
|
60
|
+
)
|
|
61
|
+
finally:
|
|
62
|
+
app_ready_event.set()
|
|
63
|
+
|
|
64
|
+
async def user_input(prompt: str = "") -> str:
|
|
65
|
+
if prompt:
|
|
66
|
+
await app.add_message(SystemMessage(prompt))
|
|
67
|
+
app.query_one(MyTextArea).disabled = False
|
|
68
|
+
app.query_one(MyTextArea).focus()
|
|
69
|
+
user_input = await app.wait_for_user_input()
|
|
70
|
+
if disable_user_input:
|
|
71
|
+
app.query_one(MyTextArea).disabled = True
|
|
72
|
+
return user_input
|
|
73
|
+
|
|
74
|
+
async def run_logic(app: VibecoreApp, ctx: VibecoreContext, user_input: UserInputFunc) -> None:
|
|
75
|
+
try:
|
|
76
|
+
await logic(app, ctx, user_input)
|
|
77
|
+
except AppIsExiting:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
app_task = asyncio.create_task(run_app(app), name=f"with_app({app})")
|
|
81
|
+
await app_ready_event.wait()
|
|
82
|
+
pilot = Pilot(app)
|
|
83
|
+
logic_task: asyncio.Task | None = None
|
|
84
|
+
|
|
85
|
+
await pilot._wait_for_screen()
|
|
86
|
+
if disable_user_input:
|
|
87
|
+
app.query_one(MyTextArea).disabled = True
|
|
88
|
+
logic_task = asyncio.create_task(run_logic(app, ctx, user_input), name="logic_task")
|
|
89
|
+
done, pending = await asyncio.wait([logic_task, app_task], return_when=asyncio.FIRST_COMPLETED)
|
|
90
|
+
|
|
91
|
+
# If app has exited and logic is still running, cancel logic
|
|
92
|
+
if app_task in done and logic_task in pending:
|
|
93
|
+
logic_task.cancel()
|
|
94
|
+
# If logic is finished and app is still running
|
|
95
|
+
elif logic_task in done and app_task in pending:
|
|
96
|
+
if shutdown:
|
|
97
|
+
if not headless:
|
|
98
|
+
await pilot._wait_for_screen()
|
|
99
|
+
await asyncio.sleep(1.0)
|
|
100
|
+
app.exit()
|
|
101
|
+
else:
|
|
102
|
+
# Enable text input so users can interact freely
|
|
103
|
+
app.query_one(MyTextArea).disabled = False
|
|
104
|
+
# Wait until app is exited
|
|
105
|
+
await app_task
|
vibecore/main.py
CHANGED
|
@@ -30,7 +30,11 @@ from vibecore.widgets.core import AppFooter, MainScroll, MyTextArea
|
|
|
30
30
|
from vibecore.widgets.info import Welcome
|
|
31
31
|
from vibecore.widgets.messages import AgentMessage, BaseMessage, MessageStatus, SystemMessage, UserMessage
|
|
32
32
|
|
|
33
|
-
AgentStatus = Literal["idle", "running"]
|
|
33
|
+
AgentStatus = Literal["idle", "running", "waiting_user_input"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AppIsExiting(Exception):
|
|
37
|
+
pass
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
def detect_reasoning_effort(prompt: str) -> Literal["low", "medium", "high"] | None:
|
|
@@ -82,6 +86,7 @@ class VibecoreApp(App):
|
|
|
82
86
|
agent: Agent,
|
|
83
87
|
session_id: str | None = None,
|
|
84
88
|
print_mode: bool = False,
|
|
89
|
+
show_welcome: bool = True,
|
|
85
90
|
) -> None:
|
|
86
91
|
"""Initialize the Vibecore app with context and agent.
|
|
87
92
|
|
|
@@ -90,6 +95,7 @@ class VibecoreApp(App):
|
|
|
90
95
|
agent: The Agent instance to use
|
|
91
96
|
session_id: Optional session ID to load existing session
|
|
92
97
|
print_mode: Whether to run in print mode (useful for pipes)
|
|
98
|
+
show_welcome: Whether to show the welcome message (default: True)
|
|
93
99
|
"""
|
|
94
100
|
self.context = context
|
|
95
101
|
self.context.app = self # Set the app reference in context
|
|
@@ -99,6 +105,7 @@ class VibecoreApp(App):
|
|
|
99
105
|
self.current_worker: Worker[None] | None = None
|
|
100
106
|
self._session_id_provided = session_id is not None # Track if continuing session
|
|
101
107
|
self.print_mode = print_mode
|
|
108
|
+
self.show_welcome = show_welcome
|
|
102
109
|
self.message_queue: deque[str] = deque() # Queue for user messages
|
|
103
110
|
|
|
104
111
|
# Initialize session based on settings
|
|
@@ -124,7 +131,8 @@ class VibecoreApp(App):
|
|
|
124
131
|
yield Header()
|
|
125
132
|
yield AppFooter()
|
|
126
133
|
with MainScroll(id="messages"):
|
|
127
|
-
|
|
134
|
+
if self.show_welcome:
|
|
135
|
+
yield Welcome()
|
|
128
136
|
|
|
129
137
|
async def on_mount(self) -> None:
|
|
130
138
|
"""Called when the app is mounted."""
|
|
@@ -162,6 +170,8 @@ class VibecoreApp(App):
|
|
|
162
170
|
Args:
|
|
163
171
|
message: The message to add
|
|
164
172
|
"""
|
|
173
|
+
if not self.is_running:
|
|
174
|
+
raise AppIsExiting("App is not running")
|
|
165
175
|
main_scroll = self.query_one("#messages", MainScroll)
|
|
166
176
|
await main_scroll.mount(message)
|
|
167
177
|
|
|
@@ -225,6 +235,14 @@ class VibecoreApp(App):
|
|
|
225
235
|
else:
|
|
226
236
|
footer.hide_loading()
|
|
227
237
|
|
|
238
|
+
async def wait_for_user_input(self) -> str:
|
|
239
|
+
"""Used in flow mode. See examples/basic_agent.py"""
|
|
240
|
+
self.agent_status = "waiting_user_input"
|
|
241
|
+
self.user_input_event = asyncio.Event()
|
|
242
|
+
await self.user_input_event.wait()
|
|
243
|
+
user_input = self.message_queue.pop()
|
|
244
|
+
return user_input
|
|
245
|
+
|
|
228
246
|
async def on_my_text_area_user_message(self, event: MyTextArea.UserMessage) -> None:
|
|
229
247
|
"""Handle user messages from the text area."""
|
|
230
248
|
if event.text:
|
|
@@ -248,8 +266,11 @@ class VibecoreApp(App):
|
|
|
248
266
|
await self.add_message(user_message)
|
|
249
267
|
user_message.scroll_visible()
|
|
250
268
|
|
|
251
|
-
|
|
269
|
+
if self.agent_status == "waiting_user_input":
|
|
270
|
+
self.message_queue.append(event.text)
|
|
271
|
+
self.user_input_event.set()
|
|
252
272
|
if self.agent_status == "running":
|
|
273
|
+
# If agent is running, queue the message
|
|
253
274
|
self.message_queue.append(event.text)
|
|
254
275
|
log(f"Message queued: {event.text}")
|
|
255
276
|
footer = self.query_one(AppFooter)
|
|
@@ -268,7 +289,7 @@ class VibecoreApp(App):
|
|
|
268
289
|
if reasoning_effort is not None:
|
|
269
290
|
# Create a copy of the agent with updated model settings
|
|
270
291
|
current_settings = self.agent.model_settings or ModelSettings()
|
|
271
|
-
new_reasoning = Reasoning(effort=reasoning_effort, summary=
|
|
292
|
+
new_reasoning = Reasoning(effort=reasoning_effort, summary=settings.reasoning_summary)
|
|
272
293
|
updated_settings = ModelSettings(
|
|
273
294
|
include_usage=current_settings.include_usage,
|
|
274
295
|
reasoning=new_reasoning,
|
|
@@ -496,8 +517,9 @@ class VibecoreApp(App):
|
|
|
496
517
|
for welcome in main_scroll.query("Welcome"):
|
|
497
518
|
welcome.remove()
|
|
498
519
|
|
|
499
|
-
# Add welcome widget back
|
|
500
|
-
|
|
520
|
+
# Add welcome widget back if show_welcome is True
|
|
521
|
+
if self.show_welcome:
|
|
522
|
+
await main_scroll.mount(Welcome())
|
|
501
523
|
|
|
502
524
|
# Show system message to confirm the clear operation
|
|
503
525
|
system_message = SystemMessage(f"✨ Session cleared! Started new session: {new_session_id}")
|
vibecore/settings.py
CHANGED
|
@@ -4,9 +4,10 @@ import os
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Literal
|
|
6
6
|
|
|
7
|
-
from agents import Model, OpenAIChatCompletionsModel
|
|
7
|
+
from agents import Model, ModelSettings, OpenAIChatCompletionsModel
|
|
8
8
|
from agents.models.multi_provider import MultiProvider
|
|
9
|
-
from
|
|
9
|
+
from openai.types import Reasoning
|
|
10
|
+
from pydantic import BaseModel, Field, field_validator
|
|
10
11
|
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, YamlConfigSettingsSource
|
|
11
12
|
|
|
12
13
|
from vibecore.models import AnthropicModel
|
|
@@ -92,9 +93,10 @@ class Settings(BaseSettings):
|
|
|
92
93
|
default_model: str = Field(
|
|
93
94
|
# default="o3",
|
|
94
95
|
# default="gpt-4.1",
|
|
96
|
+
default="gpt-5",
|
|
95
97
|
# default="qwen3-30b-a3b-mlx@8bit",
|
|
96
98
|
# default="mistralai/devstral-small-2507",
|
|
97
|
-
default="anthropic/claude-sonnet-4-20250514",
|
|
99
|
+
# default="anthropic/claude-sonnet-4-20250514",
|
|
98
100
|
# default="anthropic/claude-3-5-haiku-20241022",
|
|
99
101
|
# default="litellm/deepseek/deepseek-chat",
|
|
100
102
|
description="Default model to use for agents (e.g., 'gpt-4.1', 'o3-mini', 'anthropic/claude-sonnet-4')",
|
|
@@ -109,6 +111,18 @@ class Settings(BaseSettings):
|
|
|
109
111
|
default=None,
|
|
110
112
|
description="Default reasoning effort level for agents (null, 'minimal', 'low', 'medium', 'high')",
|
|
111
113
|
)
|
|
114
|
+
reasoning_summary: Literal["auto", "concise", "detailed"] | None = Field(
|
|
115
|
+
default="auto",
|
|
116
|
+
description="Reasoning summary mode ('auto', 'concise', 'detailed', or 'off')",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@field_validator("reasoning_summary", mode="before")
|
|
120
|
+
@classmethod
|
|
121
|
+
def validate_reasoning_summary(cls, v):
|
|
122
|
+
"""Convert string 'null' to None for reasoning_summary field."""
|
|
123
|
+
if v == "off" or v == "":
|
|
124
|
+
return None
|
|
125
|
+
return v
|
|
112
126
|
|
|
113
127
|
# Session configuration
|
|
114
128
|
session: SessionSettings = Field(
|
|
@@ -138,6 +152,17 @@ class Settings(BaseSettings):
|
|
|
138
152
|
return OpenAIChatCompletionsModel(self.default_model, openai_provider._get_client())
|
|
139
153
|
return self.default_model
|
|
140
154
|
|
|
155
|
+
@property
|
|
156
|
+
def default_model_settings(self) -> ModelSettings:
|
|
157
|
+
"""Get the default model settings."""
|
|
158
|
+
return ModelSettings(
|
|
159
|
+
include_usage=True,
|
|
160
|
+
reasoning=Reasoning(
|
|
161
|
+
summary=self.reasoning_summary,
|
|
162
|
+
effort=self.reasoning_effort,
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
|
|
141
166
|
@classmethod
|
|
142
167
|
def settings_customise_sources(
|
|
143
168
|
cls,
|
vibecore/tools/task/executor.py
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Webfetch execution logic for fetching and converting web content."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
import html2text
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .models import WebFetchParams
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def fetch_url(params: WebFetchParams) -> str:
|
|
13
|
+
"""Fetch a URL and convert its content to Markdown.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
params: WebFetch parameters including URL and options
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Markdown-formatted content or error message
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
# Validate URL
|
|
23
|
+
parsed = urlparse(params.url)
|
|
24
|
+
if not parsed.scheme:
|
|
25
|
+
return f"Error: Invalid URL - missing scheme (http:// or https://): {params.url}"
|
|
26
|
+
if not parsed.netloc:
|
|
27
|
+
return f"Error: Invalid URL - missing domain: {params.url}"
|
|
28
|
+
if parsed.scheme not in ["http", "https"]:
|
|
29
|
+
return f"Error: Unsupported URL scheme: {parsed.scheme}"
|
|
30
|
+
|
|
31
|
+
# Configure headers
|
|
32
|
+
headers = {
|
|
33
|
+
"User-Agent": params.user_agent
|
|
34
|
+
or "Mozilla/5.0 (compatible; Vibecore/1.0; +https://github.com/serialx/vibecore)",
|
|
35
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,"
|
|
36
|
+
"text/plain;q=0.8,application/json;q=0.7,*/*;q=0.5",
|
|
37
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
38
|
+
"Accept-Encoding": "gzip, deflate",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Fetch the URL
|
|
42
|
+
async with httpx.AsyncClient(
|
|
43
|
+
timeout=httpx.Timeout(params.timeout),
|
|
44
|
+
follow_redirects=params.follow_redirects,
|
|
45
|
+
) as client:
|
|
46
|
+
response = await client.get(params.url, headers=headers)
|
|
47
|
+
response.raise_for_status()
|
|
48
|
+
|
|
49
|
+
# Get content type
|
|
50
|
+
content_type = response.headers.get("content-type", "").lower()
|
|
51
|
+
|
|
52
|
+
# Handle different content types
|
|
53
|
+
if "application/json" in content_type:
|
|
54
|
+
# Pretty-print JSON as Markdown code block
|
|
55
|
+
try:
|
|
56
|
+
json_data = response.json()
|
|
57
|
+
content = f"```json\n{json.dumps(json_data, indent=2)}\n```"
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
content = response.text[: params.max_length]
|
|
60
|
+
|
|
61
|
+
elif "text/html" in content_type or "application/xhtml" in content_type:
|
|
62
|
+
# Convert HTML to Markdown
|
|
63
|
+
html_content = response.text[: params.max_length]
|
|
64
|
+
|
|
65
|
+
# Configure html2text
|
|
66
|
+
h = html2text.HTML2Text()
|
|
67
|
+
h.ignore_links = False
|
|
68
|
+
h.ignore_images = False
|
|
69
|
+
h.ignore_emphasis = False
|
|
70
|
+
h.body_width = 0 # Don't wrap lines
|
|
71
|
+
h.skip_internal_links = False
|
|
72
|
+
h.inline_links = True
|
|
73
|
+
h.wrap_links = False
|
|
74
|
+
h.wrap_list_items = False
|
|
75
|
+
h.ul_item_mark = "-"
|
|
76
|
+
h.emphasis_mark = "*"
|
|
77
|
+
h.strong_mark = "**"
|
|
78
|
+
|
|
79
|
+
content = h.handle(html_content)
|
|
80
|
+
|
|
81
|
+
# Clean up excessive newlines
|
|
82
|
+
while "\n\n\n" in content:
|
|
83
|
+
content = content.replace("\n\n\n", "\n\n")
|
|
84
|
+
content = content.strip()
|
|
85
|
+
|
|
86
|
+
elif "text/plain" in content_type or "text/" in content_type:
|
|
87
|
+
# Plain text - return as is
|
|
88
|
+
content = response.text[: params.max_length]
|
|
89
|
+
|
|
90
|
+
else:
|
|
91
|
+
# Unknown content type - try to handle as text
|
|
92
|
+
content = response.text[: params.max_length]
|
|
93
|
+
if not content:
|
|
94
|
+
return f"Error: Unable to extract text content from {content_type}"
|
|
95
|
+
|
|
96
|
+
# Add metadata
|
|
97
|
+
metadata = [
|
|
98
|
+
f"# Content from {params.url}",
|
|
99
|
+
f"**Status Code:** {response.status_code}",
|
|
100
|
+
f"**Content Type:** {content_type.split(';')[0] if content_type else 'unknown'}",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
# Add redirect info if applicable
|
|
104
|
+
if response.history:
|
|
105
|
+
metadata.append(f"**Redirected:** {len(response.history)} time(s)")
|
|
106
|
+
metadata.append(f"**Final URL:** {response.url}")
|
|
107
|
+
|
|
108
|
+
metadata.append("") # Empty line before content
|
|
109
|
+
|
|
110
|
+
# Check if content was truncated
|
|
111
|
+
if len(response.text) > params.max_length:
|
|
112
|
+
metadata.append(f"*Note: Content truncated to {params.max_length} characters*")
|
|
113
|
+
metadata.append("")
|
|
114
|
+
|
|
115
|
+
# Combine metadata and content
|
|
116
|
+
full_content = "\n".join(metadata) + "\n" + content
|
|
117
|
+
|
|
118
|
+
return full_content
|
|
119
|
+
|
|
120
|
+
except httpx.TimeoutException:
|
|
121
|
+
return f"Error: Request timed out after {params.timeout} seconds"
|
|
122
|
+
except httpx.HTTPStatusError as e:
|
|
123
|
+
return f"Error: HTTP {e.response.status_code} - {e.response.reason_phrase}"
|
|
124
|
+
except httpx.RequestError as e:
|
|
125
|
+
return f"Error: Failed to connect to {params.url}: {e!s}"
|
|
126
|
+
except Exception as e:
|
|
127
|
+
return f"Error: Unexpected error while fetching {params.url}: {e!s}"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Data models for the webfetch tool."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WebFetchParams(BaseModel):
|
|
7
|
+
"""Parameters for fetching web content."""
|
|
8
|
+
|
|
9
|
+
url: str = Field(description="The URL to fetch content from")
|
|
10
|
+
timeout: int = Field(default=30, description="Request timeout in seconds")
|
|
11
|
+
user_agent: str | None = Field(
|
|
12
|
+
default=None,
|
|
13
|
+
description="Optional custom User-Agent header",
|
|
14
|
+
)
|
|
15
|
+
follow_redirects: bool = Field(
|
|
16
|
+
default=True,
|
|
17
|
+
description="Whether to follow HTTP redirects",
|
|
18
|
+
)
|
|
19
|
+
max_length: int = Field(
|
|
20
|
+
default=1000000, # ~1MB of text
|
|
21
|
+
description="Maximum content length to fetch in characters",
|
|
22
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Webfetch tool for Vibecore agents."""
|
|
2
|
+
|
|
3
|
+
from agents import RunContextWrapper, function_tool
|
|
4
|
+
|
|
5
|
+
from vibecore.context import VibecoreContext
|
|
6
|
+
|
|
7
|
+
from .executor import fetch_url
|
|
8
|
+
from .models import WebFetchParams
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@function_tool
|
|
12
|
+
async def webfetch(
|
|
13
|
+
ctx: RunContextWrapper[VibecoreContext],
|
|
14
|
+
url: str,
|
|
15
|
+
timeout: int = 30,
|
|
16
|
+
follow_redirects: bool = True,
|
|
17
|
+
) -> str:
|
|
18
|
+
"""Fetch content from a URL and convert it to Markdown format.
|
|
19
|
+
|
|
20
|
+
This tool fetches web content and converts it to clean, readable Markdown.
|
|
21
|
+
It handles HTML pages, JSON APIs, and plain text content appropriately.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
ctx: The run context wrapper
|
|
25
|
+
url: The URL to fetch content from (must include http:// or https://)
|
|
26
|
+
timeout: Request timeout in seconds (default: 30)
|
|
27
|
+
follow_redirects: Whether to follow HTTP redirects (default: True)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Markdown-formatted content from the URL, including metadata about the
|
|
31
|
+
request (status code, content type, etc.) or an error message if the
|
|
32
|
+
fetch fails.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
- Fetch a webpage: url="https://example.com"
|
|
36
|
+
- Fetch JSON API: url="https://api.example.com/data"
|
|
37
|
+
- Fetch with timeout: url="https://slow-site.com", timeout=60
|
|
38
|
+
- Don't follow redirects: url="https://short.link/abc", follow_redirects=False
|
|
39
|
+
"""
|
|
40
|
+
params = WebFetchParams(
|
|
41
|
+
url=url,
|
|
42
|
+
timeout=timeout,
|
|
43
|
+
follow_redirects=follow_redirects,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return await fetch_url(params)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Base classes for websearch backends."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from .models import SearchParams
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WebSearchBackend(ABC):
|
|
9
|
+
"""Abstract base class for web search backends."""
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
async def search(self, params: SearchParams) -> str:
|
|
13
|
+
"""Perform a web search.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
params: Search parameters
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
JSON string containing search results or error message
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def name(self) -> str:
|
|
26
|
+
"""Return the name of this backend."""
|
|
27
|
+
pass
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""DuckDuckGo search backend implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from ddgs import DDGS
|
|
6
|
+
|
|
7
|
+
from ..base import WebSearchBackend
|
|
8
|
+
from ..models import SearchParams, SearchResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DDGSBackend(WebSearchBackend):
|
|
12
|
+
"""DuckDuckGo search backend using ddgs library."""
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def name(self) -> str:
|
|
16
|
+
"""Return the name of this backend."""
|
|
17
|
+
return "DuckDuckGo"
|
|
18
|
+
|
|
19
|
+
async def search(self, params: SearchParams) -> str:
|
|
20
|
+
"""Perform a web search using ddgs.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
params: Search parameters
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
JSON string containing search results or error message
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
# Create DDGS instance
|
|
30
|
+
ddgs = DDGS()
|
|
31
|
+
|
|
32
|
+
# Perform search (synchronous call)
|
|
33
|
+
# ddgs.text() expects 'query' as first positional argument
|
|
34
|
+
raw_results = ddgs.text(
|
|
35
|
+
query=params.query,
|
|
36
|
+
region=params.region,
|
|
37
|
+
safesearch=params.safesearch,
|
|
38
|
+
max_results=params.max_results,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Convert to our model format
|
|
42
|
+
results = []
|
|
43
|
+
for r in raw_results:
|
|
44
|
+
result = SearchResult(
|
|
45
|
+
title=r.get("title", ""),
|
|
46
|
+
href=r.get("href", ""),
|
|
47
|
+
body=r.get("body", ""),
|
|
48
|
+
)
|
|
49
|
+
results.append(result.model_dump())
|
|
50
|
+
|
|
51
|
+
if not results:
|
|
52
|
+
return json.dumps({"success": False, "message": "No search results found", "results": []})
|
|
53
|
+
|
|
54
|
+
return json.dumps(
|
|
55
|
+
{
|
|
56
|
+
"success": True,
|
|
57
|
+
"message": f"Found {len(results)} result{'s' if len(results) != 1 else ''}",
|
|
58
|
+
"query": params.query,
|
|
59
|
+
"results": results,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return json.dumps({"success": False, "message": f"Search failed: {e!s}", "results": []})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Websearch execution logic with backend selection."""
|
|
2
|
+
|
|
3
|
+
from .base import WebSearchBackend
|
|
4
|
+
from .ddgs import DDGSBackend
|
|
5
|
+
from .models import SearchParams
|
|
6
|
+
|
|
7
|
+
# Default backend
|
|
8
|
+
_default_backend: WebSearchBackend = DDGSBackend()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def set_default_backend(backend: WebSearchBackend) -> None:
|
|
12
|
+
"""Set the default search backend.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
backend: The backend to use for searches
|
|
16
|
+
"""
|
|
17
|
+
global _default_backend
|
|
18
|
+
_default_backend = backend
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_default_backend() -> WebSearchBackend:
|
|
22
|
+
"""Get the current default search backend.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The current default backend
|
|
26
|
+
"""
|
|
27
|
+
return _default_backend
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def perform_websearch(params: SearchParams, backend: WebSearchBackend | None = None) -> str:
|
|
31
|
+
"""Perform a web search using the specified or default backend.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
params: Search parameters
|
|
35
|
+
backend: Optional specific backend to use (defaults to default backend)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
JSON string containing search results or error message
|
|
39
|
+
"""
|
|
40
|
+
if backend is None:
|
|
41
|
+
backend = _default_backend
|
|
42
|
+
|
|
43
|
+
return await backend.search(params)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Models for websearch tool."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SearchResult(BaseModel):
|
|
7
|
+
"""A single search result."""
|
|
8
|
+
|
|
9
|
+
title: str = Field(description="Title of the search result")
|
|
10
|
+
href: str = Field(description="URL of the search result")
|
|
11
|
+
body: str = Field(description="Description/snippet of the result")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SearchParams(BaseModel):
|
|
15
|
+
"""Parameters for web search."""
|
|
16
|
+
|
|
17
|
+
query: str = Field(description="Search query")
|
|
18
|
+
max_results: int = Field(default=5, description="Maximum number of results to return")
|
|
19
|
+
region: str | None = Field(default=None, description="Region code (e.g., 'us-en')")
|
|
20
|
+
safesearch: str = Field(default="moderate", description="SafeSearch setting: 'on', 'moderate', or 'off'")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Websearch tool for Vibecore agents."""
|
|
2
|
+
|
|
3
|
+
from agents import RunContextWrapper, function_tool
|
|
4
|
+
|
|
5
|
+
from vibecore.context import VibecoreContext
|
|
6
|
+
|
|
7
|
+
from .executor import perform_websearch
|
|
8
|
+
from .models import SearchParams
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@function_tool
|
|
12
|
+
async def websearch(
|
|
13
|
+
ctx: RunContextWrapper[VibecoreContext],
|
|
14
|
+
query: str,
|
|
15
|
+
max_results: int = 5,
|
|
16
|
+
region: str | None = None,
|
|
17
|
+
safesearch: str = "moderate",
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Search the web for information using DuckDuckGo.
|
|
20
|
+
|
|
21
|
+
This tool allows you to search the web for current information, news, and general knowledge.
|
|
22
|
+
It supports advanced search operators like quotes for exact phrases, minus for exclusions,
|
|
23
|
+
site: for specific domains, and filetype: for specific file types.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
ctx: The run context wrapper
|
|
27
|
+
query: The search query (supports advanced operators like "exact phrase", -exclude, site:example.com)
|
|
28
|
+
max_results: Maximum number of results to return (default: 5)
|
|
29
|
+
region: Optional region code for localized results (e.g., 'us-en' for US English)
|
|
30
|
+
safesearch: SafeSearch filter level ('on', 'moderate', or 'off', default: 'moderate')
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
JSON string containing search results with title, URL, and snippet for each result
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
- Basic search: query="python programming"
|
|
37
|
+
- Exact phrase: query='"machine learning algorithms"'
|
|
38
|
+
- Exclude terms: query="python -javascript"
|
|
39
|
+
- Site-specific: query="AI site:github.com"
|
|
40
|
+
- File type: query="climate change filetype:pdf"
|
|
41
|
+
"""
|
|
42
|
+
params = SearchParams(
|
|
43
|
+
query=query,
|
|
44
|
+
max_results=max_results,
|
|
45
|
+
region=region,
|
|
46
|
+
safesearch=safesearch,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return await perform_websearch(params)
|
|
@@ -18,6 +18,8 @@ from vibecore.widgets.tool_messages import (
|
|
|
18
18
|
TaskToolMessage,
|
|
19
19
|
TodoWriteToolMessage,
|
|
20
20
|
ToolMessage,
|
|
21
|
+
WebFetchToolMessage,
|
|
22
|
+
WebSearchToolMessage,
|
|
21
23
|
WriteToolMessage,
|
|
22
24
|
)
|
|
23
25
|
|
|
@@ -113,6 +115,20 @@ def create_tool_message(
|
|
|
113
115
|
else:
|
|
114
116
|
return WriteToolMessage(file_path=file_path, content=content, status=status)
|
|
115
117
|
|
|
118
|
+
elif tool_name == "websearch":
|
|
119
|
+
query = args_dict.get("query", "") if args_dict else ""
|
|
120
|
+
if output is not None:
|
|
121
|
+
return WebSearchToolMessage(query=query, output=output, status=status)
|
|
122
|
+
else:
|
|
123
|
+
return WebSearchToolMessage(query=query, status=status)
|
|
124
|
+
|
|
125
|
+
elif tool_name == "webfetch":
|
|
126
|
+
url = args_dict.get("url", "") if args_dict else ""
|
|
127
|
+
if output is not None:
|
|
128
|
+
return WebFetchToolMessage(url=url, output=output, status=status)
|
|
129
|
+
else:
|
|
130
|
+
return WebFetchToolMessage(url=url, status=status)
|
|
131
|
+
|
|
116
132
|
# Default to generic ToolMessage for all other tools
|
|
117
133
|
else:
|
|
118
134
|
if output is not None:
|
|
@@ -481,3 +481,120 @@ class MCPToolMessage(BaseToolMessage):
|
|
|
481
481
|
yield ExpandableMarkdown(
|
|
482
482
|
processed_output, language="", truncated_lines=5, classes="mcp-output-markdown"
|
|
483
483
|
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class WebSearchToolMessage(BaseToolMessage):
|
|
487
|
+
"""A widget to display web search results."""
|
|
488
|
+
|
|
489
|
+
search_query: reactive[str] = reactive("")
|
|
490
|
+
|
|
491
|
+
def __init__(self, query: str, output: str = "", status: MessageStatus = MessageStatus.EXECUTING, **kwargs) -> None:
|
|
492
|
+
"""
|
|
493
|
+
Construct a WebSearchToolMessage.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
query: The search query.
|
|
497
|
+
output: The search results as JSON string (optional, can be set later).
|
|
498
|
+
status: The status of execution.
|
|
499
|
+
**kwargs: Additional keyword arguments for Widget.
|
|
500
|
+
"""
|
|
501
|
+
super().__init__(status=status, **kwargs)
|
|
502
|
+
self.search_query = query
|
|
503
|
+
self.output = output
|
|
504
|
+
|
|
505
|
+
def compose(self) -> ComposeResult:
|
|
506
|
+
"""Create child widgets for the search message."""
|
|
507
|
+
# Header line
|
|
508
|
+
header = f"WebSearch({self.search_query})"
|
|
509
|
+
yield MessageHeader("⏺", header, status=self.status)
|
|
510
|
+
|
|
511
|
+
# Process and display search results
|
|
512
|
+
if self.output:
|
|
513
|
+
try:
|
|
514
|
+
result_data = json.loads(self.output)
|
|
515
|
+
if result_data.get("success") and result_data.get("results"):
|
|
516
|
+
with Horizontal(classes="tool-output"):
|
|
517
|
+
yield Static("└─", classes="tool-output-prefix")
|
|
518
|
+
with Vertical(classes="tool-output-content"):
|
|
519
|
+
# Format results as markdown
|
|
520
|
+
markdown_results = []
|
|
521
|
+
for i, result in enumerate(result_data["results"], 1):
|
|
522
|
+
title = result.get("title", "No title")
|
|
523
|
+
href = result.get("href", "")
|
|
524
|
+
body = result.get("body", "")
|
|
525
|
+
|
|
526
|
+
# Format each result
|
|
527
|
+
result_md = f"**{i}. [{title}]({href})**"
|
|
528
|
+
if body:
|
|
529
|
+
# Truncate body if too long
|
|
530
|
+
max_body_length = 200
|
|
531
|
+
if len(body) > max_body_length:
|
|
532
|
+
body = body[:max_body_length] + "..."
|
|
533
|
+
result_md += f"\n {body}"
|
|
534
|
+
if href:
|
|
535
|
+
result_md += f"\n 🔗 {href}"
|
|
536
|
+
|
|
537
|
+
markdown_results.append(result_md)
|
|
538
|
+
|
|
539
|
+
# Join all results with spacing
|
|
540
|
+
all_results = "\n\n".join(markdown_results)
|
|
541
|
+
|
|
542
|
+
# Add result count message
|
|
543
|
+
count_msg = result_data.get("message", "")
|
|
544
|
+
if count_msg:
|
|
545
|
+
all_results = f"_{count_msg}_\n\n{all_results}"
|
|
546
|
+
|
|
547
|
+
yield ExpandableMarkdown(
|
|
548
|
+
all_results, language="", truncated_lines=10, classes="websearch-results"
|
|
549
|
+
)
|
|
550
|
+
else:
|
|
551
|
+
# No results or error
|
|
552
|
+
with Horizontal(classes="tool-output"):
|
|
553
|
+
yield Static("└─", classes="tool-output-prefix")
|
|
554
|
+
with Vertical(classes="tool-output-content"):
|
|
555
|
+
message = result_data.get("message", "No results found")
|
|
556
|
+
yield Static(message, classes="websearch-no-results")
|
|
557
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
558
|
+
# Fallback to raw output if JSON parsing fails
|
|
559
|
+
yield from self._render_output(self.output, truncated_lines=5)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
class WebFetchToolMessage(BaseToolMessage):
|
|
563
|
+
"""A widget to display fetched web content."""
|
|
564
|
+
|
|
565
|
+
fetch_url: reactive[str] = reactive("")
|
|
566
|
+
|
|
567
|
+
def __init__(self, url: str, output: str = "", status: MessageStatus = MessageStatus.EXECUTING, **kwargs) -> None:
|
|
568
|
+
"""
|
|
569
|
+
Construct a WebFetchToolMessage.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
url: The URL that was fetched.
|
|
573
|
+
output: The fetched content as Markdown (optional, can be set later).
|
|
574
|
+
status: The status of execution.
|
|
575
|
+
**kwargs: Additional keyword arguments for Widget.
|
|
576
|
+
"""
|
|
577
|
+
super().__init__(status=status, **kwargs)
|
|
578
|
+
self.fetch_url = url
|
|
579
|
+
self.output = output
|
|
580
|
+
|
|
581
|
+
def compose(self) -> ComposeResult:
|
|
582
|
+
"""Create child widgets for the fetch message."""
|
|
583
|
+
# Header line
|
|
584
|
+
header = f"WebFetch({self.fetch_url})"
|
|
585
|
+
yield MessageHeader("⏺", header, status=self.status)
|
|
586
|
+
|
|
587
|
+
# Display fetched content
|
|
588
|
+
if self.output:
|
|
589
|
+
with Horizontal(classes="tool-output"):
|
|
590
|
+
yield Static("└─", classes="tool-output-prefix")
|
|
591
|
+
with Vertical(classes="tool-output-content"):
|
|
592
|
+
# Check if it's an error message
|
|
593
|
+
if self.output.startswith("Error:"):
|
|
594
|
+
yield Static(self.output, classes="webfetch-error")
|
|
595
|
+
else:
|
|
596
|
+
# Display as expandable markdown content
|
|
597
|
+
# Default to showing first 15 lines since web content can be long
|
|
598
|
+
yield ExpandableMarkdown(
|
|
599
|
+
self.output, language="", truncated_lines=15, classes="webfetch-content"
|
|
600
|
+
)
|
|
@@ -286,4 +286,52 @@ MCPToolMessage {
|
|
|
286
286
|
width: 1fr;
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
WebSearchToolMessage {
|
|
292
|
+
Horizontal.tool-output {
|
|
293
|
+
height: auto;
|
|
294
|
+
|
|
295
|
+
&> .tool-output-prefix {
|
|
296
|
+
height: 1;
|
|
297
|
+
width: 5;
|
|
298
|
+
padding-left: 2;
|
|
299
|
+
padding-right: 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
&> Vertical.tool-output-content {
|
|
303
|
+
height: auto;
|
|
304
|
+
|
|
305
|
+
&> .tool-output-content {
|
|
306
|
+
# color: $text-muted;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
&> .tool-output-content-more {
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
WebFetchToolMessage {
|
|
316
|
+
Horizontal.tool-output {
|
|
317
|
+
height: auto;
|
|
318
|
+
|
|
319
|
+
&> .tool-output-prefix {
|
|
320
|
+
height: 1;
|
|
321
|
+
width: 5;
|
|
322
|
+
padding-left: 2;
|
|
323
|
+
padding-right: 1;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
&> Vertical.tool-output-content {
|
|
327
|
+
height: auto;
|
|
328
|
+
|
|
329
|
+
&> .tool-output-content {
|
|
330
|
+
# color: $text-muted;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
&> .tool-output-content-more {
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
289
337
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vibecore
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0b1
|
|
4
4
|
Summary: Build your own AI-powered automation tools in the terminal with this extensible agent framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/serialx/vibecore
|
|
6
6
|
Project-URL: Repository, https://github.com/serialx/vibecore
|
|
@@ -27,6 +27,8 @@ Classifier: Topic :: Terminals
|
|
|
27
27
|
Classifier: Topic :: Text Processing :: Linguistic
|
|
28
28
|
Classifier: Typing :: Typed
|
|
29
29
|
Requires-Python: >=3.11
|
|
30
|
+
Requires-Dist: ddgs>=9.5.4
|
|
31
|
+
Requires-Dist: html2text>=2024.2.26
|
|
30
32
|
Requires-Dist: litellm>=1.72.4
|
|
31
33
|
Requires-Dist: openai-agents[litellm]>=0.2.2
|
|
32
34
|
Requires-Dist: pydantic-settings>=2.10.1
|
|
@@ -65,6 +67,7 @@ Built on [Textual](https://textual.textualize.io/) and the [OpenAI Agents SDK](h
|
|
|
65
67
|
|
|
66
68
|
### Key Features
|
|
67
69
|
|
|
70
|
+
- **Flow Mode (Experimental)** - Build structured agent-based applications with programmatic conversation control
|
|
68
71
|
- **AI-Powered Chat Interface** - Interact with state-of-the-art language models through an intuitive terminal interface
|
|
69
72
|
- **Rich Tool Integration** - Built-in tools for file operations, shell commands, Python execution, and task management
|
|
70
73
|
- **MCP Support** - Connect to external tools and services via Model Context Protocol servers
|
|
@@ -79,6 +82,26 @@ Built on [Textual](https://textual.textualize.io/) and the [OpenAI Agents SDK](h
|
|
|
79
82
|
### Prerequisites
|
|
80
83
|
|
|
81
84
|
- Python 3.11 or higher
|
|
85
|
+
- (Optional) [uv](https://docs.astral.sh/uv/) for quick testing and better package management
|
|
86
|
+
|
|
87
|
+
### Quick Test (No Installation)
|
|
88
|
+
|
|
89
|
+
Try vibecore instantly without installing it:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Install uv if you don't have it (optional)
|
|
93
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
94
|
+
|
|
95
|
+
# Configure your API key
|
|
96
|
+
export ANTHROPIC_API_KEY="your-api-key-here"
|
|
97
|
+
# or
|
|
98
|
+
export OPENAI_API_KEY="your-api-key-here"
|
|
99
|
+
|
|
100
|
+
# Run vibecore directly with uvx
|
|
101
|
+
uvx vibecore
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This will download and run vibecore in an isolated environment without affecting your system Python installation.
|
|
82
105
|
|
|
83
106
|
### Install from PyPI
|
|
84
107
|
|
|
@@ -136,6 +159,88 @@ Once vibecore is running, you can:
|
|
|
136
159
|
- `/help` - Show help and keyboard shortcuts
|
|
137
160
|
- `/clear` - Clear the current session and start a new one
|
|
138
161
|
|
|
162
|
+
## Flow Mode (Experimental)
|
|
163
|
+
|
|
164
|
+
Flow Mode is vibecore's **key differentiator** - it transforms the framework from a chat interface into a platform for building structured agent-based applications with programmatic conversation control.
|
|
165
|
+
|
|
166
|
+
### What is Flow Mode?
|
|
167
|
+
|
|
168
|
+
Flow Mode allows you to:
|
|
169
|
+
- **Define custom conversation logic** that controls how agents process user input
|
|
170
|
+
- **Build multi-step workflows** with defined sequences and decision points
|
|
171
|
+
- **Orchestrate multiple agents** with handoffs and shared context
|
|
172
|
+
- **Maintain conversation state** across interactions
|
|
173
|
+
- **Create agent-based applications** rather than just chatbots
|
|
174
|
+
|
|
175
|
+
### Example: Simple Flow
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
import asyncio
|
|
179
|
+
from agents import Agent, Runner
|
|
180
|
+
from vibecore.flow import flow, UserInputFunc
|
|
181
|
+
from vibecore.context import VibecoreContext
|
|
182
|
+
|
|
183
|
+
# Define your agent with tools
|
|
184
|
+
agent = Agent[VibecoreContext](
|
|
185
|
+
name="Assistant",
|
|
186
|
+
instructions="You are a helpful assistant",
|
|
187
|
+
tools=[...], # Your tools here
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Define your conversation logic
|
|
191
|
+
async def logic(app, ctx: VibecoreContext, user_input: UserInputFunc):
|
|
192
|
+
# Get user input programmatically
|
|
193
|
+
user_message = await user_input("What would you like to do?")
|
|
194
|
+
|
|
195
|
+
# Process with agent
|
|
196
|
+
result = Runner.run_streamed(
|
|
197
|
+
agent,
|
|
198
|
+
input=user_message,
|
|
199
|
+
context=ctx,
|
|
200
|
+
session=app.session,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Handle the response
|
|
204
|
+
app.current_worker = app.handle_streamed_response(result)
|
|
205
|
+
await app.current_worker.wait()
|
|
206
|
+
|
|
207
|
+
# Run the flow
|
|
208
|
+
async def main():
|
|
209
|
+
await flow(agent, logic)
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
asyncio.run(main())
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Example: Multi-Agent Customer Service
|
|
216
|
+
|
|
217
|
+
Flow Mode shines when building complex multi-agent systems. See `examples/customer_service.py` for a complete implementation featuring:
|
|
218
|
+
|
|
219
|
+
- **Triage Agent**: Routes requests to appropriate specialists
|
|
220
|
+
- **FAQ Agent**: Handles frequently asked questions
|
|
221
|
+
- **Booking Agent**: Manages seat reservations
|
|
222
|
+
- **Agent Handoffs**: Seamless transitions between agents with context preservation
|
|
223
|
+
- **Shared State**: Maintains customer information across the conversation
|
|
224
|
+
|
|
225
|
+
### Key Components
|
|
226
|
+
|
|
227
|
+
- **`flow()`**: Entry point that sets up the Vibecore app with your custom logic
|
|
228
|
+
- **`logic()`**: Your async function that controls the conversation flow
|
|
229
|
+
- **`UserInputFunc`**: Provides programmatic user input collection
|
|
230
|
+
- **`VibecoreContext`**: Shared state across tools and agents
|
|
231
|
+
- **Agent Handoffs**: Transfer control between specialized agents
|
|
232
|
+
|
|
233
|
+
### Use Cases
|
|
234
|
+
|
|
235
|
+
Flow Mode enables building:
|
|
236
|
+
- **Customer service systems** with routing and escalation
|
|
237
|
+
- **Guided workflows** for complex tasks
|
|
238
|
+
- **Interactive tutorials** with step-by-step guidance
|
|
239
|
+
- **Task automation** with human-in-the-loop controls
|
|
240
|
+
- **Multi-stage data processing** pipelines
|
|
241
|
+
|
|
242
|
+
The examples in the `examples/` directory are adapted from the official OpenAI Agents SDK with minimal modifications, demonstrating how easily you can build sophisticated agent applications with vibecore.
|
|
243
|
+
|
|
139
244
|
### Available Tools
|
|
140
245
|
|
|
141
246
|
vibecore comes with powerful built-in tools:
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
vibecore/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
vibecore/cli.py,sha256=JspCzt8jlPur35yil7i6Whiw_u33DYeRldx4G-36ueo,4166
|
|
3
3
|
vibecore/context.py,sha256=JUVkZpmKGUSlcchrHpxu-oSO8D21GDgHT1BXDzZDTeQ,844
|
|
4
|
-
vibecore/
|
|
4
|
+
vibecore/flow.py,sha256=ZaKzMsz4YBvgelVzJOIHnTJzMWTmvkfvudwW_hllq6U,3384
|
|
5
|
+
vibecore/main.py,sha256=MIn7Mpg_xO_20c6Mju8PgY-MmVCTER9cepHd5YFbtYs,20175
|
|
5
6
|
vibecore/main.tcss,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
7
|
vibecore/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
vibecore/settings.py,sha256=
|
|
8
|
-
vibecore/agents/default.py,sha256=
|
|
8
|
+
vibecore/settings.py,sha256=u6kWIMdd5IoLE_SJYqPChR1du8bJ3ZhuCHQQvsUy_cY,6041
|
|
9
|
+
vibecore/agents/default.py,sha256=wxeP3Hsq9MVBChyMF_sNkVHCtFFXgy5ghDIxy9eH_fQ,2287
|
|
9
10
|
vibecore/agents/prompts.py,sha256=0oO9QzcytIbzgZcKJQjWD9fSRNQqBqqK5ku0fcYZZrA,324
|
|
10
|
-
vibecore/agents/
|
|
11
|
+
vibecore/agents/task.py,sha256=cdhZDzKDA8eHHNYPhogFIKBms3oVAMvYeiBB2GqYNuE,1898
|
|
11
12
|
vibecore/handlers/__init__.py,sha256=pFogA2n3GeIi4lmlUEU5jFprNNOaA6AWRR8Wc9X-P4Y,148
|
|
12
13
|
vibecore/handlers/stream_handler.py,sha256=N3fs7jO9QcjSClSyglrwJQ3ky08F70xS8j5m-aYNkyM,10336
|
|
13
14
|
vibecore/mcp/__init__.py,sha256=sl2_8pWjPx4TotO0ZojunVA6Jn6yOhbTQNbQG9-C-Jc,199
|
|
@@ -37,12 +38,23 @@ vibecore/tools/shell/__init__.py,sha256=Ias6qmBMDK29q528VtUGtCQeYD4RU_Yx73SIAJrB
|
|
|
37
38
|
vibecore/tools/shell/executor.py,sha256=yXUkbPqLc3anlsLUB_g4yEu1A_QpzfzwsoMAqx-gplA,6933
|
|
38
39
|
vibecore/tools/shell/tools.py,sha256=hpftFrv4JWn7mbYLJwpCPLROTFyj-RiAOg1hyecV0bE,6829
|
|
39
40
|
vibecore/tools/task/__init__.py,sha256=Fyw33zGiBArMnPuRMm7qwSYE6ZRPCZVbHK6eIUJDiJY,112
|
|
40
|
-
vibecore/tools/task/executor.py,sha256=
|
|
41
|
+
vibecore/tools/task/executor.py,sha256=gRIdq0f2gjDKxnWH-b5Rbmk1H2garIs56EDYFVKfUiw,1606
|
|
41
42
|
vibecore/tools/task/tools.py,sha256=m6MBOQC3Pz07TZgd3lVAHPGQu9M-Ted-YOxQvIPrGvo,2257
|
|
42
43
|
vibecore/tools/todo/__init__.py,sha256=67o76OyzqYKBH461R9H2-rkNx7ZK6tRSydca3GjqKh8,29
|
|
43
44
|
vibecore/tools/todo/manager.py,sha256=COcVMX8sm--dtqXo0L7914krJMEcK6P2Va4OJiVroBg,871
|
|
44
45
|
vibecore/tools/todo/models.py,sha256=gSheOpIP8NJf44X1JNwvbJQNsyrkfzP3LxrP_9rXzYw,634
|
|
45
46
|
vibecore/tools/todo/tools.py,sha256=kbWXOMu5_xiMdWelxtbh6qS3yBomkveyOFlBcaJKcSY,5121
|
|
47
|
+
vibecore/tools/webfetch/__init__.py,sha256=fKfht3oiz-wMNgtukQjYIUcUC4y7g3GLKK7QXHl0Mcg,224
|
|
48
|
+
vibecore/tools/webfetch/executor.py,sha256=DFjnHgAvDPuwP5h4fgXM2JH270TgF4Vux7ndmZLs9BI,4912
|
|
49
|
+
vibecore/tools/webfetch/models.py,sha256=YvGR4i8Mi7gygCJe7-VPyrvbgacBUJ1PLHyCmOQPmuU,694
|
|
50
|
+
vibecore/tools/webfetch/tools.py,sha256=PWt8hBPD02ua2d9ZnDVfqtNVzachtIPB9QPStbkYY2Y,1494
|
|
51
|
+
vibecore/tools/websearch/__init__.py,sha256=xl3aPD-pOt0Ya4L8khMbOfqpcCpkWTy2-KVk2hUxnOU,97
|
|
52
|
+
vibecore/tools/websearch/base.py,sha256=El9Mx6MFWM3CKGG8MPbPIKgRjdbNZtylFALsPCUTPFs,596
|
|
53
|
+
vibecore/tools/websearch/executor.py,sha256=CLwFkPSDzllH7J1hNdjsp5L0SDLqoINlOSl-zoQKs2A,1114
|
|
54
|
+
vibecore/tools/websearch/models.py,sha256=5cwDw9dWLZ6krP_khx1euwsHjSYLIE4_hNefkjzrkWE,749
|
|
55
|
+
vibecore/tools/websearch/tools.py,sha256=leDf9nmvl8577TMrj7MTodYFx1vyXiIPDral0yzEYm8,1734
|
|
56
|
+
vibecore/tools/websearch/ddgs/__init__.py,sha256=XwZ7As5mVqxXz69In96L3TDChPhpc8GnZR72LgdBvX4,113
|
|
57
|
+
vibecore/tools/websearch/ddgs/backend.py,sha256=HHcckGFoPaFGYSl4k5UH6PURgF1sk8zYWSWVEYeAEtI,1959
|
|
46
58
|
vibecore/utils/__init__.py,sha256=KIS8TkfaDZ1AclSstkYcG8DvJPNsJNOE4EL4zHJE2k4,112
|
|
47
59
|
vibecore/utils/text.py,sha256=RLVFrVsD5L7xi68JTgSa0PeN9S32faqIiaF79dNCyTM,978
|
|
48
60
|
vibecore/widgets/core.py,sha256=ZIdHBvfIaAXaBhA2X3EUkDlL2pN4l5j5Kc_E-VCsM3g,12068
|
|
@@ -53,11 +65,11 @@ vibecore/widgets/info.py,sha256=hXtsRUOE13oHbIm9FNe1GCUX_FCht28pgT9SQWeJ69I,1567
|
|
|
53
65
|
vibecore/widgets/info.tcss,sha256=v30IqNt1two-ezIcm18ZEInKRKcRkAW-h-UH2r8QzSo,201
|
|
54
66
|
vibecore/widgets/messages.py,sha256=az4fJtdk3ItSoFZBG_adRDUHdTLttIV8F23E8LOb-mg,8156
|
|
55
67
|
vibecore/widgets/messages.tcss,sha256=WtBbjf5LgFkUhzhVlxJB7NMbagWladJawDizvDm7hBE,1271
|
|
56
|
-
vibecore/widgets/tool_message_factory.py,sha256=
|
|
57
|
-
vibecore/widgets/tool_messages.py,sha256=
|
|
58
|
-
vibecore/widgets/tool_messages.tcss,sha256=
|
|
59
|
-
vibecore-0.
|
|
60
|
-
vibecore-0.
|
|
61
|
-
vibecore-0.
|
|
62
|
-
vibecore-0.
|
|
63
|
-
vibecore-0.
|
|
68
|
+
vibecore/widgets/tool_message_factory.py,sha256=FMwavKMRNT8ik9RgcL37WuM19Ln-c5wuFmS0A2CkikM,5377
|
|
69
|
+
vibecore/widgets/tool_messages.py,sha256=PKmPigeOlccowo0uLpgIPzsmmE-zkichzuXIS6hWsbQ,24501
|
|
70
|
+
vibecore/widgets/tool_messages.tcss,sha256=mcFY58FE1AcfEvEiA_Yb7sMpIniTIC_IjDvv8M7vWOA,6924
|
|
71
|
+
vibecore-0.3.0b1.dist-info/METADATA,sha256=PYHen54ZzyRC9-8d6NA9GNxiV24aexWU8kPaQiiuJCk,18066
|
|
72
|
+
vibecore-0.3.0b1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
73
|
+
vibecore-0.3.0b1.dist-info/entry_points.txt,sha256=YldTakc3dNelboaWXtzCcnM5MXvU2_6pVOjc2xPjDTY,47
|
|
74
|
+
vibecore-0.3.0b1.dist-info/licenses/LICENSE,sha256=KXxxifvrcreHrZ4aOYgP-vA8DRHHueW389KKOeEbtjc,1069
|
|
75
|
+
vibecore-0.3.0b1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|