gemcode 0.3.39__tar.gz → 0.3.41__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.
Files changed (121) hide show
  1. {gemcode-0.3.39/src/gemcode.egg-info → gemcode-0.3.41}/PKG-INFO +1 -1
  2. {gemcode-0.3.39 → gemcode-0.3.41}/pyproject.toml +1 -1
  3. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/agent.py +16 -12
  4. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/cli.py +11 -0
  5. gemcode-0.3.41/src/gemcode/intent_classifier.py +226 -0
  6. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/pricing.py +1 -0
  7. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tui/scrollback.py +54 -0
  8. {gemcode-0.3.39 → gemcode-0.3.41/src/gemcode.egg-info}/PKG-INFO +1 -1
  9. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode.egg-info/SOURCES.txt +1 -0
  10. {gemcode-0.3.39 → gemcode-0.3.41}/LICENSE +0 -0
  11. {gemcode-0.3.39 → gemcode-0.3.41}/MANIFEST.in +0 -0
  12. {gemcode-0.3.39 → gemcode-0.3.41}/README.md +0 -0
  13. {gemcode-0.3.39 → gemcode-0.3.41}/setup.cfg +0 -0
  14. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/__init__.py +0 -0
  15. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/__main__.py +0 -0
  16. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/audit.py +0 -0
  17. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/autocompact.py +0 -0
  18. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/callbacks.py +0 -0
  19. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/capability_routing.py +0 -0
  20. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/compaction.py +0 -0
  21. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/computer_use/__init__.py +0 -0
  22. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/computer_use/browser_computer.py +0 -0
  23. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/config.py +0 -0
  24. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/context_budget.py +0 -0
  25. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/context_warning.py +0 -0
  26. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/credentials.py +0 -0
  27. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/hitl_session.py +0 -0
  28. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/hooks.py +0 -0
  29. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/interactions.py +0 -0
  30. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/invoke.py +0 -0
  31. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/kairos_daemon.py +0 -0
  32. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/limits.py +0 -0
  33. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/live_audio_engine.py +0 -0
  34. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/logging_config.py +0 -0
  35. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/mcp_loader.py +0 -0
  36. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/memory/__init__.py +0 -0
  37. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/memory/embedding_memory_service.py +0 -0
  38. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/memory/file_memory_service.py +0 -0
  39. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/modality_tools.py +0 -0
  40. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/model_errors.py +0 -0
  41. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/model_routing.py +0 -0
  42. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/openapi_loader.py +0 -0
  43. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/paths.py +0 -0
  44. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/permissions.py +0 -0
  45. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/plugins/__init__.py +0 -0
  46. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/plugins/terminal_hooks_plugin.py +0 -0
  47. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/plugins/tool_recovery_plugin.py +0 -0
  48. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/prompt_suggestions.py +0 -0
  49. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/query/__init__.py +0 -0
  50. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/query/config.py +0 -0
  51. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/query/deps.py +0 -0
  52. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/query/engine.py +0 -0
  53. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/query/stop_hooks.py +0 -0
  54. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/query/token_budget.py +0 -0
  55. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/query/transitions.py +0 -0
  56. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/refine.py +0 -0
  57. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/repl_commands.py +0 -0
  58. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/repl_slash.py +0 -0
  59. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/review_agent.py +0 -0
  60. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/session_runtime.py +0 -0
  61. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/session_store.py +0 -0
  62. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/slash_commands.py +0 -0
  63. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/thinking.py +0 -0
  64. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tool_prompt_manifest.py +0 -0
  65. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tool_registry.py +0 -0
  66. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/__init__.py +0 -0
  67. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/bash.py +0 -0
  68. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/browser.py +0 -0
  69. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/edit.py +0 -0
  70. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/filesystem.py +0 -0
  71. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/notes.py +0 -0
  72. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/search.py +0 -0
  73. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/shell.py +0 -0
  74. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/shell_gate.py +0 -0
  75. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/subtask.py +0 -0
  76. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/think.py +0 -0
  77. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/todo.py +0 -0
  78. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools/web.py +0 -0
  79. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tools_inspector.py +0 -0
  80. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/trust.py +0 -0
  81. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tui/input_handler.py +0 -0
  82. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tui/spinner.py +0 -0
  83. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tui/welcome_banner.py +0 -0
  84. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/tui/welcome_rich.py +0 -0
  85. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/version.py +0 -0
  86. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/vertex.py +0 -0
  87. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/web/__init__.py +0 -0
  88. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/web/claude_sse_adapter.py +0 -0
  89. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/web/terminal_repl.py +0 -0
  90. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode/workspace_hints.py +0 -0
  91. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode.egg-info/dependency_links.txt +0 -0
  92. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode.egg-info/entry_points.txt +0 -0
  93. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode.egg-info/requires.txt +0 -0
  94. {gemcode-0.3.39 → gemcode-0.3.41}/src/gemcode.egg-info/top_level.txt +0 -0
  95. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_agent_instruction.py +0 -0
  96. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_autocompact.py +0 -0
  97. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_capability_routing.py +0 -0
  98. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_claude_web_adapter_sse.py +0 -0
  99. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_cli_init.py +0 -0
  100. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_computer_use_permissions.py +0 -0
  101. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_context_budget.py +0 -0
  102. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_context_warning.py +0 -0
  103. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_credentials.py +0 -0
  104. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_interactive_permission_ask.py +0 -0
  105. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_kairos_scheduler.py +0 -0
  106. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_modality_tools.py +0 -0
  107. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_model_error_retry.py +0 -0
  108. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_model_errors.py +0 -0
  109. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_model_routing.py +0 -0
  110. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_paths.py +0 -0
  111. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_permissions.py +0 -0
  112. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_prompt_suggestions.py +0 -0
  113. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_repl_commands.py +0 -0
  114. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_repl_slash.py +0 -0
  115. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_slash_commands.py +0 -0
  116. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_thinking_config.py +0 -0
  117. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_token_budget.py +0 -0
  118. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_tool_context_circulation.py +0 -0
  119. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_tools.py +0 -0
  120. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_tools_inspector.py +0 -0
  121. {gemcode-0.3.39 → gemcode-0.3.41}/tests/test_workspace_hints.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.39
3
+ Version: 0.3.41
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gemcode"
7
- version = "0.3.39"
7
+ version = "0.3.41"
8
8
  description = "Local-first coding agent on Google Gemini + ADK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -398,22 +398,27 @@ You run locally via the GemCode CLI. You are the same agent the user launched
398
398
 
399
399
  ## Core identity and approach
400
400
 
401
- ### Classify before acting — choose the right mode for each message
401
+ ### Intent-aware workflow
402
402
 
403
- Not every message is a coding task. Read the intent first and pick the right mode:
403
+ Before you see this message, a **gemini-2.5-flash-lite** intent classifier already determined the user's intent
404
+ and stored it in session state as `_gemcode_intent`. Read it and adapt your behaviour:
404
405
 
405
- | Message type | Examples | Right action |
406
+ | `_gemcode_intent` | Meaning | What to do |
406
407
  |---|---|---|
407
- | **Greeting / chitchat** | "hi", "hii", "hello", "how are you", "thanks" | Reply directly. No tools. No orientation. |
408
- | **Pure question / concept** | "what is a closure?", "explain OAuth", "which is better, Redis or Postgres?" | Answer from knowledge. No tools unless the answer requires inspecting *this* repo. |
409
- | **Vague project question** | "how does auth work here?", "what's the folder structure?" | Call 1–2 read-only tools to find the answer, then reply. Keep it focused. |
410
- | **Engineering task** | "add pagination", "fix the bug in auth.ts", "refactor the DB layer" | Full workflow: orientplanexecuteverify. |
411
- | **Analysis / research** | "analyse the whole backend", "summarise all API endpoints" | Systematic tool use: list → read → grep synthesise. |
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: **OrientPlanExecuteVerify**. |
412
+ | `ANALYSIS` | Systematic audit or summarisation | Thorough tool sweep: list → read → grep across the affected area, then synthesise findings. |
412
413
 
413
- **NEVER call `list_directory`, `read_project_notes`, or any tool in response to a greeting or a general conversational message.** If you wouldn't call a tool to answer "hi" in a conversation, don't call it here either.
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.**
414
419
 
415
420
  ### Engineering task workflow
416
- When the message is a real engineering task:
421
+ When the intent is `ENGINEERING_TASK`:
417
422
  1. **Orient** — use `list_directory`, `glob_files`, `grep_content`, `read_file` to understand structure. These tools need **no permission** and are instant.
418
423
  2. **Plan** — for complex tasks, call `todo_write` upfront to map out the work.
419
424
  3. **Execute** — make the changes, run the checks, iterate.
@@ -442,8 +447,7 @@ You have native deep thinking capability — use it actively:
442
447
  - **For trade-off decisions** (which library, which pattern, which approach): reason through the pros/cons given this specific codebase.
443
448
 
444
449
  ## Interpreting requests
445
- - **Greetings / conversational messages** ("hi", "hii", "hey", "thanks", "cool"): reply naturally with one sentence. Do NOT call any tools. Do NOT apologise for not running code.
446
- - **General questions** that don't mention files or code ("what is X?", "explain Y"): answer from your knowledge directly. Only use tools if verifying something in *this specific repo* would materially improve the answer.
450
+ - **Let `_gemcode_intent` guide you.** The pre-classifier already did the hard work. Trust it and act accordingly (see the table above).
447
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.
448
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.
449
453
  - **Never propose edits to files you haven't read.** Read first, then edit.
@@ -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,226 @@
1
+ """
2
+ LLM-based intent pre-classifier.
3
+
4
+ Before each user turn a lightweight Gemini call (gemini-2.5-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.5-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
+ # One-line summaries for the TUI — same visual lane as ∴ Thinking (collapsed)
45
+ INTENT_THINKING_SUMMARY: dict[str, str] = {
46
+ INTENT_GREETING: "Greeting / chitchat — no tools",
47
+ INTENT_CONCEPT: "General knowledge — answer without repo reads if possible",
48
+ INTENT_PROJECT_QUESTION: "About this repo — a few read-only tools, then answer",
49
+ INTENT_ENGINEERING_TASK: "Engineering task — orient → plan → execute → verify",
50
+ INTENT_ANALYSIS: "Deep analysis — systematic read / grep / synthesise",
51
+ }
52
+
53
+ # How the intent was determined (for TUI suffix)
54
+ SOURCE_LOCAL = "local" # obvious greeting / heuristic, no classifier API call
55
+ SOURCE_LLM = "llm" # gemini-2.5-flash-lite classifier
56
+ SOURCE_OFF = "off" # classifier disabled
57
+
58
+
59
+ def format_intent_thinking_line(intent: str, source: str) -> str | None:
60
+ """
61
+ Single line of text after ``∴ Intent`` in the TUI (same visual lane as
62
+ collapsed ``∴ Thinking``). Returns None when the classifier is off and
63
+ nothing should be shown.
64
+ """
65
+ if source == SOURCE_OFF:
66
+ return None
67
+ summary = INTENT_THINKING_SUMMARY.get(intent, intent)
68
+ if source == SOURCE_LOCAL:
69
+ tag = "instant"
70
+ else:
71
+ tag = "flash-lite classifier"
72
+ return f"{intent} — {summary} · {tag}"
73
+
74
+ # ── Prompts ───────────────────────────────────────────────────────────────────
75
+ _CLASSIFY_PROMPT = """\
76
+ Classify the user message into exactly ONE intent label from the list below.
77
+
78
+ Labels:
79
+ GREETING — greetings, thanks, social messages, chitchat
80
+ (hi, hii, hello, hey, thanks, cool, nice, ok, goodbye, how are you, etc.)
81
+ CONCEPT — general knowledge question needing no project files
82
+ (what is X, explain Y, compare A vs B, best practice for Z)
83
+ PROJECT_QUESTION — question about THIS specific codebase
84
+ (how does auth work here, what does this file do, where is X defined)
85
+ ENGINEERING_TASK — request to write, fix, modify, debug, or implement code
86
+ ANALYSIS — request to systematically audit, review, summarise, or map the codebase
87
+
88
+ User message: "{message}"
89
+
90
+ Reply with ONLY the label name, nothing else."""
91
+
92
+ _GREETING_SYSTEM = (
93
+ "You are GemCode, an expert coding assistant. "
94
+ "The user sent you a short conversational message. "
95
+ "Reply naturally and warmly in ONE brief sentence. "
96
+ "Do not mention code, tools, files, or ask what they need."
97
+ )
98
+
99
+ _CLASSIFIER_MODEL_ENV = "GEMCODE_INTENT_MODEL"
100
+ _DEFAULT_CLASSIFIER_MODEL = "gemini-2.5-flash-lite"
101
+
102
+ # Single-word / very-short messages that are unambiguously greetings —
103
+ # checked locally before spending an API call on the classifier.
104
+ _OBVIOUS_GREETINGS: frozenset[str] = frozenset({
105
+ "hi", "hii", "hiii", "hey", "hello", "heya", "hiya", "howdy", "sup", "yo",
106
+ "thanks", "thank you", "thx", "ty", "thankyou",
107
+ "cool", "nice", "great", "awesome", "ok", "okay", "k",
108
+ "bye", "goodbye", "cya", "later",
109
+ "good morning", "good evening", "good night", "good afternoon",
110
+ "what's up", "whats up", "wassup",
111
+ })
112
+
113
+
114
+ def _classifier_enabled() -> bool:
115
+ v = os.environ.get("GEMCODE_INTENT_CLASSIFY_ENABLED", "1")
116
+ return v.lower() not in ("0", "false", "no", "off")
117
+
118
+
119
+ def _get_classifier_model() -> str:
120
+ return os.environ.get(_CLASSIFIER_MODEL_ENV) or _DEFAULT_CLASSIFIER_MODEL
121
+
122
+
123
+ def _get_api_key() -> str:
124
+ return (
125
+ os.environ.get("GOOGLE_API_KEY")
126
+ or os.environ.get("GEMINI_API_KEY")
127
+ or ""
128
+ )
129
+
130
+
131
+ async def classify_intent_with_source(message: str) -> tuple[str, str]:
132
+ """
133
+ Classify a user message; return (intent, source).
134
+
135
+ ``source`` is one of: ``SOURCE_LOCAL`` (heuristic, no API call),
136
+ ``SOURCE_LLM`` (classifier model), ``SOURCE_OFF`` (classifier disabled).
137
+ """
138
+ if not _classifier_enabled():
139
+ return INTENT_ENGINEERING_TASK, SOURCE_OFF
140
+
141
+ stripped = (message or "").strip()
142
+ if not stripped:
143
+ return INTENT_GREETING, SOURCE_LOCAL
144
+
145
+ # Fast local check for unambiguously short greetings — saves an API round-trip.
146
+ lower = stripped.lower()
147
+ if lower in _OBVIOUS_GREETINGS or (len(lower) <= 3 and lower.isalpha()):
148
+ return INTENT_GREETING, SOURCE_LOCAL
149
+
150
+ try:
151
+ import google.genai as genai
152
+ from google.genai import types as gtypes
153
+
154
+ client = genai.Client(api_key=_get_api_key())
155
+ resp = await client.aio.models.generate_content(
156
+ model=_get_classifier_model(),
157
+ contents=_CLASSIFY_PROMPT.format(message=stripped[:600]),
158
+ config=gtypes.GenerateContentConfig(
159
+ temperature=0.0,
160
+ max_output_tokens=15,
161
+ ),
162
+ )
163
+ label = (resp.text or "").strip().upper()
164
+ # Exact match first
165
+ if label in _VALID_INTENTS:
166
+ return label, SOURCE_LLM
167
+ # Partial match (model may return extra punctuation or lowercase)
168
+ for key in _VALID_INTENTS:
169
+ if key in label:
170
+ return key, SOURCE_LLM
171
+ return INTENT_ENGINEERING_TASK, SOURCE_LLM
172
+ except Exception:
173
+ return INTENT_ENGINEERING_TASK, SOURCE_LLM
174
+
175
+
176
+ async def classify_intent(message: str) -> str:
177
+ """
178
+ Classify a user message using a lightweight Gemini call.
179
+
180
+ Returns one of: GREETING, CONCEPT, PROJECT_QUESTION, ENGINEERING_TASK, ANALYSIS.
181
+ Falls back to ENGINEERING_TASK on any error (the main agent handles all real
182
+ tasks safely with that default).
183
+
184
+ Classification is disabled when GEMCODE_INTENT_CLASSIFY_ENABLED=0.
185
+ """
186
+ intent, _ = await classify_intent_with_source(message)
187
+ return intent
188
+
189
+
190
+ async def generate_greeting_reply(message: str) -> str:
191
+ """
192
+ Generate a warm, natural one-sentence reply for a greeting message.
193
+
194
+ Uses the same lightweight classifier model so the response is instant.
195
+ Falls back to a generic reply on any error.
196
+ """
197
+ try:
198
+ import google.genai as genai
199
+ from google.genai import types as gtypes
200
+
201
+ client = genai.Client(api_key=_get_api_key())
202
+ resp = await client.aio.models.generate_content(
203
+ model=_get_classifier_model(),
204
+ contents=[
205
+ gtypes.Content(
206
+ role="user",
207
+ parts=[gtypes.Part(text=_GREETING_SYSTEM)],
208
+ ),
209
+ gtypes.Content(
210
+ role="model",
211
+ parts=[gtypes.Part(text="Got it.")],
212
+ ),
213
+ gtypes.Content(
214
+ role="user",
215
+ parts=[gtypes.Part(text=message)],
216
+ ),
217
+ ],
218
+ config=gtypes.GenerateContentConfig(
219
+ temperature=0.8,
220
+ max_output_tokens=80,
221
+ ),
222
+ )
223
+ text = (resp.text or "").strip()
224
+ return text or "Hey! What can I help you with today?"
225
+ except Exception:
226
+ return "Hey! What can I help you with today?"
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
  _PRICING_TABLE: dict[str, tuple[float, float]] = {
17
17
  # ── Gemini 2.5 ──────────────────────────────────────────────────────────
18
18
  "gemini-2.5-pro": (3.50, 10.50), # standard context ≤200k
19
+ "gemini-2.5-flash-lite": (0.10, 0.40), # lowest-cost 2.5 (must precede flash)
19
20
  "gemini-2.5-flash": (0.15, 0.60), # standard context
20
21
  "gemini-2.5-flash-8b": (0.037, 0.15), # 8B lite variant
21
22
  # ── Gemini 2.0 ──────────────────────────────────────────────────────────
@@ -515,6 +515,60 @@ 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
+ # gemini-2.5-flash-lite classifies the message (same lane as Thinking)
520
+ try:
521
+ from gemcode.intent_classifier import (
522
+ classify_intent_with_source,
523
+ generate_greeting_reply,
524
+ format_intent_thinking_line,
525
+ INTENT_GREETING,
526
+ INTENT_DESCRIPTIONS,
527
+ )
528
+ _intent, _intent_src = await classify_intent_with_source(prompt)
529
+ _intent_line = format_intent_thinking_line(_intent, _intent_src)
530
+ if _intent_line:
531
+ print(
532
+ f" \u23bf {ansi.dim}\u2234 Intent {_intent_line}{ansi.reset}"
533
+ )
534
+ print("")
535
+
536
+ if _intent == INTENT_GREETING:
537
+ _start_anim("Replying\u2026")
538
+ try:
539
+ _reply = await generate_greeting_reply(prompt)
540
+ finally:
541
+ _stop_anim()
542
+ print(f" \u23bf {ansi.bold}GemCode{ansi.reset}:")
543
+ console.print(_RichPadding(_RichMarkdown(_reply), (0, 0, 0, 4)))
544
+ print("")
545
+ if os.environ.get("GEMCODE_TUI_TURN_RULE", "1").lower() in (
546
+ "1", "true", "yes", "on"
547
+ ):
548
+ print(f"{ansi.dim}{_hr(ch='─')}{ansi.reset}")
549
+ print("")
550
+ continue
551
+
552
+ # Non-greeting: store classified intent in session state so the main
553
+ # agent can read it and adapt its tool-use strategy accordingly.
554
+ try:
555
+ ss = runner.session_service
556
+ app = getattr(runner, "app_name", None) or getattr(cfg, "app_name", "gemcode")
557
+ sess = await ss.get_session(
558
+ app_name=app, user_id="local", session_id=current_session_id
559
+ )
560
+ if sess is not None:
561
+ _desc = INTENT_DESCRIPTIONS.get(_intent, _intent)
562
+ sess.state["_gemcode_intent"] = _intent
563
+ sess.state["_gemcode_intent_desc"] = _desc
564
+ await ss.update_session(sess)
565
+ except Exception:
566
+ pass # State injection is best-effort
567
+
568
+ except Exception:
569
+ pass # Classifier unavailable — fall through to main agent unchanged
570
+ # ─────────────────────────────────────────────────────────────────────────
571
+
518
572
  # Snapshot pre-turn capability state so we can detect routing-triggered changes.
519
573
  _pre_dr = cfg.enable_deep_research
520
574
  _pre_emb = cfg.enable_embeddings
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.39
3
+ Version: 0.3.41
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -17,6 +17,7 @@ src/gemcode/context_warning.py
17
17
  src/gemcode/credentials.py
18
18
  src/gemcode/hitl_session.py
19
19
  src/gemcode/hooks.py
20
+ src/gemcode/intent_classifier.py
20
21
  src/gemcode/interactions.py
21
22
  src/gemcode/invoke.py
22
23
  src/gemcode/kairos_daemon.py
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