From 5329b3eeb0c2224f2eb6613ff8710d40c12c1233 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Mon, 18 May 2026 17:54:11 +0300 Subject: [PATCH 1/5] docs(tools): add Xquik examples Signed-off-by: kriptoburak --- python/examples/README.md | 1 + python/examples/tools/custom/xquik.py | 107 ++++++++++++++++++++++ typescript/examples/README.md | 1 + typescript/examples/tools/custom/xquik.ts | 55 +++++++++++ 4 files changed, 164 insertions(+) create mode 100644 python/examples/tools/custom/xquik.py create mode 100644 typescript/examples/tools/custom/xquik.ts diff --git a/python/examples/README.md b/python/examples/README.md index 5dc925f1c..a0e9f4f16 100644 --- a/python/examples/README.md +++ b/python/examples/README.md @@ -60,6 +60,7 @@ This repository contains examples demonstrating the usage of the BeeAI Framework - [`decorator.py`](/python/examples/tools/decorator.py): Tool creation using decorator - [`duckduckgo.py`](/python/examples/tools/duckduckgo.py): DDG Search Tool for searching the web - [`openmeteo.py`](/python/examples/tools/openmeteo.py): Open-Meteo Tool for retrieving weather data +- [`custom/xquik.py`](/python/examples/tools/custom/xquik.py): Xquik API tool for searching X posts ## Observability diff --git a/python/examples/tools/custom/xquik.py b/python/examples/tools/custom/xquik.py new file mode 100644 index 000000000..eed53f904 --- /dev/null +++ b/python/examples/tools/custom/xquik.py @@ -0,0 +1,107 @@ +import asyncio +import json +import os +import sys +from typing import Any, Literal +from urllib.error import HTTPError, URLError +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +from beeai_framework.context import RunContext +from beeai_framework.emitter import Emitter +from beeai_framework.errors import FrameworkError +from beeai_framework.tools import JSONToolOutput, Tool, ToolError, ToolRunOptions +from pydantic import BaseModel, Field + + +class XquikSearchTweetsToolInput(BaseModel): + query: str = Field(description="X search query with standard search operators.") + query_type: Literal["Latest", "Top"] = Field(default="Latest", description="Sort order.") + limit: int = Field(default=5, ge=1, le=20, description="Maximum tweets to return.") + + +class XquikSearchTweetsToolOutput(JSONToolOutput[dict[str, Any]]): + pass + + +def fetch_xquik_json(url: str, api_key: str) -> dict[str, Any]: + request = Request( + url, + headers={"Accept": "application/json", "x-api-key": api_key}, + ) + try: + with urlopen(request, timeout=30) as response: + body = response.read().decode("utf-8") + except HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") + raise ToolError( + "Request to Xquik API failed.", + cause=RuntimeError(detail), + context={"status_code": exc.code}, + ) from exc + except URLError as exc: + raise ToolError("Could not connect to Xquik API.", cause=exc) from exc + + result = json.loads(body) + if not isinstance(result, dict): + raise ToolError("Xquik API returned an unexpected response shape.") + + return result + + +class XquikSearchTweetsTool( + Tool[XquikSearchTweetsToolInput, ToolRunOptions, XquikSearchTweetsToolOutput] +): + name = "XquikSearchTweets" + description = "Searches X posts through the Xquik REST API." + input_schema = XquikSearchTweetsToolInput + + def _create_emitter(self) -> Emitter: + return Emitter.root().child( + namespace=["tool", "example", "xquik"], + creator=self, + ) + + async def _run( + self, + tool_input: XquikSearchTweetsToolInput, + options: ToolRunOptions | None, + context: RunContext, + ) -> XquikSearchTweetsToolOutput: + api_key = os.getenv("XQUIK_API_KEY") + if not api_key: + raise ToolError("Set XQUIK_API_KEY before running the Xquik search example.") + + base_url = os.getenv("XQUIK_BASE_URL", "https://xquik.com/api/v1").rstrip("/") + params = urlencode( + { + "q": tool_input.query, + "queryType": tool_input.query_type, + "limit": tool_input.limit, + } + ) + result = await asyncio.to_thread(fetch_xquik_json, f"{base_url}/x/tweets/search?{params}", api_key) + + return XquikSearchTweetsToolOutput(result) + + +async def main() -> None: + if not os.getenv("XQUIK_API_KEY"): + print("Set XQUIK_API_KEY to run this example.") + return + + tool = XquikSearchTweetsTool() + result = await tool.run( + XquikSearchTweetsToolInput( + query="from:xquikcom", + limit=5, + ) + ) + print(result.get_text_content()) + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except FrameworkError as e: + sys.exit(e.explain()) diff --git a/typescript/examples/README.md b/typescript/examples/README.md index f53fc60e5..d37c78ffa 100644 --- a/typescript/examples/README.md +++ b/typescript/examples/README.md @@ -118,6 +118,7 @@ This repository contains examples demonstrating the usage of the BeeAI Framework - [`base.ts`](/typescript/examples/tools/custom/base.ts): Custom tool base implementation - [`dynamic.ts`](/typescript/examples/tools/custom/dynamic.ts): Dynamic tool creation - [`openLibrary.ts`](/typescript/examples/tools/custom/openLibrary.ts): OpenLibrary API tool +- [`xquik.ts`](/typescript/examples/tools/custom/xquik.ts): Xquik API tool for searching X posts - [`python.ts`](/typescript/examples/tools/custom/python.ts): Python-based custom tool - [`langchain.ts`](/typescript/examples/tools/langchain.ts): LangChain tool integration diff --git a/typescript/examples/tools/custom/xquik.ts b/typescript/examples/tools/custom/xquik.ts new file mode 100644 index 000000000..085386633 --- /dev/null +++ b/typescript/examples/tools/custom/xquik.ts @@ -0,0 +1,55 @@ +import { DynamicTool, JSONToolOutput, ToolError } from "beeai-framework/tools/base"; +import { z } from "zod"; + +type XquikSearchTweetsResponse = Record; + +const getBaseUrl = () => + (process.env.XQUIK_BASE_URL ?? "https://xquik.com/api/v1").replace(/\/$/, ""); + +export const xquikSearchTweetsTool = new DynamicTool({ + name: "XquikSearchTweets", + description: "Searches X posts through the Xquik REST API.", + inputSchema: z.object({ + query: z.string().min(1).describe("X search query with standard search operators."), + queryType: z.enum(["Latest", "Top"]).default("Latest").describe("Sort order."), + limit: z.number().int().min(1).max(20).default(5).describe("Maximum tweets to return."), + }), + async handler(input, _options, run) { + const apiKey = process.env.XQUIK_API_KEY; + if (!apiKey) { + throw new ToolError("Set XQUIK_API_KEY before running the Xquik search example."); + } + + const url = new URL(`${getBaseUrl()}/x/tweets/search`); + url.searchParams.set("q", input.query); + url.searchParams.set("queryType", input.queryType); + url.searchParams.set("limit", input.limit.toString()); + + const response = await fetch(url, { + headers: { + "Accept": "application/json", + "x-api-key": apiKey, + }, + signal: run.signal, + }); + + if (!response.ok) { + throw new ToolError("Request to Xquik API failed.", [new Error(await response.text())], { + context: { statusCode: response.status }, + }); + } + + const result = (await response.json()) as XquikSearchTweetsResponse; + return new JSONToolOutput(result); + }, +}); + +if (!process.env.XQUIK_API_KEY) { + console.log("Set XQUIK_API_KEY to run this example."); +} else { + const result = await xquikSearchTweetsTool.run({ + query: "from:xquikcom", + limit: 5, + }); + console.log(result.getTextContent()); +} From b0df8cd3d0e04153c923e1f4d87afec4b3ec6567 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Mon, 18 May 2026 18:20:54 +0300 Subject: [PATCH 2/5] docs(tools): address Xquik example review Signed-off-by: kriptoburak --- python/examples/tools/custom/xquik.py | 2 +- typescript/examples/tools/custom/xquik.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/python/examples/tools/custom/xquik.py b/python/examples/tools/custom/xquik.py index eed53f904..b9fbdfae0 100644 --- a/python/examples/tools/custom/xquik.py +++ b/python/examples/tools/custom/xquik.py @@ -31,7 +31,7 @@ def fetch_xquik_json(url: str, api_key: str) -> dict[str, Any]: ) try: with urlopen(request, timeout=30) as response: - body = response.read().decode("utf-8") + body = response.read().decode("utf-8", errors="replace") except HTTPError as exc: detail = exc.read().decode("utf-8", errors="replace") raise ToolError( diff --git a/typescript/examples/tools/custom/xquik.ts b/typescript/examples/tools/custom/xquik.ts index 085386633..377fbf54a 100644 --- a/typescript/examples/tools/custom/xquik.ts +++ b/typescript/examples/tools/custom/xquik.ts @@ -1,4 +1,5 @@ import { DynamicTool, JSONToolOutput, ToolError } from "beeai-framework/tools/base"; +import { pathToFileURL } from "node:url"; import { z } from "zod"; type XquikSearchTweetsResponse = Record; @@ -44,9 +45,11 @@ export const xquikSearchTweetsTool = new DynamicTool({ }, }); -if (!process.env.XQUIK_API_KEY) { +const isDirectRun = import.meta.url === pathToFileURL(process.argv[1] ?? "").href; + +if (isDirectRun && !process.env.XQUIK_API_KEY) { console.log("Set XQUIK_API_KEY to run this example."); -} else { +} else if (isDirectRun) { const result = await xquikSearchTweetsTool.run({ query: "from:xquikcom", limit: 5, From b478a60eb1841097aad1dfcbdd5806128c362da2 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Mon, 15 Jun 2026 19:30:48 +0200 Subject: [PATCH 3/5] docs(tools): fix Xquik Python tool signature Signed-off-by: kriptoburak --- python/examples/tools/custom/xquik.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/examples/tools/custom/xquik.py b/python/examples/tools/custom/xquik.py index b9fbdfae0..e5ffece4e 100644 --- a/python/examples/tools/custom/xquik.py +++ b/python/examples/tools/custom/xquik.py @@ -64,7 +64,7 @@ def _create_emitter(self) -> Emitter: async def _run( self, - tool_input: XquikSearchTweetsToolInput, + input: XquikSearchTweetsToolInput, options: ToolRunOptions | None, context: RunContext, ) -> XquikSearchTweetsToolOutput: @@ -75,9 +75,9 @@ async def _run( base_url = os.getenv("XQUIK_BASE_URL", "https://xquik.com/api/v1").rstrip("/") params = urlencode( { - "q": tool_input.query, - "queryType": tool_input.query_type, - "limit": tool_input.limit, + "q": input.query, + "queryType": input.query_type, + "limit": input.limit, } ) result = await asyncio.to_thread(fetch_xquik_json, f"{base_url}/x/tweets/search?{params}", api_key) From 07f574e42081b34060cc98420b2b50b6267c2d52 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Tue, 16 Jun 2026 23:32:25 +0200 Subject: [PATCH 4/5] docs(tools): fix Xquik Python import order Signed-off-by: kriptoburak --- python/examples/tools/custom/xquik.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/python/examples/tools/custom/xquik.py b/python/examples/tools/custom/xquik.py index e5ffece4e..f56db54d7 100644 --- a/python/examples/tools/custom/xquik.py +++ b/python/examples/tools/custom/xquik.py @@ -7,11 +7,12 @@ from urllib.parse import urlencode from urllib.request import Request, urlopen +from pydantic import BaseModel, Field + from beeai_framework.context import RunContext from beeai_framework.emitter import Emitter from beeai_framework.errors import FrameworkError from beeai_framework.tools import JSONToolOutput, Tool, ToolError, ToolRunOptions -from pydantic import BaseModel, Field class XquikSearchTweetsToolInput(BaseModel): @@ -49,9 +50,7 @@ def fetch_xquik_json(url: str, api_key: str) -> dict[str, Any]: return result -class XquikSearchTweetsTool( - Tool[XquikSearchTweetsToolInput, ToolRunOptions, XquikSearchTweetsToolOutput] -): +class XquikSearchTweetsTool(Tool[XquikSearchTweetsToolInput, ToolRunOptions, XquikSearchTweetsToolOutput]): name = "XquikSearchTweets" description = "Searches X posts through the Xquik REST API." input_schema = XquikSearchTweetsToolInput From dabc768b2afe987a140fa457b8700f5f97334455 Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Thu, 18 Jun 2026 00:54:11 +0300 Subject: [PATCH 5/5] docs(tools): gate Xquik TypeScript example Signed-off-by: kriptoburak --- typescript/tests/examples/examples.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typescript/tests/examples/examples.test.ts b/typescript/tests/examples/examples.test.ts index e9b997f24..744f36e76 100644 --- a/typescript/tests/examples/examples.test.ts +++ b/typescript/tests/examples/examples.test.ts @@ -59,6 +59,7 @@ const exclude: string[] = [ !getEnv("ANTHROPIC_API_KEY") && ["examples/backend/providers/anthropic.ts"], !getEnv("XAI_API_KEY") && ["examples/backend/providers/xai.ts"], !getEnv("MINIMAX_API_KEY") && ["examples/backend/providers/minimax.ts"], + !getEnv("XQUIK_API_KEY") && ["examples/tools/custom/xquik.ts"], "examples/tools/custom/extending.ts", // DDG problems ] .filter(isTruthy)