quicksnap 0.2.0__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.
quicksnap/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """QuickSnap - Never forget the engineering stories behind your code."""
2
+
3
+ __version__ = "0.2.0"
quicksnap/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import app
2
+
3
+ app()
quicksnap/ai.py ADDED
@@ -0,0 +1,256 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from typing import Optional
6
+
7
+
8
+ def _has_httpx() -> bool:
9
+ try:
10
+ import httpx
11
+ return True
12
+ except ImportError:
13
+ return False
14
+
15
+
16
+ class AIServiceError(Exception):
17
+ def __init__(self, message: str, status_code: int = 0):
18
+ super().__init__(message)
19
+ self.status_code = status_code
20
+
21
+
22
+ def generate_with_local_ai(context: dict, model: str = "llama3") -> Optional[dict]:
23
+ if not _has_httpx():
24
+ return None
25
+ import httpx
26
+
27
+ prompt = _build_prompt(context)
28
+ try:
29
+ resp = httpx.post(
30
+ "http://localhost:11434/api/generate",
31
+ json={"model": model, "prompt": prompt, "stream": False},
32
+ timeout=120,
33
+ )
34
+ if resp.status_code == 200:
35
+ text = resp.json().get("response", "")
36
+ return _parse_ai_response(text)
37
+ return None
38
+ except httpx.ConnectError:
39
+ return None
40
+ except httpx.TimeoutException:
41
+ return None
42
+ except Exception:
43
+ return None
44
+
45
+
46
+ def generate_with_openai(context: dict, model: str = "gpt-4o-mini") -> Optional[dict]:
47
+ if not _has_httpx():
48
+ return None
49
+ import httpx
50
+
51
+ api_key = os.environ.get("OPENAI_API_KEY")
52
+ if not api_key:
53
+ return None
54
+
55
+ prompt = _build_prompt(context)
56
+ try:
57
+ resp = httpx.post(
58
+ "https://api.openai.com/v1/chat/completions",
59
+ headers={"Authorization": f"Bearer {api_key}"},
60
+ json={
61
+ "model": model,
62
+ "messages": [{"role": "user", "content": prompt}],
63
+ "temperature": 0.3,
64
+ },
65
+ timeout=60,
66
+ )
67
+ if resp.status_code == 401:
68
+ raise AIServiceError("Invalid OpenAI API key", 401)
69
+ if resp.status_code == 429:
70
+ raise AIServiceError("OpenAI rate limited", 429)
71
+ resp.raise_for_status()
72
+ text = resp.json()["choices"][0]["message"]["content"]
73
+ return _parse_ai_response(text)
74
+ except AIServiceError:
75
+ raise
76
+ except Exception:
77
+ return None
78
+
79
+
80
+ def generate_with_anthropic(context: dict, model: str = "claude-3-5-sonnet-20241022") -> Optional[dict]:
81
+ if not _has_httpx():
82
+ return None
83
+ import httpx
84
+
85
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
86
+ if not api_key:
87
+ return None
88
+
89
+ prompt = _build_prompt(context)
90
+ try:
91
+ resp = httpx.post(
92
+ "https://api.anthropic.com/v1/messages",
93
+ headers={
94
+ "x-api-key": api_key,
95
+ "anthropic-version": "2023-06-01",
96
+ },
97
+ json={
98
+ "model": model,
99
+ "max_tokens": 2000,
100
+ "messages": [{"role": "user", "content": prompt}],
101
+ },
102
+ timeout=60,
103
+ )
104
+ if resp.status_code == 401:
105
+ raise AIServiceError("Invalid Anthropic API key", 401)
106
+ if resp.status_code == 429:
107
+ raise AIServiceError("Anthropic rate limited", 429)
108
+ resp.raise_for_status()
109
+ text = resp.json()["content"][0]["text"]
110
+ return _parse_ai_response(text)
111
+ except AIServiceError:
112
+ raise
113
+ except Exception:
114
+ return None
115
+
116
+
117
+ def generate_with_gemini(context: dict, model: str = "gemini-1.5-flash") -> Optional[dict]:
118
+ if not _has_httpx():
119
+ return None
120
+ import httpx
121
+
122
+ api_key = os.environ.get("GEMINI_API_KEY")
123
+ if not api_key:
124
+ return None
125
+
126
+ prompt = _build_prompt(context)
127
+ try:
128
+ resp = httpx.post(
129
+ f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent",
130
+ headers={"x-goog-api-key": api_key},
131
+ json={"contents": [{"parts": [{"text": prompt}]}]},
132
+ timeout=60,
133
+ )
134
+ if resp.status_code == 400:
135
+ raise AIServiceError("Invalid Gemini API key or model", 400)
136
+ resp.raise_for_status()
137
+ data = resp.json()
138
+ candidates = data.get("candidates", [])
139
+ if candidates:
140
+ text = candidates[0].get("content", {}).get("parts", [{}])[0].get("text", "")
141
+ return _parse_ai_response(text)
142
+ return None
143
+ except AIServiceError:
144
+ raise
145
+ except Exception:
146
+ return None
147
+
148
+
149
+ def generate_ai_summary(context: dict, mode: str = "auto") -> tuple[Optional[dict], str]:
150
+ try:
151
+ if mode == "local":
152
+ return generate_with_local_ai(context), ""
153
+ elif mode == "openai":
154
+ return generate_with_openai(context), ""
155
+ elif mode == "anthropic":
156
+ return generate_with_anthropic(context), ""
157
+ elif mode == "gemini":
158
+ return generate_with_gemini(context), ""
159
+
160
+ if os.environ.get("OPENAI_API_KEY"):
161
+ return generate_with_openai(context), ""
162
+ if os.environ.get("ANTHROPIC_API_KEY"):
163
+ return generate_with_anthropic(context), ""
164
+ if os.environ.get("GEMINI_API_KEY"):
165
+ return generate_with_gemini(context), ""
166
+ result = generate_with_local_ai(context)
167
+ if result is None:
168
+ return None, "No AI configured. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, or install Ollama."
169
+ return result, ""
170
+ except AIServiceError as e:
171
+ error_map = {
172
+ 401: "Invalid API key. Check your environment variable.",
173
+ 429: "Rate limited. Wait a moment and try again.",
174
+ 400: "Invalid request. Check your API key and model name.",
175
+ }
176
+ msg = error_map.get(e.status_code, f"AI error: {e}")
177
+ return None, msg
178
+ except Exception as e:
179
+ return None, f"Unexpected error: {e}"
180
+
181
+
182
+ def _build_prompt(context: dict) -> str:
183
+ diff = context.get("diff", "")[:3000]
184
+ commits = "\n".join(context.get("commit_messages", []))
185
+ files = "\n".join(context.get("files_changed", []))
186
+
187
+ return f"""You are helping an engineer document their work for future interviews.
188
+
189
+ Git repository: {context.get('repo_name', 'unknown')}
190
+ Branch: {context.get('branch', 'unknown')}
191
+ Files changed: {files}
192
+ Recent commits: {commits}
193
+
194
+ Diff:
195
+ {diff}
196
+
197
+ Generate a JSON response with these fields:
198
+ {{
199
+ "title": "A concise, descriptive title for this change",
200
+ "problem": "What problem was being solved (2-3 sentences)",
201
+ "context": "Technical context and scale",
202
+ "challenge": "What made this difficult",
203
+ "alternatives": ["alternative 1", "alternative 2", "alternative 3"],
204
+ "decision": "Why this approach was chosen",
205
+ "tradeoffs": ["tradeoff 1", "tradeoff 2"],
206
+ "result": "Measurable outcome if possible",
207
+ "lessons": "Key lessons learned",
208
+ "surprises": "What surprised you during this work?",
209
+ "risks_taken": "What was the biggest risk you took?",
210
+ "assumptions": "What assumption did you make that proved correct or incorrect?",
211
+ "almost_failed": "What almost went wrong?",
212
+ "advice": "What advice would you give your future self about this?",
213
+ "tags": ["tag1", "tag2", "tag3"],
214
+ "star": {{
215
+ "situation": "...",
216
+ "task": "...",
217
+ "action": "...",
218
+ "result": "..."
219
+ }},
220
+ "bullets": [
221
+ "Resume bullet point 1",
222
+ "Resume bullet point 2"
223
+ ]
224
+ }}
225
+
226
+ Respond ONLY with valid JSON. Be specific and technical."""
227
+
228
+
229
+ def _parse_ai_response(text: str) -> Optional[dict]:
230
+ text = text.strip()
231
+ if text.startswith("```"):
232
+ lines = text.splitlines()
233
+ lines = lines[1:]
234
+ if lines and lines[-1].strip() == "```":
235
+ lines = lines[:-1]
236
+ text = "\n".join(lines)
237
+
238
+ try:
239
+ result = json.loads(text)
240
+ if isinstance(result, dict):
241
+ return result
242
+ return None
243
+ except json.JSONDecodeError:
244
+ pass
245
+
246
+ start = text.find("{")
247
+ end = text.rfind("}")
248
+ if start != -1 and end != -1 and end > start:
249
+ try:
250
+ result = json.loads(text[start:end + 1])
251
+ if isinstance(result, dict):
252
+ return result
253
+ except json.JSONDecodeError:
254
+ pass
255
+
256
+ return None