chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:44:08 +08:00
commit 983960e2dd
1244 changed files with 281996 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
from .src.python.grok_tool import GrokTool
__all__ = ["GrokTool"]
@@ -0,0 +1,4 @@
{
"GROK_API_KEY": null,
"GROK_MODEL": "grok-4-fast-reasoning-latest"
}
@@ -0,0 +1,482 @@
import { Tool } from '@sdk/base-tool'
import { ToolkitConfig } from '@sdk/toolkit-config'
/**
* xAI Grok Tool with Server-Side Agentic Search
* Uses the Responses API (/v1/responses) for tool support
* Reference: https://docs.x.ai/docs/guides/tools/search-tools
*/
// Hardcoded default settings for Grok tool
const GROK_API_KEY: string | null = null
const GROK_MODEL = 'grok-4-fast-reasoning-latest'
const DEFAULT_SETTINGS: Record<string, unknown> = {
GROK_API_KEY,
GROK_MODEL
}
const REQUIRED_SETTINGS = ['GROK_API_KEY']
interface GrokMessage {
role: 'system' | 'user' | 'assistant'
content: string
}
// xAI Responses API tool format
interface WebSearchTool {
type: 'web_search'
allowed_domains?: string[]
excluded_domains?: string[]
enable_image_understanding?: boolean
}
interface XSearchTool {
type: 'x_search'
allowed_x_handles?: string[]
excluded_x_handles?: string[]
from_date?: string
to_date?: string
enable_image_understanding?: boolean
enable_video_understanding?: boolean
}
interface GrokChatOptions {
input: GrokMessage[] // Responses API uses "input" not "messages"
model?: string
temperature?: number
max_output_tokens?: number
stream?: boolean
tools?: Array<WebSearchTool | XSearchTool>
}
interface Annotation {
type: string
url?: string
start_index?: number
end_index?: number
title?: string
}
interface ContentItem {
type: string
text?: string
logprobs?: unknown[]
annotations?: Annotation[]
}
interface MessageOutput {
type: 'message'
id: string
role: string
status: string
content: ContentItem[]
}
interface ToolCallOutput {
id: string
type: 'web_search_call' | 'x_search_call'
status: string
action: {
type: string
query?: string
url?: string
sources?: unknown[]
}
}
type OutputItem = MessageOutput | ToolCallOutput
interface GrokModelsResponse {
object: string
data: Array<{
id: string
object: string
created: number
owned_by: string
}>
}
interface GrokResponsesApiResponse {
id: string
output: OutputItem[]
usage: {
input_tokens: number
output_tokens: number
total_tokens: number
reasoning_tokens?: number
}
}
interface GrokResponse {
success: boolean
data?: GrokResponsesApiResponse
error?: string
// Convenience helpers
content?: string
citations?: string[]
annotations?: Annotation[]
[key: string]: unknown
}
export default class GrokTool extends Tool {
private static readonly TOOLKIT = 'search_web'
private readonly config: ReturnType<typeof ToolkitConfig.load>
private apiKey: string | null
private model: string
private baseUrl: string = 'https://api.x.ai'
constructor() {
super()
this.config = ToolkitConfig.load(GrokTool.TOOLKIT, this.toolName)
const toolSettings = ToolkitConfig.loadToolSettings(
GrokTool.TOOLKIT,
this.toolName,
DEFAULT_SETTINGS
)
this.settings = toolSettings
this.requiredSettings = REQUIRED_SETTINGS
this.checkRequiredSettings(this.toolName)
// Priority: toolkit settings > hardcoded default
this.apiKey = (this.settings['GROK_API_KEY'] as string) || GROK_API_KEY
this.model = (this.settings['GROK_MODEL'] as string) || GROK_MODEL
}
get toolName(): string {
return 'grok'
}
get toolkit(): string {
return GrokTool.TOOLKIT
}
get description(): string {
return this.config['description']
}
/**
* Set the Grok API key
*/
setApiKey(apiKey: string): void {
this.apiKey = apiKey
}
/**
* List available models
* Reference: https://docs.x.ai/docs/api-reference
*/
async listModels(): Promise<{
success: boolean
data?: GrokModelsResponse
error?: string
}> {
if (!this.apiKey) {
return {
success: false,
error: 'Grok API key is not set. Please call setApiKey() first.'
}
}
try {
const response = await fetch(`${this.baseUrl}/v1/models`, {
method: 'GET',
headers: {
Authorization: `Bearer ${this.apiKey}`
}
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(
`Grok API error: ${response.status} - ${JSON.stringify(errorData)}`
)
}
const data = await response.json() as GrokModelsResponse
return {
success: true,
data
}
} catch (error: unknown) {
return {
success: false,
error: `Failed to list models: ${(error as Error).message}`
}
}
}
/**
* Perform a chat completion with Grok using server-side agentic search tools
* Uses the /v1/responses endpoint (Responses API) for tool support
* Reference: https://docs.x.ai/docs/guides/tools/search-tools
*/
async chatCompletion(
options: GrokChatOptions
): Promise<GrokResponse> {
if (!this.apiKey) {
return {
success: false,
error: 'Grok API key is not set. Please call setApiKey() first.'
}
}
const {
input,
model,
temperature = 0.7,
max_output_tokens = 4096,
stream = false,
tools
} = options
// Use default model if none provided
const finalModel = model || this.model
try {
const requestBody: Record<string, unknown> = {
model: finalModel,
input,
temperature,
max_output_tokens,
stream
}
// Add server-side search tools if provided
if (tools && tools.length > 0) {
requestBody['tools'] = tools
}
// Use /v1/responses endpoint for tools support (not /v1/chat/completions)
const response = await fetch(`${this.baseUrl}/v1/responses`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`
},
body: JSON.stringify(requestBody)
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(
`Grok API error: ${response.status} - ${JSON.stringify(errorData)}`
)
}
const data = await response.json() as GrokResponsesApiResponse
// Extract the final text output from the output array
let content = ''
let annotations: Annotation[] = []
let citations: string[] = []
if (data.output && Array.isArray(data.output)) {
// Find the message item (type: "message")
for (let i = data.output.length - 1; i >= 0; i--) {
const item = data.output[i]
if (!item || item.type !== 'message' || !Array.isArray(item.content)) {
continue
}
// Find output_text in the content array
for (const contentItem of item.content) {
if (contentItem.type === 'output_text' && contentItem.text) {
content = contentItem.text
annotations = contentItem.annotations || []
// Extract URLs from annotations for citations
citations = annotations
.filter((a) => a.url)
.map((a) => a.url as string)
break
}
}
break
}
}
return {
success: true,
data,
content,
citations,
annotations
}
} catch (error: unknown) {
return {
success: false,
error: `Failed to complete chat: ${(error as Error).message}`
}
}
}
/**
* Search the web using Grok's server-side agentic web search tool
* The model will autonomously call the web_search tool during reasoning
* Reference: https://docs.x.ai/docs/guides/tools/search-tools
*/
async searchWeb(
query: string,
options?: {
allowed_domains?: string[] // Max 5
excluded_domains?: string[] // Max 5
enable_image_understanding?: boolean
}
): Promise<GrokResponse> {
const webSearchTool: WebSearchTool = {
type: 'web_search',
...options
}
return this.chatCompletion({
input: [
{
role: 'user',
content: query
}
],
model: this.model,
temperature: 0.5,
tools: [webSearchTool]
})
}
/**
* Search X/Twitter using Grok's server-side agentic X search tool
* The model will autonomously call the x_search tool during reasoning
* Reference: https://docs.x.ai/docs/guides/tools/search-tools
*/
async searchX(
query: string,
options?: {
allowed_x_handles?: string[] // Max 10
excluded_x_handles?: string[] // Max 10
from_date?: string // ISO8601: "YYYY-MM-DD"
to_date?: string // ISO8601: "YYYY-MM-DD"
enable_image_understanding?: boolean
enable_video_understanding?: boolean
}
): Promise<GrokResponse> {
const xSearchTool: XSearchTool = {
type: 'x_search',
...options
}
return this.chatCompletion({
input: [
{
role: 'user',
content: query
}
],
model: this.model,
temperature: 0.5,
tools: [xSearchTool]
})
}
/**
* Search both web and X using both server-side search tools
* The model will autonomously call both tools during reasoning
* Reference: https://docs.x.ai/docs/guides/tools/search-tools
*/
async search(
query: string,
options?: {
web_options?: {
allowed_domains?: string[]
excluded_domains?: string[]
enable_image_understanding?: boolean
}
x_options?: {
allowed_x_handles?: string[]
excluded_x_handles?: string[]
from_date?: string
to_date?: string
enable_image_understanding?: boolean
enable_video_understanding?: boolean
}
}
): Promise<GrokResponse> {
const tools: Array<WebSearchTool | XSearchTool> = []
// Add web search tool
const webSearchTool: WebSearchTool = {
type: 'web_search',
...options?.web_options
}
tools.push(webSearchTool)
// Add X search tool
const xSearchTool: XSearchTool = {
type: 'x_search',
...options?.x_options
}
tools.push(xSearchTool)
return this.chatCompletion({
input: [
{
role: 'user',
content: query
}
],
model: this.model,
temperature: 0.5,
tools
})
}
/**
* Perform deep research on a topic using web search
* The model will iteratively call search tools to gather comprehensive information
* Reference: https://docs.x.ai/docs/guides/tools/search-tools
*/
async deepResearch(
topic: string,
focusAreas?: string[],
options?: {
allowed_domains?: string[]
}
): Promise<GrokResponse> {
const focusText =
focusAreas && focusAreas.length > 0
? `Focus on these specific areas: ${focusAreas.join(', ')}.`
: ''
const prompt = `Conduct comprehensive research on: ${topic}
${focusText}
Provide a detailed analysis including:
1. Overview and key findings
2. Recent developments and trends
3. Important statistics and data
4. Expert opinions and credible sources
5. Relevant links and references
Use web search to gather current and accurate information.`
return this.searchWeb(prompt, {
...(options?.allowed_domains
? { allowed_domains: options.allowed_domains }
: {}),
enable_image_understanding: true
})
}
/**
* Get what's trending on X/Twitter
* Reference: https://docs.x.ai/docs/guides/tools/search-tools
*/
async getTrendingOnX(
location?: string
): Promise<GrokResponse> {
const locationText = location ? ` in ${location}` : ' globally'
const prompt = `What are the top trending topics and discussions on X/Twitter${locationText} right now? Provide details about each trend including what it's about and key posts.`
return this.searchX(prompt, {
enable_image_understanding: true
})
}
}
@@ -0,0 +1 @@
export { default } from './grok-tool'
@@ -0,0 +1,366 @@
"""
xAI Grok Tool with Server-Side Agentic Search
Uses the Responses API (/v1/responses) for tool support
Reference: https://docs.x.ai/docs/guides/tools/search-tools
"""
import json
from typing import Dict, Any, List, Optional
import requests
from bridges.python.src.sdk.base_tool import BaseTool
from bridges.python.src.sdk.toolkit_config import ToolkitConfig
# Hardcoded default settings for Grok tool
GROK_API_KEY = None
GROK_MODEL = "grok-4-fast-reasoning-latest"
DEFAULT_SETTINGS = {
"GROK_API_KEY": GROK_API_KEY,
"GROK_MODEL": GROK_MODEL,
}
REQUIRED_SETTINGS = ["GROK_API_KEY"]
class GrokTool(BaseTool):
"""
Grok Tool for AI-powered web and X/Twitter search using xAI's server-side tools.
Features:
- Web search with domain filtering and image understanding
- X/Twitter search with handle filtering, date ranges, and video understanding
- Server-side agentic tool calling
- Citation tracking (citations and inline_citations)
- Deep research capabilities
"""
TOOLKIT = "search_web"
def __init__(self):
super().__init__()
self.config = ToolkitConfig.load(self.TOOLKIT, self.tool_name)
tool_settings = ToolkitConfig.load_tool_settings(
self.TOOLKIT, self.tool_name, DEFAULT_SETTINGS
)
self.settings = tool_settings
self.required_settings = REQUIRED_SETTINGS
self._check_required_settings(self.tool_name)
# Priority: toolkit settings > hardcoded default
self.api_key = self.settings.get("GROK_API_KEY", GROK_API_KEY)
self.model = self.settings.get("GROK_MODEL", GROK_MODEL)
self.base_url = "https://api.x.ai"
@property
def tool_name(self) -> str:
return "grok"
@property
def toolkit(self) -> str:
return self.TOOLKIT
@property
def description(self) -> str:
return self.config.get("description", "")
def set_api_key(self, api_key: str) -> None:
"""Set the Grok API key"""
self.api_key = api_key
def list_models(self) -> Dict[str, Any]:
"""
List available models
Reference: https://docs.x.ai/docs/api-reference
"""
if not self.api_key:
return {
"success": False,
"error": "Grok API key is not set. Please call set_api_key() first.",
}
try:
response = requests.get(
f"{self.base_url}/v1/models",
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=30,
)
if not response.ok:
error_data = response.json() if response.text else {}
raise Exception(
f"Grok API error: {response.status_code} - {json.dumps(error_data)}"
)
data = response.json()
return {"success": True, "data": data}
except Exception as error:
return {"success": False, "error": f"Failed to list models: {str(error)}"}
def chat_completion(
self,
input: List[Dict[str, str]],
model: Optional[str] = None,
temperature: float = 0.7,
max_output_tokens: int = 4096,
stream: bool = False,
tools: Optional[List[Dict[str, Any]]] = None,
) -> Dict[str, Any]:
"""
Perform a chat completion with Grok using server-side agentic search tools
Uses the /v1/responses endpoint (Responses API) for tool support
Reference: https://docs.x.ai/docs/guides/tools/search-tools
"""
if not self.api_key:
return {
"success": False,
"error": "Grok API key is not set. Please call set_api_key() first.",
}
# Use default model if none provided
model = model or self.model
try:
request_body: Dict[str, Any] = {
"model": model,
"input": input,
"temperature": temperature,
"max_output_tokens": max_output_tokens,
"stream": stream,
}
# Add server-side search tools if provided
if tools and len(tools) > 0:
request_body["tools"] = tools
# Use /v1/responses endpoint for tools support (not /v1/chat/completions)
response = requests.post(
f"{self.base_url}/v1/responses",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}",
},
json=request_body,
timeout=120,
)
if not response.ok:
error_data = response.json() if response.text else {}
raise Exception(
f"Grok API error: {response.status_code} - {json.dumps(error_data)}"
)
data = response.json()
# Extract the final text output from the output array
content = ""
annotations = []
citations = []
if "output" in data and isinstance(data["output"], list):
# Find the message item (type: "message")
for item in reversed(data["output"]):
if item.get("type") == "message" and "content" in item:
content_array = item.get("content", [])
if isinstance(content_array, list):
# Find output_text in the content array
for content_item in content_array:
if content_item.get(
"type"
) == "output_text" and content_item.get("text"):
content = content_item["text"]
annotations = content_item.get("annotations", [])
# Extract URLs from annotations for citations
citations = [
a["url"] for a in annotations if a.get("url")
]
break
break
return {
"success": True,
"data": data,
"content": content,
"citations": citations,
"annotations": annotations,
}
except Exception as error:
return {"success": False, "error": f"Failed to complete chat: {str(error)}"}
def search_web(
self,
query: str,
allowed_domains: Optional[List[str]] = None,
excluded_domains: Optional[List[str]] = None,
enable_image_understanding: bool = False,
) -> Dict[str, Any]:
"""
Search the web using Grok's server-side agentic web search tool.
The model will autonomously call the web_search tool during reasoning.
Reference: https://docs.x.ai/docs/guides/tools/search-tools
Args:
query: The search query
allowed_domains: Max 5 domains to search within
excluded_domains: Max 5 domains to exclude
enable_image_understanding: Enable image analysis
"""
web_search_tool: Dict[str, Any] = {"type": "web_search"}
if allowed_domains:
web_search_tool["allowed_domains"] = allowed_domains
if excluded_domains:
web_search_tool["excluded_domains"] = excluded_domains
if enable_image_understanding:
web_search_tool["enable_image_understanding"] = enable_image_understanding
return self.chat_completion(
input=[{"role": "user", "content": query}],
model=self.model,
temperature=0.5,
tools=[web_search_tool],
)
def search_x(
self,
query: str,
allowed_x_handles: Optional[List[str]] = None,
excluded_x_handles: Optional[List[str]] = None,
from_date: Optional[str] = None,
to_date: Optional[str] = None,
enable_image_understanding: bool = False,
enable_video_understanding: bool = False,
) -> Dict[str, Any]:
"""
Search X/Twitter using Grok's server-side agentic X search tool.
The model will autonomously call the x_search tool during reasoning.
Reference: https://docs.x.ai/docs/guides/tools/search-tools
Args:
query: The search query
allowed_x_handles: Max 10 handles to search within
excluded_x_handles: Max 10 handles to exclude
from_date: ISO8601 date "YYYY-MM-DD"
to_date: ISO8601 date "YYYY-MM-DD"
enable_image_understanding: Enable image analysis
enable_video_understanding: Enable video analysis
"""
x_search_tool: Dict[str, Any] = {"type": "x_search"}
if allowed_x_handles:
x_search_tool["allowed_x_handles"] = allowed_x_handles
if excluded_x_handles:
x_search_tool["excluded_x_handles"] = excluded_x_handles
if from_date:
x_search_tool["from_date"] = from_date
if to_date:
x_search_tool["to_date"] = to_date
if enable_image_understanding:
x_search_tool["enable_image_understanding"] = enable_image_understanding
if enable_video_understanding:
x_search_tool["enable_video_understanding"] = enable_video_understanding
return self.chat_completion(
input=[{"role": "user", "content": query}],
model=self.model,
temperature=0.5,
tools=[x_search_tool],
)
def search(
self,
query: str,
web_options: Optional[Dict[str, Any]] = None,
x_options: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Search both web and X using both server-side search tools.
The model will autonomously call both tools during reasoning.
Reference: https://docs.x.ai/docs/guides/tools/search-tools
Args:
query: The search query
web_options: Options for web search (allowed_domains, excluded_domains, etc.)
x_options: Options for X search (allowed_x_handles, from_date, etc.)
"""
tools: List[Dict[str, Any]] = []
# Add web search tool
web_search_tool: Dict[str, Any] = {"type": "web_search"}
if web_options:
web_search_tool.update(web_options)
tools.append(web_search_tool)
# Add X search tool
x_search_tool: Dict[str, Any] = {"type": "x_search"}
if x_options:
x_search_tool.update(x_options)
tools.append(x_search_tool)
return self.chat_completion(
input=[{"role": "user", "content": query}],
model=self.model,
temperature=0.5,
tools=tools,
)
def deep_research(
self,
topic: str,
focus_areas: Optional[List[str]] = None,
allowed_domains: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""
Perform deep research on a topic using web search.
The model will iteratively call search tools to gather comprehensive information.
Reference: https://docs.x.ai/docs/guides/tools/search-tools
Args:
topic: The research topic
focus_areas: Specific areas to focus on
allowed_domains: Domains to search within
"""
focus_text = (
f"Focus on these specific areas: {', '.join(focus_areas)}."
if focus_areas
else ""
)
prompt = f"""Conduct comprehensive research on: {topic}
{focus_text}
Provide a detailed analysis including:
1. Overview and key findings
2. Recent developments and trends
3. Important statistics and data
4. Expert opinions and credible sources
5. Relevant links and references
Use web search to gather current and accurate information."""
return self.search_web(
query=prompt,
allowed_domains=allowed_domains,
enable_image_understanding=True,
)
def get_trending_on_x(
self,
location: Optional[str] = None,
) -> Dict[str, Any]:
"""
Get what's trending on X/Twitter.
Reference: https://docs.x.ai/docs/guides/tools/search-tools
Args:
location: Geographic location (e.g., "United States", "London")
"""
location_text = f" in {location}" if location else " globally"
prompt = f"What are the top trending topics and discussions on X/Twitter{location_text} right now? Provide details about each trend including what it's about and key posts."
return self.search_x(
query=prompt,
enable_image_understanding=True,
)
+372
View File
@@ -0,0 +1,372 @@
{
"$schema": "../../../schemas/tool-schemas/tool.json",
"tool_id": "grok",
"toolkit_id": "search_web",
"name": "Grok",
"description": "AI-powered deep research tool with real-time web search and X/Twitter access. Use this for comprehensive research, trending topics on X, searching social media posts, or when the owner explicitly asks for Grok.",
"icon_name": "grok-ai-line",
"author": {
"name": "Louis Grenard",
"email": "louis@getleon.ai",
"url": "https://twitter.com/grenlouis"
},
"functions": {
"listModels": {
"description": "List available Grok models.",
"parameters": {
"type": "object",
"properties": {}
}
},
"chatCompletion": {
"description": "Create a chat completion using the Grok Responses API.",
"parameters": {
"type": "object",
"properties": {
"options": {
"type": "object",
"properties": {
"input": {
"type": "array",
"items": {
"type": "object",
"properties": {
"role": {
"type": "string",
"enum": [
"system",
"user",
"assistant"
]
},
"content": {
"type": "string"
}
},
"required": [
"role",
"content"
],
"additionalProperties": false
}
},
"model": {
"type": "string"
},
"temperature": {
"type": "number"
},
"max_output_tokens": {
"type": "number"
},
"stream": {
"type": "boolean"
},
"tools": {
"type": "array",
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"web_search"
]
},
"allowed_domains": {
"type": "array",
"items": {
"type": "string"
}
},
"excluded_domains": {
"type": "array",
"items": {
"type": "string"
}
},
"enable_image_understanding": {
"type": "boolean"
}
},
"required": [
"type"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"x_search"
]
},
"allowed_x_handles": {
"type": "array",
"items": {
"type": "string"
}
},
"excluded_x_handles": {
"type": "array",
"items": {
"type": "string"
}
},
"from_date": {
"type": "string"
},
"to_date": {
"type": "string"
},
"enable_image_understanding": {
"type": "boolean"
},
"enable_video_understanding": {
"type": "boolean"
}
},
"required": [
"type"
],
"additionalProperties": false
}
]
}
}
},
"required": [
"input"
],
"additionalProperties": false
}
},
"required": [
"options"
]
}
},
"searchWeb": {
"description": "Search the web using Grok's server-side web search tool.",
"hooks": {
"post_execution": {
"response_jq": ".result.content"
}
},
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"allowed_domains": {
"type": "array",
"items": {
"type": "string"
}
},
"excluded_domains": {
"type": "array",
"items": {
"type": "string"
}
},
"enable_image_understanding": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"required": [
"query"
]
}
},
"searchX": {
"description": "Search X/Twitter using Grok's server-side X search tool.",
"hooks": {
"post_execution": {
"response_jq": ".result.content"
}
},
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"allowed_x_handles": {
"type": "array",
"items": {
"type": "string"
}
},
"excluded_x_handles": {
"type": "array",
"items": {
"type": "string"
}
},
"from_date": {
"type": "string"
},
"to_date": {
"type": "string"
},
"enable_image_understanding": {
"type": "boolean"
},
"enable_video_understanding": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"required": [
"query"
]
}
},
"search": {
"description": "Search both web and X using Grok's server-side search tools.",
"hooks": {
"post_execution": {
"response_jq": ".result.content"
}
},
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"web_options": {
"type": "object",
"properties": {
"allowed_domains": {
"type": "array",
"items": {
"type": "string"
}
},
"excluded_domains": {
"type": "array",
"items": {
"type": "string"
}
},
"enable_image_understanding": {
"type": "boolean"
}
},
"additionalProperties": false
},
"x_options": {
"type": "object",
"properties": {
"allowed_x_handles": {
"type": "array",
"items": {
"type": "string"
}
},
"excluded_x_handles": {
"type": "array",
"items": {
"type": "string"
}
},
"from_date": {
"type": "string"
},
"to_date": {
"type": "string"
},
"enable_image_understanding": {
"type": "boolean"
},
"enable_video_understanding": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"required": [
"query"
]
}
},
"deepResearch": {
"description": "Perform deep research on a topic using web search.",
"hooks": {
"post_execution": {
"response_jq": ".result.content"
}
},
"parameters": {
"type": "object",
"properties": {
"topic": {
"type": "string"
},
"focusAreas": {
"type": "array",
"items": {
"type": "string"
}
},
"options": {
"type": "object",
"properties": {
"allowed_domains": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}
},
"required": [
"topic"
]
}
},
"getTrendingOnX": {
"description": "Get trending topics on X/Twitter.",
"hooks": {
"post_execution": {
"response_jq": ".result.content"
}
},
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string"
}
}
}
}
}
}
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1 @@
{}
@@ -0,0 +1,380 @@
import { createAnthropic } from '@ai-sdk/anthropic'
import { createOpenAI } from '@ai-sdk/openai'
import { Tool } from '@sdk/base-tool'
import { ToolkitConfig } from '@sdk/toolkit-config'
const TOOLKIT_ID = 'search_web'
const TOOL_ID = 'hosted'
const DEFAULT_MAX_OUTPUT_TOKENS = 2_000
const DEFAULT_TEMPERATURE = 0.2
const DEFAULT_SETTINGS: Record<string, unknown> = {}
type HostedSearchProvider = 'openai' | 'anthropic'
interface HostedSearchOptions {
provider?: 'auto' | HostedSearchProvider
model?: string
max_output_tokens?: number
temperature?: number
}
interface HostedSearchResult {
provider: HostedSearchProvider
model: string
content: string
used_input_tokens?: number
used_output_tokens?: number
}
interface ResolvedTarget {
provider: HostedSearchProvider
model: string
}
interface ModelTarget {
provider: string
model: string
}
interface GenerationState {
text: string
usedInputTokens?: number
usedOutputTokens?: number
}
export default class HostedTool extends Tool {
private readonly config: ReturnType<typeof ToolkitConfig.load>
constructor() {
super()
this.config = ToolkitConfig.load(TOOLKIT_ID, this.toolName)
this.settings = ToolkitConfig.loadToolSettings(
TOOLKIT_ID,
this.toolName,
DEFAULT_SETTINGS
)
}
get toolName(): string {
return TOOL_ID
}
get toolkit(): string {
return TOOLKIT_ID
}
get description(): string {
return this.config['description']
}
async searchWeb(
query: string,
options?: HostedSearchOptions
): Promise<HostedSearchResult> {
return this.searchWithProvider(
query,
this.resolveTarget(options?.provider || 'auto', options?.model),
options
)
}
async searchOpenAI(
query: string,
options?: Omit<HostedSearchOptions, 'provider'>
): Promise<HostedSearchResult> {
return this.searchWithProvider(
query,
this.resolveTarget('openai', options?.model),
options
)
}
async searchAnthropic(
query: string,
options?: Omit<HostedSearchOptions, 'provider'>
): Promise<HostedSearchResult> {
return this.searchWithProvider(
query,
this.resolveTarget('anthropic', options?.model),
options
)
}
private resolveTarget(
requestedProvider: 'auto' | HostedSearchProvider,
requestedModel?: string
): ResolvedTarget {
if (requestedProvider !== 'auto') {
return {
provider: requestedProvider,
model: this.resolveModel(requestedProvider, requestedModel)
}
}
const activeTarget = this.getActiveLLMTarget()
if (
activeTarget &&
this.isSupportedProvider(activeTarget.provider)
) {
return {
provider: activeTarget.provider,
model: requestedModel || activeTarget.model
}
}
throw new Error(
'The active LLM provider does not support hosted search. Choose provider openai or anthropic.'
)
}
private async searchWithProvider(
query: string,
target: ResolvedTarget,
options?: Omit<HostedSearchOptions, 'provider'>
): Promise<HostedSearchResult> {
const state = await this.runHostedSearch(query, target, options)
const content = state.text.trim()
if (!content) {
throw new Error(
`Hosted search returned no text for ${target.provider}/${target.model}.`
)
}
return {
provider: target.provider,
model: target.model,
content,
...(typeof state.usedInputTokens === 'number'
? { used_input_tokens: state.usedInputTokens }
: {}),
...(typeof state.usedOutputTokens === 'number'
? { used_output_tokens: state.usedOutputTokens }
: {})
}
}
private async runHostedSearch(
query: string,
target: ResolvedTarget,
options?: Omit<HostedSearchOptions, 'provider'>
): Promise<GenerationState> {
const maxOutputTokens = this.resolveMaxOutputTokens(options)
const temperature = this.resolveTemperature(options)
const callOptions: Record<string, unknown> = {
prompt: this.toPrompt(query),
maxOutputTokens,
temperature,
tools: [this.createHostedSearchTool(target.provider)]
}
const languageModel = this.createLanguageModel(target)
const result = await (
languageModel as {
doGenerate: (
options: Record<string, unknown>
) => Promise<Record<string, unknown>>
}
).doGenerate(callOptions)
return this.extractGenerationState(result)
}
private createLanguageModel(target: ResolvedTarget): unknown {
if (target.provider === 'openai') {
const apiKey = this.readRequiredEnv('LEON_OPENAI_API_KEY')
const provider = createOpenAI({
apiKey,
baseURL: 'https://api.openai.com/v1'
})
return provider.responses(target.model)
}
const apiKey = this.readRequiredEnv('LEON_ANTHROPIC_API_KEY')
const provider = createAnthropic({
apiKey,
baseURL: 'https://api.anthropic.com/v1'
})
return provider(target.model)
}
private createHostedSearchTool(
providerName: HostedSearchProvider
): Record<string, unknown> {
if (providerName === 'openai') {
const provider = createOpenAI()
return provider.tools.webSearch() as unknown as Record<string, unknown>
}
const provider = createAnthropic()
return provider.tools.webSearch_20250305({}) as unknown as Record<
string,
unknown
>
}
private toPrompt(query: string): Array<Record<string, unknown>> {
return [
{
role: 'system',
content:
'Answer the user request using hosted web search when current public information is needed. Return a concise, direct answer. Do not mention internal tool usage.'
},
{
role: 'user',
content: [
{
type: 'text',
text: query
}
]
}
]
}
private extractGenerationState(result: Record<string, unknown>): GenerationState {
const state: GenerationState = {
text: ''
}
const content = Array.isArray(result['content'])
? (result['content'] as Array<Record<string, unknown>>)
: []
for (const part of content) {
if (part['type'] === 'text' && typeof part['text'] === 'string') {
state.text += part['text']
}
}
this.appendUsage(state, result['usage'])
return state
}
private appendUsage(state: GenerationState, usage: unknown): void {
if (!usage || typeof usage !== 'object') {
return
}
const usageRecord = usage as Record<string, unknown>
const inputTokens =
this.readTokenCount(usageRecord['inputTokens']) ??
this.readTokenCount(usageRecord['input_tokens']) ??
this.readTokenCount(usageRecord['promptTokens']) ??
this.readTokenCount(usageRecord['prompt_tokens'])
const outputTokens =
this.readTokenCount(usageRecord['outputTokens']) ??
this.readTokenCount(usageRecord['output_tokens']) ??
this.readTokenCount(usageRecord['completionTokens']) ??
this.readTokenCount(usageRecord['completion_tokens'])
if (typeof inputTokens === 'number') {
state.usedInputTokens = inputTokens
}
if (typeof outputTokens === 'number') {
state.usedOutputTokens = outputTokens
}
}
private readTokenCount(value: unknown): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (value && typeof value === 'object') {
const total = (value as Record<string, unknown>)['total']
if (typeof total === 'number' && Number.isFinite(total)) {
return total
}
}
return undefined
}
private resolveModel(
providerName: HostedSearchProvider,
requestedModel?: string
): string {
if (requestedModel?.trim()) {
return requestedModel.trim()
}
const activeTarget = this.getActiveLLMTarget()
if (activeTarget?.provider === providerName && activeTarget.model) {
return activeTarget.model
}
throw new Error(
`No active .env LLM model is configured for hosted search provider "${providerName}". Use searchWeb with provider auto, configure LEON_AGENT_LLM/LEON_LLM with the same provider, or pass options.model.`
)
}
private resolveMaxOutputTokens(
options?: Omit<HostedSearchOptions, 'provider'>
): number {
const value = options?.max_output_tokens
return typeof value === 'number' && Number.isFinite(value)
? Math.max(1, Math.floor(value))
: DEFAULT_MAX_OUTPUT_TOKENS
}
private resolveTemperature(
options?: Omit<HostedSearchOptions, 'provider'>
): number {
return typeof options?.temperature === 'number' &&
Number.isFinite(options.temperature)
? options.temperature
: DEFAULT_TEMPERATURE
}
private getActiveLLMTarget(): ModelTarget | null {
const rawTarget =
process.env['LEON_AGENT_LLM'] ||
process.env['LEON_LLM'] ||
process.env['LEON_WORKFLOW_LLM'] ||
''
return this.parseModelTarget(rawTarget)
}
private parseModelTarget(rawTarget: string): ModelTarget | null {
const normalizedTarget = rawTarget.trim()
const separatorIndex = normalizedTarget.indexOf('/')
if (separatorIndex <= 0) {
return null
}
const provider = normalizedTarget.slice(0, separatorIndex).trim()
const model = normalizedTarget.slice(separatorIndex + 1).trim()
if (!provider || !model) {
return null
}
return {
provider,
model
}
}
private isSupportedProvider(
providerName: string
): providerName is HostedSearchProvider {
return (
providerName === 'openai' ||
providerName === 'anthropic'
)
}
private readRequiredEnv(key: string): string {
const value = process.env[key]
if (!value) {
throw new Error(
`${key} is not configured. Configure the regular LLM provider API key.`
)
}
return value
}
}
@@ -0,0 +1 @@
export { default } from './hosted-tool'
+125
View File
@@ -0,0 +1,125 @@
{
"$schema": "../../../schemas/tool-schemas/tool.json",
"tool_id": "hosted",
"toolkit_id": "search_web",
"name": "Hosted LLM Search",
"description": "Native web search through the configured LLM provider. Reuses the same provider, model, and API key configured for the active LLM in .env.",
"icon_name": "global-line",
"author": {
"name": "Louis Grenard",
"email": "louis@getleon.ai",
"url": "https://twitter.com/grenlouis"
},
"functions": {
"searchWeb": {
"description": "Search the web using the active LLM provider's native hosted search when supported. Reuses the same provider, model, and API key configured for the active LLM in .env.",
"hooks": {
"post_execution": {
"response_jq": ".result.content"
}
},
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"provider": {
"type": "string",
"enum": [
"auto",
"openai",
"anthropic"
]
},
"model": {
"type": "string"
},
"max_output_tokens": {
"type": "number"
},
"temperature": {
"type": "number"
}
},
"additionalProperties": false
}
},
"required": [
"query"
]
}
},
"searchOpenAI": {
"description": "Search the web using OpenAI's native hosted web search and the configured LEON_OPENAI_API_KEY.",
"hooks": {
"post_execution": {
"response_jq": ".result.content"
}
},
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"model": {
"type": "string"
},
"max_output_tokens": {
"type": "number"
},
"temperature": {
"type": "number"
}
},
"additionalProperties": false
}
},
"required": [
"query"
]
}
},
"searchAnthropic": {
"description": "Search the web using Anthropic's native hosted web search and the configured LEON_ANTHROPIC_API_KEY.",
"hooks": {
"post_execution": {
"response_jq": ".result.content"
}
},
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string"
},
"options": {
"type": "object",
"properties": {
"model": {
"type": "string"
},
"max_output_tokens": {
"type": "number"
},
"temperature": {
"type": "number"
}
},
"additionalProperties": false
}
},
"required": [
"query"
]
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
"name": "Search & Web",
"description": "Tools to search the web and social media platforms.",
"icon_name": "global-line",
"context_files": [
"BROWSER_HISTORY.md"
],
"tools": [
"hosted",
"grok"
]
}