chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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,
|
||||
)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user