Commit Graph

30 Commits

Author SHA1 Message Date
javis-bot
aebf183950 feat: browser-control server on host (real input) + remote-bot routing + ignore env backups
- control-server.mjs runs chrome-control.mjs LOCALLY on the browser host, so a
  remote bot's controlBrowser (BROWSER_CONTROL_URL) drives real xdotool input
  on THIS screen instead of the bot machine. Published on the LAN.
- controlBrowser tool posts to BROWSER_CONTROL_URL when set, else runs locally.
- Drop hard depends_on ollama so a browser-host doesn't start Ollama.
- gitignore .env.bak*/*.bak (a backup with tokens had been left untracked).
2026-06-15 10:41:57 +09:00
javis-bot
bdb012fc7c fix: weather passes named city from utterance; clean navigate reply
GeoIP auto-detect is unavailable in the container, so '부산 날씨' failed
(no location). Extract the city from the utterance and pass it to getWeather.
Also report navigate by site name instead of the mid-load about:blank url.
2026-06-15 01:02:35 +09:00
javis-bot
49061d30f0 fix: deterministic browser navigate for 'go back to <site>' (구글로 돌아가)
The 7B narrated '구글 메인으로 돌아갑니다' without acting, so the screen stayed on
Naver. Split site intent into SEARCH vs NAV: nav words (돌아가/이동/열어/메인/go
back) now drive controlBrowser.navigate to the site homepage directly, search
words run controlBrowser.search — both deterministically, no LLM.
2026-06-15 00:58:53 +09:00
javis-bot
c522e1b285 fix: deterministic weather → one clean Korean sentence (no 'Celsius')
getWeather now returns only the Korean sentence (지금 <곳> 날씨는 <상태>, 기온 N도
(체감 M도)입니다) with no English/°C source. A deterministic weather path in the
engine returns it verbatim, bypassing the 7B which was rephrasing into multiple
sentences and leaking 'Celsius'.
2026-06-14 22:46:03 +09:00
javis-bot
d970bf276e fix: harden Korean-only output lock (front+end, explicit script ban)
qwen2.5:7b leaked Chinese/Cyrillic mid-reply despite the OUTPUT_LANGUAGE
lock, which was buried mid-prompt. Repeat the lock at the END of the system
prompt (recency) and ban specific foreign scripts explicitly.
2026-06-14 22:25:33 +09:00
javis-bot
3d620dc4c7 fix: concise Korean weather reply (current conditions, one sentence)
getWeather returned a verbose multi-section English forecast that the 3B
re-synthesised into long, CJK/°F-leaking answers. Hand it a ready-to-speak
Korean one-liner (지금 <곳> 날씨는 <상태>, 기온 N도(체감 M도)입니다) and drop the
hourly/7-day firehose from the default voice reply.
2026-06-14 21:49:53 +09:00
javis-bot
11a72cb296 fix: deterministic on-screen site search + lock replies to Korean
- Site-specified search ("네이버에서 X 검색해줘") now runs controlBrowser.search
  directly in the engine when broadcasting, instead of relying on the 3B model
  to emit the tool call (it kept narrating "검색하겠습니다" without acting).
- Set OUTPUT_LANGUAGE=ko so replies are Korean-only — stops the small model
  leaking CJK/Hanja and English fragments (每, 朗, "feels like") into weather
  and other answers, and keeps them concise.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 21:07:26 +09:00
javis-bot
37759f2b2c perf: conversational fast-path (skip enrichment) + shorter silence wait
Greetings/small-talk routed no data tool yet still ran the episodic memory
enrichment (LLM keyword extract + diary/graph search, ~1s) every turn. Skip it
when the router picked no external-data tool — the always-injected warm profile
still personalises the reply. Also drop the voice silence-detection wait
800ms -> 600ms for snappier turn-taking. Warm "안녕" now lands well under the
3-4s target.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 03:21:52 +09:00
javis-bot
4bc7f836ae fix: native tool-calling for qwen2.5 (tools actually fire) + kill Chrome infobars
Root cause of "weather/search do nothing": the engine forced TEXT tool-calling
for all <=7B models, but qwen2.5:3b emits clean NATIVE tool calls and fails at
the text format — so it just narrated ("부산 날씨는 맑습니다") and never called
getWeather/webSearch/controlBrowser. Use native tool-calling for tool-capable
small families (qwen2.5/qwen3/llama3.x/mistral); native still auto-falls back
to text on HTTP 400, so non-tool models (gemma) are unaffected.

Also launch Chrome with --test-type (removes the "--no-sandbox unsupported
flag" infobar) and disable the Translate feature/popup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 03:08:49 +09:00
javis-bot
642fa42561 fix: drop webSearch when a site is named in screen-share, forcing controlBrowser
The 3B model kept choosing webSearch over controlBrowser even when offered, so
'네이버에서 X 검색' still used the invisible web path. When broadcasting and the
user explicitly names a site, remove webSearch from the allow-list so the
on-screen browser is the only search route.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 02:57:51 +09:00
javis-bot
f3d34bab4d fix: always offer controlBrowser in screen-share so on-screen search works
The small router reflexively routed every "search/open" intent to webSearch
and never surfaced controlBrowser, so "네이버에서 X 검색해줘" did nothing on the
broadcast. Union controlBrowser (+browseAndPlay) into the allow-list every
turn in screen-share mode (like setBroadcast), and steer the model in the
system prompt to prefer the on-screen browser over webSearch when available.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 02:55:33 +09:00
javis-bot
c21c5b225f feat: controlBrowser 'search' action + one-sentence voice replies
- Add a one-shot `search` action (site=naver/google/daum/youtube/bing) that
  navigates the on-screen browser straight to the results page, so a small
  model can satisfy "search X on Naver" in a single tool call instead of a
  fragile navigate->type->enter chain.
- Sharpen the tool description to steer the router to controlBrowser (not
  webSearch) for anything that should happen IN the visible browser.
- System prompt: answer in one short sentence (voice assistant) — also cuts
  TTS time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 02:52:47 +09:00
javis-bot
109dbc7e16 feat: robust controlBrowser — real xdotool input, tabs/back/forward/popups
- Install xdotool + wmctrl (late Docker layer, preserves the melo cache) so
  the on-screen Chrome gets real X input (visible cursor, char-by-char typing)
  instead of synthetic events; falls back to the Playwright API if absent.
- Fix active-tab detection (probe document.visibilityState instead of assuming
  tab 0) so sequential ops target the right tab.
- Add back / forward / refresh; new/switch/close tabs via real keyboard
  (Ctrl+T / Ctrl+<n> / Ctrl+W) when xdotool is present.
- Auto-dismiss native JS dialogs; closePopups clears blank/popup tabs.
- Report broadcast (Go-Live) state in status from the turn context.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 02:33:59 +09:00
javis-bot
3d1e56f60f feat: controlBrowser tool — human-operable on-screen Chrome
Adds a general browser-control tool (navigate to any site, list/open/close/
switch tabs, close popups, click, type, scroll, screenshot) for the Go-Live
Chrome, on top of the existing CDP + xdotool human-input layer (visible
cursor, char-by-char typing). Closes the gap where "open Naver" had no tool
and the model confabulated success. Also adds a system-prompt rule against
claiming actions no tool actually performed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 02:22:36 +09:00
javis-bot
44ebfeafa8 feat: per-call LLM timing, speaker ID, cancel captures on leave
- llm.py: log each Ollama call's caller + total/load/prompt/gen durations
  so a slow voice turn is attributable to a specific internal call
  (router/enrichment/digest/main); a RELOAD marker flags cold reloads.
- voice.ts: track in-flight Opus captures and abort them on session
  destroy(); drop any utterance that finishes after the user left, so no
  trailing post-leave VAD turns are reported.
- userbot.ts: show the speaker's Discord user ID on each transcript line
  (answered and dropped) so it's clear whose audio produced the turn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 00:38:26 +09:00
javis-bot
2c38e7576d perf: unify Ollama num_ctx so a voice turn keeps one resident model
Ollama keeps a separate loaded model instance per (model, num_ctx). The
main agentic chat used num_ctx=8192 while the router/enrichment/digest
passes used 4096, so every voice turn forced at least one cold reload
(~3.4s) when switching context sizes — the dominant per-turn latency
(measured: resident chat call 0.27s vs cold 3.4s).

Introduce a single OLLAMA_NUM_CTX (default 8192, env-tunable for tight
VRAM) used by call_llm_direct, chat_with_messages, call_llm_streaming and
the planner, collapsing a turn to one resident instance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 00:19:53 +09:00
javis-bot
3a4776c709 perf(brain): env to disable pre-loop planner; cut voice latency
Warm per-turn timing showed STT 0.1s, TTS ~1-3s, but the reply engine (LLM) was
8-17s — even a simple "고마워" took 16.7s — because it makes multiple model calls
per turn. Add a PLANNER_ENABLED env override (config.py) and default it to 0 in
the userbot compose so the pre-loop planner's extra LLM round-trip is dropped on
this latency-sensitive voice deployment. Also pins STT_LANGUAGE=ko in compose.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-13 21:59:35 +09:00
javis-bot
989a4f3e98 perf(memory): keep embed model warm across turns (keep_alive 0 -> 5m)
Empirical A/B/C measurement against the live RTX 5050 Ollama stack
(qwen2.5:3b + nomic-embed-text) showed keep_alive=0 unloads the embed
model ~2s after every call, so each turn after a brief idle gap pays a
cold reload. VRAM is not the constraint (~4.4-4.7 GB free with both
models resident) and keep_alive=0 never evicted the chat model, so CPU
embedding (num_gpu=0) gave no benefit. A short positive keep_alive is
the fastest of the three: it keeps the ~0.3 GB embed model resident
across consecutive turns at negligible VRAM cost.

Add tests/test_embeddings.py covering the warm-across-turns behaviour.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-12 23:45:16 +09:00
javis-bot
ccbaed9030 feat(weather): romanise non-Latin place names before geocoding
Open-Meteo's geocoder only matches Latin/English spellings, so a Korean city
name like "서울" returns zero results even though the place exists. With
OUTPUT_LANGUAGE locked to Korean the tool-calling model naturally fills
`location` with the Korean name, which dead-ended on every weather request
("could not find location").

When the first geocode is empty and the name is non-ASCII, ask the warm small
model for the common English exonym ("서울" -> "Seoul") and retry once. ASCII
names skip the round-trip entirely.

Live-verified: "서울 날씨 알려줘" now returns real Seoul weather. Tests cover the
romanise-and-retry path and the ASCII short-circuit.
2026-06-12 21:59:42 +09:00
javis-bot
c8a04a110f fix(brain): recover colon-JSON and single tool_call object forms
qwen2.5:3b emits tool calls in text shapes the parser dropped, breaking
two reviewer-reported behaviours:

- `getWeather: {"location": "Seoul"}` (a JSON object after the colon) was
  dumped wholesale into {"query": "{...}"}, so `location` never reached the
  tool. getWeather then ran with empty args, returned the auto-detected
  location's weather, the model noticed the mismatch and retried — looping up
  to 8 times before giving up with an English error. Now the JSON object after
  the colon is parsed directly as the argument dict.
- `call_stop: {"id":..., "function": {"name": "setBroadcast",
  "arguments": "{\"action\": \"stop\"}"}}` — a single tool_call object without
  the `tool_calls: [...]` array wrapper, behind a `call_xxx:` label — matched
  no form, so the raw JSON leaked to the user AND setBroadcast never ran
  ("방송 꺼줘" did nothing). Now name + arguments are pulled from the embedded
  `function` object when the name is in the allow-list.

Field-captured from the live qwen2.5:3b brain (2026-06-12). Tests cover both
shapes, non-ASCII args, dict/string arguments, and unknown-tool rejection.
2026-06-12 21:59:26 +09:00
javis-bot
8a2a109d5e feat(brain): make OUTPUT_LANGUAGE lock robust on small models
Harden the reply-language lock so qwen2.5:3b reliably stays in the locked
language instead of leaking the query language back in:

- reply_language_directive(): single resolver with clear precedence —
  explicit OUTPUT_LANGUAGE lock wins over the Piper/Chatterbox English-only
  fallback (this deployment's actual TTS is Korean MeloTTS, so the legacy
  English lock was both wrong and contradicting the Korean lock).
- Stronger, override-explicit directive wording, inserted near the FRONT of
  the system prompt so a small model gives it primacy over the persona.
- build_system_prompt(output_language=...): rewrite the persona's "in the
  user's language" clause to the locked language so the persona stops
  fighting the lock.
- docs/llm_contexts.md: document the resolver, precedence, and placement.

Live-verified on the running brain (qwen2.5:3b): Korean voice-style input
and a cold English query both return fully Korean replies with no CJK/Hanja
leak. Tests cover unset/set/agnostic/whitespace + precedence + persona rewrite.
2026-06-12 21:18:47 +09:00
javis-bot
006a32276a feat(brain): add OUTPUT_LANGUAGE reply-language lock
Add an optional OUTPUT_LANGUAGE env var that forces every reply into a
single language. When set, output_language_directive() injects a "respond
only in <language>" instruction (also forbidding other scripts) into the
chat loop's system prompt, next to the existing TTS English-only lock.
Empty (default) keeps the multilingual "reply in the user's language"
behaviour, so upstream is unaffected.

For the Korean-only deployment this also suppresses the occasional trailing
CJK/Hanja fragment qwen2.5:3b leaks on free-form chit-chat.

- system_prompt.py: language-agnostic output_language_directive() helper
- engine.py: read OUTPUT_LANGUAGE, append directive in _build_initial_system_message
- docker-compose.yml + .env.example: document/pass the new var
- docs/llm_contexts.md: note the new gating on the main reply context
- tests: cover unset/set/agnostic/whitespace cases
2026-06-12 21:08:44 +09:00
javis-bot
b91c05a355 perf(brain): pin chat model per-request, unload embeddings; default qwen2.5:3b
Replace the blunt global OLLAMA_KEEP_ALIVE=-1 (which kept every model,
including nomic-embed, resident in VRAM forever) with per-request residency:

- llm.py: all three /api/chat payloads send keep_alive=30m so the actively
  used chat model stays resident and voice turns never pay a cold reload.
- embeddings.py: /api/embeddings sends keep_alive=0 so nomic-embed unloads
  right after each call instead of squatting in VRAM next to the chat model.
- docker-compose.yml: drop the global OLLAMA_KEEP_ALIVE=-1; document the
  per-request scheme on the ollama service.

Switch the default chat model qwen3:8b -> qwen2.5:3b. Verified live on the
RTX 5050 (8GB):
- ollama ps: qwen2.5:3b 2.4GB, 100% GPU (8B was 92% GPU / 8% CPU), UNTIL ~30m
  (the 30m pin, not "Forever"); nomic-embed absent after several enriched turns.
- nvidia-smi: ~3.2GB VRAM used total (qwen 2.4GB + whisper 0.7GB) vs ~6.6GB.
- Korean /text turns: warm 1.7-4s (cold first load ~52s), vs ~5-7s on 8B;
  time/weather/places tool calls fire and reply in Korean.

Known limitation: qwen2.5:3b can occasionally leak a trailing CJK phrase on
free-form chit-chat (factual/tool replies stay clean).
2026-06-12 20:36:19 +09:00
javis-bot
03375a77d0 fix(docker+brain): make container Chrome search work, harden, improve toggle
Addresses review findings on the dockerized stack:
- Container Chrome search was dead: add --remote-debugging-port + a non-default
  --user-data-dir (Chrome 136+ refuses CDP on the default profile), add the
  playwright dep (browse-search.mjs connectOverCDP) with browser download
  skipped, and connect to 127.0.0.1 not "localhost" (container localhost -> ::1
  while Chrome binds IPv4). Verified: browse-search returns real results.
- Broadcast toggle reliability: always offer setBroadcast in screen-share mode
  (the embedding/keyword router dropped it for non-English utterances) and make
  its description force a tool call. "방송 꺼줘"->stop now 5/5; no false triggers.
- Stop the broadcast on voice leave (no orphaned stream).
- Security: bind VNC/noVNC to loopback by default (VNC_BIND override) and the
  bridge to the container loopback (BRIDGE_HOST=127.0.0.1), not published.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 10:44:56 +09:00
javis-bot
ca86390407 feat: couple broadcast to voice + voice-controlled broadcast toggle
Completes the STREAM_BROWSER=true behaviour:
- handleJoin auto-starts the broadcast on voice join and wires the session to
  the guild streamer; each turn reports the live state to the brain so search
  routes Chrome (live) vs Gemini (off).
- New setBroadcast tool lets the user toggle the broadcast by voice ("방송
  켜줘/꺼줘") via the LLM (no hardcoded phrases); it refuses when
  STREAM_BROWSER=false. The directive flows brain -> bridge (broadcast_action)
  -> bot streamer.start/stop, guarded by isActive() so it's idempotent.
- Per-turn IPC uses a thread-local (reply/turn_state.py) instead of threading
  params through the whole engine chain: bridge sets broadcasting in, tool
  records the directive out; Tool.execute exposes broadcasting on ToolContext.

Bot typecheck clean; brain covered by tests/test_set_broadcast.py (+ existing
routing tests). Specs + docs updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 10:08:30 +09:00
javis-bot
5d45d1d3bd feat(brain): route real-time search by live broadcast state
STREAM_BROWSER becomes the broadcast *capability* master flag; the live
screen-share state (new ToolContext.broadcasting, passed per turn by the bot)
decides the backend:
  - master off            -> broadcast disabled, always Gemini
  - master on + live on    -> on-screen Chrome (visible on the stream)
  - master on + live off   -> Gemini
context.broadcasting is None outside the voice path (evals, text entry, older
bot) and falls back to the master flag, so current behaviour is unchanged.
This is the brain-side foundation; bot-side wiring (bridge passes broadcast
state, auto-broadcast on voice join, voice on/off toggle) follows.

Specs + docs/llm_contexts.md updated. Covered by
tests/test_web_search_broadcast_routing.py.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 01:17:50 +09:00
javis-bot
f54e2a46ae fix(brain): drop --approval-mode yolo from Gemini CLI search
yolo auto-approves every tool call, so a real-time search query could in
principle trigger write/shell tools. Default approval mode still auto-runs the
CLI's read-only web search in headless mode but never silently approves
destructive tools. Verified end-to-end: a grounded query returns a current
answer in ~23s with the account OAuth login. Test asserts yolo is absent;
specs and docs/llm_contexts.md updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 00:58:46 +09:00
javis-bot
b88def6756 feat(brain): add Gemini CLI OAuth path for STREAM_BROWSER=false real-time search
Adds a GEMINI_AUTH=oauth (default) sub-mode that shells out to the Gemini CLI
using the user's Google-account login instead of an API key. gemini_cli_search()
runs `gemini -p <query> -o json --skip-trust --approval-mode yolo`, strips
GEMINI_API_KEY/GOOGLE_API_KEY and sets GOOGLE_GENAI_USE_GCA=true so the CLI
selects the account OAuth method and fails fast when no login exists. Bounded by
a 30s timeout and fail-open to the DDG/Brave/Wikipedia cascade on any failure
(CLI missing, login expired, quota 429, timeout). GEMINI_AUTH=apikey keeps the
legacy REST path. Specs and docs/llm_contexts.md updated; behaviour covered by
tests/test_realtime_gemini_cli.py.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 00:53:10 +09:00
javis-bot
702fe8017e feat(brain): wire STREAM_BROWSER real-time modes into the reply engine (browser + Gemini)
Completes the two info modes in the Python brain:

- config.py: read STREAM_BROWSER / GEMINI_API_KEY / GEMINI_MODEL from env into
  Settings (stream_browser, gemini_api_key, gemini_model). Verified load_settings
  reads both modes.
- realtime_search.py: two fail-open backends returning the same fenced
  UNTRUSTED-WEB-EXTRACT envelope: browser_search() shells the Node CDP helper to
  drive the on-screen Chrome (visible on the broadcast); gemini_search() calls
  the Gemini REST API with google_search grounding.
- web_search.run(): routes by mode before the DDG cascade (true->browser,
  false->Gemini), falling through to DDG/Brave/Wikipedia on any miss.
- browse_and_play tool: plays a YouTube video on the shared screen (true mode
  only); registered in the tool registry.
- specs + docs/llm_contexts.md updated (new Gemini LLM context); CLAUDE.md spec
  registry updated.

Verified live against the running Chrome: true-mode webSearch returned real
Google results for "오늘 서울 날씨", browseAndPlay played the IU 밤편지 MV, and
false-mode degrades gracefully on a bad/absent key. A valid GEMINI_API_KEY is
still needed to confirm the real Gemini grounding output.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 16:46:58 +09:00
javis-bot
c4abf63f38 Add Discord-native hybrid front-end for Jarvis (bot + bridge)
Some checks failed
Release / semantic-release (push) Successful in 59s
tests / Unit tests (Linux, Python 3.11) (push) Successful in 13m45s
Release / build-linux (push) Failing after 7m47s
Release / build-windows (push) Has been cancelled
Release / build-macos (arm64, macos-latest) (push) Has been cancelled
Release / build-macos (x64, macos-15-intel) (push) Has been cancelled
Release / release-main (push) Has been cancelled
Release / release-develop (push) Has been cancelled
Transform isair/jarvis into a Discord-controlled voice assistant running on
the Ubuntu VNC desktop, keeping the mature ~39k-line Python brain intact.

- bot/ (Node + bun, discord.js): /자비스 slash commands (ephemeral),
  voice channel join + voice receive/playback, pluggable VNC screen broadcast
  (selfbot live / noVNC / screenshot)
- bridge/ (Python, Flask): wraps jarvis STT + run_reply_engine + Piper TTS
  behind a thin localhost HTTP API
- .env.example, scripts/ (start_bridge/start_bot/dev), README rewrite,
  docs/language-comparison.md and docs/vnc-xfce-setup.md

Language decision: hybrid (Python brain + Node/bun Discord layer) because
Discord blocks bot video; native screen broadcast only works via a Node
selfbot library.
2026-06-09 14:51:05 +09:00