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.

@@ -1,8 +1,7 @@
1
1
  from typing import TYPE_CHECKING
2
2
 
3
- from agents import Agent, ModelSettings
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=ModelSettings(
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, ModelSettings
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=ModelSettings(
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
- yield Welcome()
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
- # If agent is running, queue the message
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="auto")
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
- await main_scroll.mount(Welcome())
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 pydantic import BaseModel, Field
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,
@@ -7,7 +7,7 @@ from agents import (
7
7
  )
8
8
  from textual import log
9
9
 
10
- from vibecore.agents.task_agent import create_task_agent
10
+ from vibecore.agents.task import create_task_agent
11
11
  from vibecore.context import VibecoreContext
12
12
  from vibecore.settings import settings
13
13
 
@@ -0,0 +1,7 @@
1
+ """Webfetch tool for fetching and converting web content to Markdown."""
2
+
3
+ from .executor import fetch_url
4
+ from .models import WebFetchParams
5
+ from .tools import webfetch
6
+
7
+ __all__ = ["WebFetchParams", "fetch_url", "webfetch"]
@@ -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,5 @@
1
+ """Websearch tool for Vibecore agents."""
2
+
3
+ from .tools import websearch
4
+
5
+ __all__ = ["websearch"]
@@ -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,5 @@
1
+ """DuckDuckGo search backend for websearch tool."""
2
+
3
+ from .backend import DDGSBackend
4
+
5
+ __all__ = ["DDGSBackend"]
@@ -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.2.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/main.py,sha256=dml8vUoEQrqBZIaH04i15xuBRo_LDZbPwQt8dnVZHZU,19274
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=_X2HLh6LbBiQOUKst5Z3sdzcaAxH3m7xNHXI-JXPrHs,5154
8
- vibecore/agents/default.py,sha256=HnJ33t9rxwco7jfbbZ9dzgyRcpqvGoYH-3KPdTRKX-c,2539
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/task_agent.py,sha256=WcqmSIml1TVZZSWg1xqODAnzpfvSkC-caZuV9ejjUi0,2073
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=gPOAbPNf5Rwxke8pC9-dDfMp07gNptNcL6HsMC7en0U,1612
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=8sSJOo_yXsKY46lDGExdbyB179kFUIEGyfAHWzc6ksY,4749
57
- vibecore/widgets/tool_messages.py,sha256=uF_QoqtdxPEN0VnoFlvPLdRJKGvIpklHe_NwhqVFStw,19167
58
- vibecore/widgets/tool_messages.tcss,sha256=K7aCmx_zCbYYwCs6IFfPfvK9pygRVf1PDd8gX0vVtc0,5999
59
- vibecore-0.2.0.dist-info/METADATA,sha256=uJl8-HZOKevE6JRuUy3RiE34LK8H02FEeciBzyFOMwU,14355
60
- vibecore-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
61
- vibecore-0.2.0.dist-info/entry_points.txt,sha256=YldTakc3dNelboaWXtzCcnM5MXvU2_6pVOjc2xPjDTY,47
62
- vibecore-0.2.0.dist-info/licenses/LICENSE,sha256=KXxxifvrcreHrZ4aOYgP-vA8DRHHueW389KKOeEbtjc,1069
63
- vibecore-0.2.0.dist-info/RECORD,,
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,,