gemcode 0.3.97__py3-none-any.whl → 0.3.99__py3-none-any.whl
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/invoke.py +26 -8
- gemcode/multimodal_input.py +18 -0
- gemcode/session_summariser.py +53 -13
- {gemcode-0.3.97.dist-info → gemcode-0.3.99.dist-info}/METADATA +1 -1
- {gemcode-0.3.97.dist-info → gemcode-0.3.99.dist-info}/RECORD +9 -9
- {gemcode-0.3.97.dist-info → gemcode-0.3.99.dist-info}/WHEEL +0 -0
- {gemcode-0.3.97.dist-info → gemcode-0.3.99.dist-info}/entry_points.txt +0 -0
- {gemcode-0.3.97.dist-info → gemcode-0.3.99.dist-info}/licenses/LICENSE +0 -0
- {gemcode-0.3.97.dist-info → gemcode-0.3.99.dist-info}/top_level.txt +0 -0
gemcode/invoke.py
CHANGED
|
@@ -184,15 +184,33 @@ async def run_turn(
|
|
|
184
184
|
# user-granted "Files and Folders" permissions on first access.
|
|
185
185
|
# If approved once, we don't re-prompt for the rest of this session.
|
|
186
186
|
attach_allow = True
|
|
187
|
-
if
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
187
|
+
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
|
|
188
|
+
# Default-on: attachments can read any local file path (not workspace-scoped),
|
|
189
|
+
# but we ask once per session so the user is in control and macOS can trigger
|
|
190
|
+
# its permission prompt at the moment we attempt the read.
|
|
191
|
+
attach_allow = os.environ.get("GEMCODE_ATTACHMENTS_ASK", "1").lower() not in (
|
|
192
|
+
"0",
|
|
193
|
+
"false",
|
|
194
|
+
"no",
|
|
195
|
+
"off",
|
|
196
|
+
)
|
|
197
|
+
if cfg is not None:
|
|
198
|
+
# If user already approved earlier in this session, don't prompt again.
|
|
199
|
+
if bool(getattr(cfg, "_attachments_allowed", False)):
|
|
200
|
+
attach_allow = True
|
|
201
|
+
# If yes-to-all is enabled, auto-allow attachments.
|
|
202
|
+
elif bool(getattr(cfg, "yes_to_all", False)):
|
|
203
|
+
attach_allow = True
|
|
195
204
|
object.__setattr__(cfg, "_attachments_allowed", True)
|
|
205
|
+
elif attach_allow:
|
|
206
|
+
attach_allow = _prompt_yes_no(
|
|
207
|
+
"Allow GemCode to read and upload the attached file(s) from disk? (y/n) "
|
|
208
|
+
)
|
|
209
|
+
if attach_allow:
|
|
210
|
+
object.__setattr__(cfg, "_attachments_allowed", True)
|
|
211
|
+
else:
|
|
212
|
+
# Non-interactive sessions can't prompt; default to allow.
|
|
213
|
+
attach_allow = True
|
|
196
214
|
effective_attachments = attachment_paths if attach_allow else None
|
|
197
215
|
|
|
198
216
|
root = cfg.project_root if cfg is not None else Path.cwd()
|
gemcode/multimodal_input.py
CHANGED
|
@@ -97,6 +97,18 @@ def _infer_mime(path: Path, data: bytes) -> tuple[str, list[str]]:
|
|
|
97
97
|
return "application/octet-stream", warnings
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def _is_supported_mime(m: str) -> bool:
|
|
101
|
+
mm = (m or "").strip().lower()
|
|
102
|
+
if not mm:
|
|
103
|
+
return False
|
|
104
|
+
if mm.startswith(("image/", "audio/", "video/", "text/")):
|
|
105
|
+
return True
|
|
106
|
+
if mm == "application/pdf":
|
|
107
|
+
return True
|
|
108
|
+
# Most other application/* types are rejected by Gemini file parts.
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
100
112
|
def build_user_content(
|
|
101
113
|
prompt: str,
|
|
102
114
|
attachment_paths: Sequence[Path | str] | None,
|
|
@@ -135,6 +147,12 @@ def build_user_content(
|
|
|
135
147
|
continue
|
|
136
148
|
mime, mw = _infer_mime(p, data)
|
|
137
149
|
warnings.extend(mw)
|
|
150
|
+
if not _is_supported_mime(mime) or mime == "application/octet-stream":
|
|
151
|
+
warnings.append(
|
|
152
|
+
f"unsupported attachment type for {p} (mime={mime}); "
|
|
153
|
+
"skipping (export to PDF/image/text if needed)"
|
|
154
|
+
)
|
|
155
|
+
continue
|
|
138
156
|
parts.append(types.Part(inline_data=types.Blob(data=data, mime_type=mime)))
|
|
139
157
|
|
|
140
158
|
text = (prompt or "").strip() or (
|
gemcode/session_summariser.py
CHANGED
|
@@ -100,16 +100,51 @@ def _build_prompt(transcript_lines: list[str], *, focus: str = "") -> str:
|
|
|
100
100
|
)
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
def
|
|
103
|
+
def _extract_json_object(text: str) -> dict[str, Any] | None:
|
|
104
|
+
"""
|
|
105
|
+
Best-effort parse:
|
|
106
|
+
- strict JSON object
|
|
107
|
+
- JSON object embedded in extra text (common model behavior)
|
|
108
|
+
"""
|
|
109
|
+
raw = (text or "").strip()
|
|
110
|
+
if not raw:
|
|
111
|
+
return None
|
|
112
|
+
try:
|
|
113
|
+
obj = json.loads(raw)
|
|
114
|
+
return obj if isinstance(obj, dict) else None
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# Try to recover embedded JSON: find the largest {...} span.
|
|
119
|
+
start = raw.find("{")
|
|
120
|
+
end = raw.rfind("}")
|
|
121
|
+
if start == -1 or end == -1 or end <= start:
|
|
122
|
+
return None
|
|
123
|
+
candidate = raw[start : end + 1].strip()
|
|
124
|
+
try:
|
|
125
|
+
obj = json.loads(candidate)
|
|
126
|
+
return obj if isinstance(obj, dict) else None
|
|
127
|
+
except Exception:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _call_summary_model(*, model: str, prompt: str) -> tuple[dict[str, Any] | None, str]:
|
|
104
132
|
api_key = os.environ.get("GOOGLE_API_KEY")
|
|
105
133
|
if not api_key:
|
|
106
134
|
raise RuntimeError("GOOGLE_API_KEY not set")
|
|
107
135
|
|
|
108
136
|
client = Client(api_key=api_key)
|
|
137
|
+
cfg = types.GenerateContentConfig(temperature=0.2)
|
|
138
|
+
# Prefer structured JSON output when supported by the SDK/model.
|
|
139
|
+
try:
|
|
140
|
+
setattr(cfg, "response_mime_type", "application/json")
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
109
144
|
resp = client.models.generate_content(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
145
|
+
model=model,
|
|
146
|
+
contents=[types.Content(role="user", parts=[types.Part(text=prompt)])],
|
|
147
|
+
config=cfg,
|
|
113
148
|
)
|
|
114
149
|
|
|
115
150
|
out_parts: list[str] = []
|
|
@@ -127,14 +162,7 @@ def _call_summary_model(*, model: str, prompt: str) -> dict[str, Any]:
|
|
|
127
162
|
text = "".join(out_parts).strip()
|
|
128
163
|
if not text:
|
|
129
164
|
raise RuntimeError("session summariser returned empty text")
|
|
130
|
-
|
|
131
|
-
try:
|
|
132
|
-
data = json.loads(text)
|
|
133
|
-
except Exception as e:
|
|
134
|
-
raise RuntimeError(f"session summariser returned invalid JSON: {e}") from e
|
|
135
|
-
if not isinstance(data, dict):
|
|
136
|
-
raise RuntimeError("session summariser returned non-object JSON")
|
|
137
|
-
return data
|
|
165
|
+
return _extract_json_object(text), text
|
|
138
166
|
|
|
139
167
|
|
|
140
168
|
def summarise_session(
|
|
@@ -149,7 +177,18 @@ def summarise_session(
|
|
|
149
177
|
return {"error": "session transcript is empty", "session_id": session_id}
|
|
150
178
|
|
|
151
179
|
prompt = _build_prompt(transcript_lines, focus=focus)
|
|
152
|
-
data = _call_summary_model(model=model, prompt=prompt)
|
|
180
|
+
data, raw_text = _call_summary_model(model=model, prompt=prompt)
|
|
181
|
+
if data is None:
|
|
182
|
+
# Graceful fallback: save raw text as summary_markdown so /summarise never hard-fails
|
|
183
|
+
# due to JSON formatting drift.
|
|
184
|
+
data = {
|
|
185
|
+
"title": f"Session {session_id[:8]}",
|
|
186
|
+
"summary_markdown": raw_text[:20_000],
|
|
187
|
+
"memory_facts": [],
|
|
188
|
+
"user_facts": [],
|
|
189
|
+
"notes_markdown": "",
|
|
190
|
+
"open_items": [],
|
|
191
|
+
}
|
|
153
192
|
|
|
154
193
|
title = str(data.get("title") or f"Session {session_id[:8]}").strip()[:120]
|
|
155
194
|
summary_markdown = str(data.get("summary_markdown") or "").strip()
|
|
@@ -219,6 +258,7 @@ def summarise_session(
|
|
|
219
258
|
"summary_path": str(out_path),
|
|
220
259
|
"title": title,
|
|
221
260
|
"summary_markdown": summary_markdown,
|
|
261
|
+
"used_json": bool(data is not None and _extract_json_object(raw_text) is not None),
|
|
222
262
|
"memory_facts_saved": saved_memory,
|
|
223
263
|
"user_facts_saved": saved_user,
|
|
224
264
|
"notes_status": note_status,
|
|
@@ -21,7 +21,7 @@ gemcode/ide_protocol.py,sha256=WJO4KdwyxjQcH1O_vTn7SPuy1ZZMm0eC8_xRLA9RYQo,2108
|
|
|
21
21
|
gemcode/ide_stdio.py,sha256=qDZ8qCR0kWipvyxLJ3tbZfAXChZtosi46dLeNuMejFk,11066
|
|
22
22
|
gemcode/intent_classifier.py,sha256=YfRVEe8gHeKlRkjuSWef1bZ0MPBwyYMp5jymP5Vig5U,8507
|
|
23
23
|
gemcode/interactions.py,sha256=B0b3QNE_I2i5_HtiebX4ehhjlc4Nbqjf_XbvcTLyJT0,641
|
|
24
|
-
gemcode/invoke.py,sha256=
|
|
24
|
+
gemcode/invoke.py,sha256=CpDnz3v8NdWPV9JTJVfh2nzkIGN7I7ihIw452QJhEcI,12694
|
|
25
25
|
gemcode/kaira_daemon.py,sha256=Bzkpc96HocfYAV9D5skid_Gi4bJDOLgO5YlD8vbTgyY,6960
|
|
26
26
|
gemcode/learning.py,sha256=o4Ivczm626NPRiNbSEb7-RvKJMefnv0ZpYt4UB2C3JA,3856
|
|
27
27
|
gemcode/limits.py,sha256=3j6N8V643X7-nP-cAIf37Xg9bkGpQlEJB3nPptApQWk,2504
|
|
@@ -31,7 +31,7 @@ gemcode/mcp_loader.py,sha256=alipHTl5aA7ZCPG6Rq9cyy3UZLsdCra0CETb_fRJl_k,4964
|
|
|
31
31
|
gemcode/modality_tools.py,sha256=L2U756l0YM4SOUx5YxcKEkjSF1QOGzFOSE4o3U0q-4Y,7213
|
|
32
32
|
gemcode/model_errors.py,sha256=j1nb1dopJyZ6MQQvuuADBqvmcqdL80kQWACuWKMkP4Q,4185
|
|
33
33
|
gemcode/model_routing.py,sha256=_8mnXNwxMPA8wAfl-Yx5lWNgjhyWYTCCCMGlqdGAw90,5174
|
|
34
|
-
gemcode/multimodal_input.py,sha256=
|
|
34
|
+
gemcode/multimodal_input.py,sha256=eLFNj_yAykvoTMyiv0hGUXMgb6xlN4pyZ6X--swhnw0,5015
|
|
35
35
|
gemcode/openapi_loader.py,sha256=g_NZD8YL9_9iIJJ9qykhdbBrylJ1195A4FyHGC0mroc,4157
|
|
36
36
|
gemcode/output_styles.py,sha256=6ZgbEsEM5qbdGDZMuRFRHAmHCoKpi53SqPqXZ2sfbS4,2189
|
|
37
37
|
gemcode/paths.py,sha256=UQp4R4sUBv7HsM2OVoGlPxyOIOQZE5wpY53G6nHpB5Q,2671
|
|
@@ -47,7 +47,7 @@ gemcode/review_agent.py,sha256=4t7_5-aE60b4-EheJ_eSB_H2eQYf9GppKoui6jw0TME,5264
|
|
|
47
47
|
gemcode/rules.py,sha256=Itg02VpifOo6jqGj5xwna_ahaPPb0OVtaeR2cNI0pLE,3018
|
|
48
48
|
gemcode/session_runtime.py,sha256=MGOWWUz4ZUnWkuaYkc5EZ_uYYIvLjJc1N35X2GUX79k,23489
|
|
49
49
|
gemcode/session_store.py,sha256=POUT_QQf715c74jbXj0s5vCd4dlAgJz_CLsIWuEUoO0,6051
|
|
50
|
-
gemcode/session_summariser.py,sha256=
|
|
50
|
+
gemcode/session_summariser.py,sha256=ixRJVGbgWGWP-tT8hGLEqBlZ3uMXJlwQdbFsYfwF4hk,8370
|
|
51
51
|
gemcode/skills.py,sha256=nnrzYUCiuEkU_i57p_jJpPHRfw1t2t2EA3pJHNqvpzw,12554
|
|
52
52
|
gemcode/slash_commands.py,sha256=bcD-S_H7p7AlTli6g2dLPPG46HejPje0Imb3ScDTCaQ,798
|
|
53
53
|
gemcode/thinking.py,sha256=-1TVkOMG-7CSQN0Mc18EqINkUxWOMBgeTlF7CX9zYL4,4641
|
|
@@ -107,9 +107,9 @@ gemcode/web/__init__.py,sha256=EysmUAWs6g-lmMk4VFljKfaHVrEgb_FiIzwQmBdORJc,40
|
|
|
107
107
|
gemcode/web/sse_adapter.py,sha256=fXhKxn_bdJJUGqlmvkxLNSYL-ZiIZDaLHtQCF_BheRc,7108
|
|
108
108
|
gemcode/web/terminal_repl.py,sha256=fQt895g0qcr6VBhXfv_5b_bsC5zHT5-MO0ysBdgi2Fg,3886
|
|
109
109
|
gemcode/web/web_sse_compat.py,sha256=9A2s-GI7El7AotJqhO263FrLwppCXXkdydZ5EiOQbao,504
|
|
110
|
-
gemcode-0.3.
|
|
111
|
-
gemcode-0.3.
|
|
112
|
-
gemcode-0.3.
|
|
113
|
-
gemcode-0.3.
|
|
114
|
-
gemcode-0.3.
|
|
115
|
-
gemcode-0.3.
|
|
110
|
+
gemcode-0.3.99.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
|
|
111
|
+
gemcode-0.3.99.dist-info/METADATA,sha256=xCMSBtFzQrLEDtt9BawkHs1NfdmLTQbGf0lcJ-RHsWg,17084
|
|
112
|
+
gemcode-0.3.99.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
113
|
+
gemcode-0.3.99.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
|
|
114
|
+
gemcode-0.3.99.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
|
|
115
|
+
gemcode-0.3.99.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|