gemcode 0.3.38__tar.gz → 0.3.40__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gemcode-0.3.38/src/gemcode.egg-info → gemcode-0.3.40}/PKG-INFO +1 -1
- {gemcode-0.3.38 → gemcode-0.3.40}/pyproject.toml +1 -1
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/agent.py +26 -6
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/cli.py +11 -0
- gemcode-0.3.40/src/gemcode/intent_classifier.py +185 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/notes.py +4 -2
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tui/scrollback.py +51 -0
- {gemcode-0.3.38 → gemcode-0.3.40/src/gemcode.egg-info}/PKG-INFO +1 -1
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode.egg-info/SOURCES.txt +1 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/LICENSE +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/MANIFEST.in +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/README.md +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/setup.cfg +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/__init__.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/__main__.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/audit.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/autocompact.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/callbacks.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/capability_routing.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/compaction.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/computer_use/__init__.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/computer_use/browser_computer.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/config.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/context_budget.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/context_warning.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/credentials.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/hitl_session.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/hooks.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/interactions.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/invoke.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/kairos_daemon.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/limits.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/live_audio_engine.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/logging_config.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/mcp_loader.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/memory/__init__.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/memory/embedding_memory_service.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/memory/file_memory_service.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/modality_tools.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/model_errors.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/model_routing.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/openapi_loader.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/paths.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/permissions.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/plugins/__init__.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/pricing.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/prompt_suggestions.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/query/__init__.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/query/config.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/query/deps.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/query/engine.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/query/stop_hooks.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/query/token_budget.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/query/transitions.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/refine.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/repl_commands.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/repl_slash.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/review_agent.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/session_runtime.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/session_store.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/slash_commands.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/thinking.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tool_prompt_manifest.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tool_registry.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/__init__.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/bash.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/browser.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/edit.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/filesystem.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/search.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/shell.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/shell_gate.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/subtask.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/think.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/todo.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools/web.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tools_inspector.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/trust.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tui/input_handler.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tui/spinner.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tui/welcome_banner.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/tui/welcome_rich.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/version.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/vertex.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/web/__init__.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/web/claude_sse_adapter.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/web/terminal_repl.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode/workspace_hints.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode.egg-info/dependency_links.txt +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode.egg-info/entry_points.txt +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode.egg-info/requires.txt +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/src/gemcode.egg-info/top_level.txt +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_agent_instruction.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_autocompact.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_capability_routing.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_claude_web_adapter_sse.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_cli_init.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_computer_use_permissions.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_context_budget.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_context_warning.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_credentials.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_interactive_permission_ask.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_kairos_scheduler.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_modality_tools.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_model_error_retry.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_model_errors.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_model_routing.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_paths.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_permissions.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_prompt_suggestions.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_repl_commands.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_repl_slash.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_slash_commands.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_thinking_config.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_token_budget.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_tool_context_circulation.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_tools.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_tools_inspector.py +0 -0
- {gemcode-0.3.38 → gemcode-0.3.40}/tests/test_workspace_hints.py +0 -0
|
@@ -397,8 +397,29 @@ You run locally via the GemCode CLI. You are the same agent the user launched
|
|
|
397
397
|
{_build_runtime_facts(cfg)}
|
|
398
398
|
|
|
399
399
|
## Core identity and approach
|
|
400
|
-
|
|
401
|
-
|
|
400
|
+
|
|
401
|
+
### Intent-aware workflow
|
|
402
|
+
|
|
403
|
+
Before you see this message, a lightweight LLM classifier already determined the user's intent
|
|
404
|
+
and stored it in session state as `_gemcode_intent`. Read it and adapt your behaviour:
|
|
405
|
+
|
|
406
|
+
| `_gemcode_intent` | Meaning | What to do |
|
|
407
|
+
|---|---|---|
|
|
408
|
+
| `GREETING` | Social / chitchat | *(You will never see this — handled upstream before reaching you.)* |
|
|
409
|
+
| `CONCEPT` | General knowledge question | Answer from your training knowledge. No tools unless the answer truly requires this repo's files. |
|
|
410
|
+
| `PROJECT_QUESTION` | Question about this codebase | Use 1–2 focused read-only tools (`list_directory`, `read_file`, `grep_content`) to find the answer, then reply concisely. |
|
|
411
|
+
| `ENGINEERING_TASK` | Code modification / fix / build | Full workflow: **Orient → Plan → Execute → Verify**. |
|
|
412
|
+
| `ANALYSIS` | Systematic audit or summarisation | Thorough tool sweep: list → read → grep across the affected area, then synthesise findings. |
|
|
413
|
+
|
|
414
|
+
If `_gemcode_intent` is not set (first turn or classifier unavailable), infer the intent yourself
|
|
415
|
+
from the message before acting — the categories above still apply.
|
|
416
|
+
|
|
417
|
+
**Under no circumstances call `list_directory`, `read_project_notes`, or any tool in
|
|
418
|
+
response to a greeting or a simple general question that requires no project context.**
|
|
419
|
+
|
|
420
|
+
### Engineering task workflow
|
|
421
|
+
When the intent is `ENGINEERING_TASK`:
|
|
422
|
+
1. **Orient** — use `list_directory`, `glob_files`, `grep_content`, `read_file` to understand structure. These tools need **no permission** and are instant.
|
|
402
423
|
2. **Plan** — for complex tasks, call `todo_write` upfront to map out the work.
|
|
403
424
|
3. **Execute** — make the changes, run the checks, iterate.
|
|
404
425
|
4. **Verify** — confirm the result is correct before reporting done.
|
|
@@ -426,8 +447,8 @@ You have native deep thinking capability — use it actively:
|
|
|
426
447
|
- **For trade-off decisions** (which library, which pattern, which approach): reason through the pros/cons given this specific codebase.
|
|
427
448
|
|
|
428
449
|
## Interpreting requests
|
|
429
|
-
-
|
|
430
|
-
-
|
|
450
|
+
- **Let `_gemcode_intent` guide you.** The pre-classifier already did the hard work. Trust it and act accordingly (see the table above).
|
|
451
|
+
- **Engineering tasks** ("fix", "add", "refactor", "analyse", "debug"): infer from the repo — search, read, then act. Do not give abstract advice when concrete files exist.
|
|
431
452
|
- If the user refers to symbols or behaviors, **find them** with `glob_files`/`grep_content`/`list_directory` — never ask them to paste paths you can discover yourself.
|
|
432
453
|
- **Never propose edits to files you haven't read.** Read first, then edit.
|
|
433
454
|
- When something fails, diagnose (re-read the error, check assumptions) before switching strategy. Do not repeat the same failed call.
|
|
@@ -615,8 +636,7 @@ You have two tools to persist project insights across sessions, like Claude Code
|
|
|
615
636
|
Call this **immediately** when you discover something useful — not just at the end of tasks.
|
|
616
637
|
Notes are loaded at session start so future sessions inherit this knowledge.
|
|
617
638
|
|
|
618
|
-
- **`read_project_notes()`** — read current notes
|
|
619
|
-
If notes exist and you haven't read them yet, read them first to avoid re-discovering known information."""
|
|
639
|
+
- **`read_project_notes()`** — read current notes **only when starting a real engineering task** (editing, debugging, building). Do NOT call this for greetings or general questions. If notes exist and you're about to work on a task, read them once to avoid re-discovering known information."""
|
|
620
640
|
|
|
621
641
|
# Inject capability-specific strategy sections only when those caps are on.
|
|
622
642
|
if getattr(cfg, "enable_computer_use", False):
|
|
@@ -154,6 +154,17 @@ async def _run_prompt(
|
|
|
154
154
|
# MCP and OpenAPI toolsets are now loaded inside create_runner() directly.
|
|
155
155
|
runner = create_runner(cfg, extra_tools=None)
|
|
156
156
|
try:
|
|
157
|
+
# LLM intent pre-classifier: greetings bypass the main agent entirely.
|
|
158
|
+
try:
|
|
159
|
+
from gemcode.intent_classifier import (
|
|
160
|
+
classify_intent, generate_greeting_reply, INTENT_GREETING
|
|
161
|
+
)
|
|
162
|
+
_intent = await classify_intent(prompt)
|
|
163
|
+
if _intent == INTENT_GREETING:
|
|
164
|
+
return await generate_greeting_reply(prompt)
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
|
|
157
168
|
collected = await run_turn(
|
|
158
169
|
runner,
|
|
159
170
|
user_id="local",
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM-based intent pre-classifier.
|
|
3
|
+
|
|
4
|
+
Before each user turn a lightweight Gemini call (gemini-2.0-flash-lite by
|
|
5
|
+
default) classifies the message into one of five intents. The TUI / CLI
|
|
6
|
+
can then:
|
|
7
|
+
|
|
8
|
+
- Short-circuit GREETING turns entirely (instant reply, zero tool calls)
|
|
9
|
+
- Inject the intent label into session state so the main agent adapts its
|
|
10
|
+
workflow automatically without re-classifying
|
|
11
|
+
|
|
12
|
+
Configure:
|
|
13
|
+
GEMCODE_INTENT_MODEL Override the classifier model (default: gemini-2.0-flash-lite)
|
|
14
|
+
GEMCODE_INTENT_CLASSIFY_ENABLED Set "0" to disable (falls back to main agent)
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
# ── Intent labels ─────────────────────────────────────────────────────────────
|
|
21
|
+
INTENT_GREETING = "GREETING"
|
|
22
|
+
INTENT_CONCEPT = "CONCEPT"
|
|
23
|
+
INTENT_PROJECT_QUESTION = "PROJECT_QUESTION"
|
|
24
|
+
INTENT_ENGINEERING_TASK = "ENGINEERING_TASK"
|
|
25
|
+
INTENT_ANALYSIS = "ANALYSIS"
|
|
26
|
+
|
|
27
|
+
_VALID_INTENTS = {
|
|
28
|
+
INTENT_GREETING,
|
|
29
|
+
INTENT_CONCEPT,
|
|
30
|
+
INTENT_PROJECT_QUESTION,
|
|
31
|
+
INTENT_ENGINEERING_TASK,
|
|
32
|
+
INTENT_ANALYSIS,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Mapping from intent to a short description injected into the main agent's context
|
|
36
|
+
INTENT_DESCRIPTIONS: dict[str, str] = {
|
|
37
|
+
INTENT_GREETING: "Conversational greeting or social message — reply warmly, no tools.",
|
|
38
|
+
INTENT_CONCEPT: "General knowledge question — answer from knowledge, no project files needed.",
|
|
39
|
+
INTENT_PROJECT_QUESTION: "Question about this specific codebase — use 1-2 read-only tools, then reply.",
|
|
40
|
+
INTENT_ENGINEERING_TASK: "Code task (fix / add / refactor / debug) — orient → plan → execute → verify.",
|
|
41
|
+
INTENT_ANALYSIS: "Systematic audit or summarisation — thorough tool sweep, then synthesise.",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# ── Prompts ───────────────────────────────────────────────────────────────────
|
|
45
|
+
_CLASSIFY_PROMPT = """\
|
|
46
|
+
Classify the user message into exactly ONE intent label from the list below.
|
|
47
|
+
|
|
48
|
+
Labels:
|
|
49
|
+
GREETING — greetings, thanks, social messages, chitchat
|
|
50
|
+
(hi, hii, hello, hey, thanks, cool, nice, ok, goodbye, how are you, etc.)
|
|
51
|
+
CONCEPT — general knowledge question needing no project files
|
|
52
|
+
(what is X, explain Y, compare A vs B, best practice for Z)
|
|
53
|
+
PROJECT_QUESTION — question about THIS specific codebase
|
|
54
|
+
(how does auth work here, what does this file do, where is X defined)
|
|
55
|
+
ENGINEERING_TASK — request to write, fix, modify, debug, or implement code
|
|
56
|
+
ANALYSIS — request to systematically audit, review, summarise, or map the codebase
|
|
57
|
+
|
|
58
|
+
User message: "{message}"
|
|
59
|
+
|
|
60
|
+
Reply with ONLY the label name, nothing else."""
|
|
61
|
+
|
|
62
|
+
_GREETING_SYSTEM = (
|
|
63
|
+
"You are GemCode, an expert coding assistant. "
|
|
64
|
+
"The user sent you a short conversational message. "
|
|
65
|
+
"Reply naturally and warmly in ONE brief sentence. "
|
|
66
|
+
"Do not mention code, tools, files, or ask what they need."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
_CLASSIFIER_MODEL_ENV = "GEMCODE_INTENT_MODEL"
|
|
70
|
+
_DEFAULT_CLASSIFIER_MODEL = "gemini-2.0-flash-lite"
|
|
71
|
+
|
|
72
|
+
# Single-word / very-short messages that are unambiguously greetings —
|
|
73
|
+
# checked locally before spending an API call on the classifier.
|
|
74
|
+
_OBVIOUS_GREETINGS: frozenset[str] = frozenset({
|
|
75
|
+
"hi", "hii", "hiii", "hey", "hello", "heya", "hiya", "howdy", "sup", "yo",
|
|
76
|
+
"thanks", "thank you", "thx", "ty", "thankyou",
|
|
77
|
+
"cool", "nice", "great", "awesome", "ok", "okay", "k",
|
|
78
|
+
"bye", "goodbye", "cya", "later",
|
|
79
|
+
"good morning", "good evening", "good night", "good afternoon",
|
|
80
|
+
"what's up", "whats up", "wassup",
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _classifier_enabled() -> bool:
|
|
85
|
+
v = os.environ.get("GEMCODE_INTENT_CLASSIFY_ENABLED", "1")
|
|
86
|
+
return v.lower() not in ("0", "false", "no", "off")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_classifier_model() -> str:
|
|
90
|
+
return os.environ.get(_CLASSIFIER_MODEL_ENV) or _DEFAULT_CLASSIFIER_MODEL
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_api_key() -> str:
|
|
94
|
+
return (
|
|
95
|
+
os.environ.get("GOOGLE_API_KEY")
|
|
96
|
+
or os.environ.get("GEMINI_API_KEY")
|
|
97
|
+
or ""
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def classify_intent(message: str) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Classify a user message using a lightweight Gemini call.
|
|
104
|
+
|
|
105
|
+
Returns one of: GREETING, CONCEPT, PROJECT_QUESTION, ENGINEERING_TASK, ANALYSIS.
|
|
106
|
+
Falls back to ENGINEERING_TASK on any error (the main agent handles all real
|
|
107
|
+
tasks safely with that default).
|
|
108
|
+
|
|
109
|
+
Classification is disabled when GEMCODE_INTENT_CLASSIFY_ENABLED=0.
|
|
110
|
+
"""
|
|
111
|
+
if not _classifier_enabled():
|
|
112
|
+
return INTENT_ENGINEERING_TASK
|
|
113
|
+
|
|
114
|
+
stripped = (message or "").strip()
|
|
115
|
+
if not stripped:
|
|
116
|
+
return INTENT_GREETING
|
|
117
|
+
|
|
118
|
+
# Fast local check for unambiguously short greetings — saves an API round-trip.
|
|
119
|
+
lower = stripped.lower()
|
|
120
|
+
if lower in _OBVIOUS_GREETINGS or (len(lower) <= 3 and lower.isalpha()):
|
|
121
|
+
return INTENT_GREETING
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
import google.genai as genai
|
|
125
|
+
from google.genai import types as gtypes
|
|
126
|
+
|
|
127
|
+
client = genai.Client(api_key=_get_api_key())
|
|
128
|
+
resp = await client.aio.models.generate_content(
|
|
129
|
+
model=_get_classifier_model(),
|
|
130
|
+
contents=_CLASSIFY_PROMPT.format(message=stripped[:600]),
|
|
131
|
+
config=gtypes.GenerateContentConfig(
|
|
132
|
+
temperature=0.0,
|
|
133
|
+
max_output_tokens=15,
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
label = (resp.text or "").strip().upper()
|
|
137
|
+
# Exact match first
|
|
138
|
+
if label in _VALID_INTENTS:
|
|
139
|
+
return label
|
|
140
|
+
# Partial match (model may return extra punctuation or lowercase)
|
|
141
|
+
for key in _VALID_INTENTS:
|
|
142
|
+
if key in label:
|
|
143
|
+
return key
|
|
144
|
+
return INTENT_ENGINEERING_TASK
|
|
145
|
+
except Exception:
|
|
146
|
+
return INTENT_ENGINEERING_TASK
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def generate_greeting_reply(message: str) -> str:
|
|
150
|
+
"""
|
|
151
|
+
Generate a warm, natural one-sentence reply for a greeting message.
|
|
152
|
+
|
|
153
|
+
Uses the same lightweight classifier model so the response is instant.
|
|
154
|
+
Falls back to a generic reply on any error.
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
import google.genai as genai
|
|
158
|
+
from google.genai import types as gtypes
|
|
159
|
+
|
|
160
|
+
client = genai.Client(api_key=_get_api_key())
|
|
161
|
+
resp = await client.aio.models.generate_content(
|
|
162
|
+
model=_get_classifier_model(),
|
|
163
|
+
contents=[
|
|
164
|
+
gtypes.Content(
|
|
165
|
+
role="user",
|
|
166
|
+
parts=[gtypes.Part(text=_GREETING_SYSTEM)],
|
|
167
|
+
),
|
|
168
|
+
gtypes.Content(
|
|
169
|
+
role="model",
|
|
170
|
+
parts=[gtypes.Part(text="Got it.")],
|
|
171
|
+
),
|
|
172
|
+
gtypes.Content(
|
|
173
|
+
role="user",
|
|
174
|
+
parts=[gtypes.Part(text=message)],
|
|
175
|
+
),
|
|
176
|
+
],
|
|
177
|
+
config=gtypes.GenerateContentConfig(
|
|
178
|
+
temperature=0.8,
|
|
179
|
+
max_output_tokens=80,
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
text = (resp.text or "").strip()
|
|
183
|
+
return text or "Hey! What can I help you with today?"
|
|
184
|
+
except Exception:
|
|
185
|
+
return "Hey! What can I help you with today?"
|
|
@@ -86,8 +86,10 @@ def build_notes_tools(project_root: Path) -> list:
|
|
|
86
86
|
"""
|
|
87
87
|
Read the current contents of .gemcode/notes.md.
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
ONLY call this when you are about to work on an actual engineering task
|
|
90
|
+
(editing files, debugging, building something) and want to avoid re-discovering
|
|
91
|
+
already-known project facts. Do NOT call this for greetings, chitchat, or
|
|
92
|
+
general questions that don't require project context.
|
|
91
93
|
|
|
92
94
|
Returns the full notes content as a string, or an empty string if no notes exist.
|
|
93
95
|
"""
|
|
@@ -515,6 +515,57 @@ async def run_gemcode_scrollback_tui(
|
|
|
515
515
|
continue
|
|
516
516
|
prompt = slash.model_prompt or prompt
|
|
517
517
|
|
|
518
|
+
# ── LLM intent pre-classifier ────────────────────────────────────────────
|
|
519
|
+
# A lightweight Gemini call (gemini-2.0-flash-lite) classifies the message
|
|
520
|
+
# before the main agent runs. Greetings are short-circuited entirely:
|
|
521
|
+
# we generate a natural reply directly and skip the main agent, so the
|
|
522
|
+
# user gets an instant response with zero unnecessary tool calls.
|
|
523
|
+
# For all other intents the classified label is stored in session state
|
|
524
|
+
# so the main agent can adapt its workflow without re-classifying.
|
|
525
|
+
try:
|
|
526
|
+
from gemcode.intent_classifier import (
|
|
527
|
+
classify_intent,
|
|
528
|
+
generate_greeting_reply,
|
|
529
|
+
INTENT_GREETING,
|
|
530
|
+
INTENT_DESCRIPTIONS,
|
|
531
|
+
)
|
|
532
|
+
_intent = await classify_intent(prompt)
|
|
533
|
+
|
|
534
|
+
if _intent == INTENT_GREETING:
|
|
535
|
+
# Fast path: generate a warm reply with the lightweight model and skip
|
|
536
|
+
# the main agent entirely — no tool calls, no spinner, instant UX.
|
|
537
|
+
_reply = await generate_greeting_reply(prompt)
|
|
538
|
+
print(f" \u23bf {ansi.bold}GemCode{ansi.reset}:")
|
|
539
|
+
console.print(_RichPadding(_RichMarkdown(_reply), (0, 0, 0, 4)))
|
|
540
|
+
# Print minimal footer (no token stats since we bypassed the main model)
|
|
541
|
+
print("")
|
|
542
|
+
if os.environ.get("GEMCODE_TUI_TURN_RULE", "1").lower() in (
|
|
543
|
+
"1", "true", "yes", "on"
|
|
544
|
+
):
|
|
545
|
+
print(f"{ansi.dim}{_hr(ch='─')}{ansi.reset}")
|
|
546
|
+
print("")
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
# Non-greeting: store classified intent in session state so the main
|
|
550
|
+
# agent can read it and adapt its tool-use strategy accordingly.
|
|
551
|
+
try:
|
|
552
|
+
ss = runner.session_service
|
|
553
|
+
app = getattr(runner, "app_name", None) or getattr(cfg, "app_name", "gemcode")
|
|
554
|
+
sess = await ss.get_session(
|
|
555
|
+
app_name=app, user_id="local", session_id=current_session_id
|
|
556
|
+
)
|
|
557
|
+
if sess is not None:
|
|
558
|
+
_desc = INTENT_DESCRIPTIONS.get(_intent, _intent)
|
|
559
|
+
sess.state["_gemcode_intent"] = _intent
|
|
560
|
+
sess.state["_gemcode_intent_desc"] = _desc
|
|
561
|
+
await ss.update_session(sess)
|
|
562
|
+
except Exception:
|
|
563
|
+
pass # State injection is best-effort
|
|
564
|
+
|
|
565
|
+
except Exception:
|
|
566
|
+
pass # Classifier unavailable — fall through to main agent unchanged
|
|
567
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
568
|
+
|
|
518
569
|
# Snapshot pre-turn capability state so we can detect routing-triggered changes.
|
|
519
570
|
_pre_dr = cfg.enable_deep_research
|
|
520
571
|
_pre_emb = cfg.enable_embeddings
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|