relay-code 0.1.0__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.
tools/network/fetch.py ADDED
@@ -0,0 +1,72 @@
1
+ from urllib.parse import urlparse
2
+
3
+ import httpx
4
+
5
+ from tools.base import Tool, ToolInvocation, ToolKind, ToolResult
6
+ from pydantic import BaseModel, Field
7
+
8
+ class WebFetchParams(BaseModel):
9
+
10
+ url: str = Field(
11
+ ...,
12
+ description='URL to fetch (must be http:// or https://)'
13
+ )
14
+
15
+ timeout: int = Field(
16
+ 30,
17
+ ge=5,
18
+ le=120,
19
+ description='Request timeout in seconds (default: 120)'
20
+ )
21
+
22
+ class WebFetchTool(Tool):
23
+ name = 'fetch'
24
+ description = 'Fetch content from the URL. Returns the response body as text'
25
+ kind = ToolKind.NETWORK
26
+ schema = WebFetchParams
27
+
28
+ async def execute(self, invocation: ToolInvocation) -> ToolResult:
29
+ params = WebFetchParams(**invocation.params)
30
+
31
+ parsed = urlparse(params.url)
32
+ if not parsed.scheme or parsed.scheme not in ('http', 'https'):
33
+ return ToolResult.error_result(
34
+ f'Url must be http:// or https://'
35
+ )
36
+
37
+ try:
38
+ async with httpx.AsyncClient(
39
+ timeout=httpx.Timeout(params.timeout),
40
+ follow_redirects=True,
41
+ ) as client:
42
+ response = await client.get(params.url)
43
+ response.raise_for_status()
44
+ text = response.text
45
+ except httpx.HTTPStatusError as e:
46
+ return ToolResult.error_result(
47
+ f'HTTP {e.response.status_code}: {e.response.reason_phrase}'
48
+ )
49
+ except httpx.TimeoutException:
50
+ return ToolResult.error_result(
51
+ f'Request failed: {e}'
52
+ )
53
+ except Exception as e:
54
+ return ToolResult.error_result(
55
+ f'Search failed: {e}'
56
+ )
57
+
58
+
59
+ if len(text) > 100 * 1024:
60
+ text = text[:100 * 1024] + '\n... [content truncated]'
61
+
62
+ return ToolResult.success_result(
63
+ text,
64
+ metadata = {
65
+ 'status_code': response.status_code,
66
+ 'content_length': len(response.content),
67
+ }
68
+ )
69
+
70
+
71
+
72
+
@@ -0,0 +1,72 @@
1
+ from ddgs import DDGS
2
+
3
+ from tools.base import Tool, ToolInvocation, ToolKind, ToolResult
4
+ from pydantic import BaseModel, Field
5
+
6
+ class WebSearchParams(BaseModel):
7
+
8
+ query: str = Field(
9
+ ...,
10
+ description='Search query'
11
+ )
12
+
13
+ max_results: int = Field(
14
+ 10,
15
+ ge=1,
16
+ le=50,
17
+ description='Maximum results to return (default: 10)'
18
+ )
19
+
20
+ class WebSearchTool(Tool):
21
+ name = 'search'
22
+ description = 'Search the web for information. Returns the search results that contains titles, URL(s) and snippets.'
23
+ kind = ToolKind.NETWORK
24
+ schema = WebSearchParams
25
+
26
+ async def execute(self, invocation: ToolInvocation) -> ToolResult:
27
+ params = WebSearchParams(**invocation.params)
28
+
29
+ try:
30
+ results = DDGS().text(
31
+ params.query,
32
+ region='us-en',
33
+ safesearch='off',
34
+ timelimit='y',
35
+ page=1,
36
+ backend='auto'
37
+ )
38
+ except Exception as e:
39
+ return ToolResult.error_result(
40
+ f'Search failed: {e}'
41
+ )
42
+
43
+ if not results:
44
+ return ToolResult.success_result(
45
+ f'No results found for: {params.query}',
46
+ metadata = {
47
+ 'results': 0,
48
+ }
49
+ )
50
+
51
+ output_lines = [
52
+ f'Search results for: {params.query}'
53
+ ]
54
+
55
+ for i, result in enumerate(results, start=1):
56
+ output_lines.append(f"{i}. Title: {result['title']}")
57
+ output_lines.append(f" URL: {result['href']}")
58
+ if result.get('body'):
59
+ output_lines.append(f" Snippet: {result['body']}")
60
+
61
+ output_lines.append("")
62
+
63
+ return ToolResult.success_result(
64
+ '\n'.join(output_lines),
65
+ metadata = {
66
+ 'results': len(results),
67
+ }
68
+ )
69
+
70
+
71
+
72
+
tools/registry.py ADDED
@@ -0,0 +1,96 @@
1
+ from config.config import Config
2
+ from tools.base import Tool
3
+ from typing import Any
4
+ from pathlib import Path
5
+ from tools.base import ToolResult, ToolInvocation
6
+ from tools.core import ReadFileTool, get_all_core_tools
7
+ import logging
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class ToolRegistry:
13
+
14
+ def __init__(self):
15
+ self._tools: dict[str, Tool] = {}
16
+
17
+ def register(self, tool: Tool) -> None:
18
+ if tool.name in self._tools:
19
+ logger.warning(f"Overwriting existing tool: {tool.name}")
20
+
21
+ self._tools[tool.name] = tool
22
+ logger.debug(f"Registered tool: {tool.name}")
23
+
24
+ def unregister(self, name: str) -> bool:
25
+ if name in self._tools:
26
+ del self._tools[name]
27
+ return True
28
+
29
+ return False
30
+
31
+ def get(self, name: str) -> Tool | None:
32
+ if name in self._tools:
33
+ return self._tools[name]
34
+
35
+ return None
36
+
37
+ def get_tools(self) -> list[Tool]:
38
+ tools: list[Tool] = []
39
+
40
+ for tool in self._tools.values():
41
+ tools.append(tool)
42
+
43
+ return tools
44
+
45
+ def get_schemas(self) -> list[dict[str, Any]]:
46
+ return [tool.to_openai_schema() for tool in self.get_tools()]
47
+
48
+ async def invoke(
49
+ self,
50
+ name: str,
51
+ params: dict[str, Any],
52
+ cwd: Path | None
53
+ ) -> ToolResult:
54
+ tool = self.get(name)
55
+ if tool is None:
56
+ return ToolResult.error_result(
57
+ f"Unknown Tool: {name}",
58
+ metadata={
59
+ 'tool_name': name
60
+ },
61
+ )
62
+
63
+ validiation_errors = tool.validate_params(params)
64
+ if validiation_errors:
65
+ return ToolResult.error_result(
66
+ f"Invalid paramaters: {'; '.join(validiation_errors)}",
67
+ metadata={
68
+ 'tool_name': name,
69
+ 'validation_errors': validiation_errors
70
+ }
71
+ )
72
+ invocation = ToolInvocation(
73
+ params=params,
74
+ cwd=cwd,
75
+ )
76
+ try:
77
+ result = await tool.execute(invocation)
78
+ except Exception as e:
79
+ logger.exception(f"Tool {name} raised unexpected error")
80
+ result = ToolResult.error_result(
81
+ f"Internal error: {(str(e))}",
82
+ metadata = {
83
+ "tool_name": name
84
+ }
85
+ )
86
+
87
+ return result
88
+
89
+ def create_default_registery(config: Config) -> ToolRegistry:
90
+
91
+ registery = ToolRegistry()
92
+
93
+ for tool_class in get_all_core_tools():
94
+ registery.register(tool_class(config))
95
+
96
+ return registery
ui/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+
2
+ from ui.app import RelayApp
3
+ from ui.renderer import TUI, get_console
4
+
5
+ __all__ = ['RelayApp', 'TUI', 'get_console']