max-cli 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.
- max_cli/__init__.py +0 -0
- max_cli/common/cache.py +145 -0
- max_cli/common/concurrent.py +83 -0
- max_cli/common/exceptions.py +40 -0
- max_cli/common/logger.py +22 -0
- max_cli/common/logging.py +24 -0
- max_cli/common/retry.py +51 -0
- max_cli/common/utils.py +40 -0
- max_cli/config.py +43 -0
- max_cli/core/ai_engine.py +541 -0
- max_cli/core/file_organizer.py +254 -0
- max_cli/core/image_processor.py +139 -0
- max_cli/core/media_engine.py +681 -0
- max_cli/core/network_engine.py +103 -0
- max_cli/core/pdf_engine.py +520 -0
- max_cli/core/system_engine.py +57 -0
- max_cli/interface/cli_ai.py +376 -0
- max_cli/interface/cli_config.py +363 -0
- max_cli/interface/cli_files.py +388 -0
- max_cli/interface/cli_images.py +176 -0
- max_cli/interface/cli_media.py +558 -0
- max_cli/interface/cli_network.py +174 -0
- max_cli/interface/cli_pdf.py +651 -0
- max_cli/interface/cli_tools.py +60 -0
- max_cli/main.py +91 -0
- max_cli/plugins/__init__.py +4 -0
- max_cli/plugins/base.py +39 -0
- max_cli/plugins/manager.py +81 -0
- max_cli-0.2.0.dist-info/METADATA +632 -0
- max_cli-0.2.0.dist-info/RECORD +34 -0
- max_cli-0.2.0.dist-info/WHEEL +5 -0
- max_cli-0.2.0.dist-info/entry_points.txt +2 -0
- max_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- max_cli-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import typer
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Any, List, Optional
|
|
6
|
+
from openai import OpenAI
|
|
7
|
+
from max_cli.config import settings
|
|
8
|
+
from max_cli.common.exceptions import MaxError
|
|
9
|
+
from max_cli.common.utils import encode_image_to_base64
|
|
10
|
+
from max_cli.common.cache import get_default_cache
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AIEngine:
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.client = None
|
|
16
|
+
if settings.OPENAI_API_KEY:
|
|
17
|
+
self.client = OpenAI(
|
|
18
|
+
api_key=settings.OPENAI_API_KEY, base_url=settings.OPENAI_BASE_URL
|
|
19
|
+
)
|
|
20
|
+
self.history: List[Dict[str, str]] = []
|
|
21
|
+
self._history_file = Path.home() / ".max_cli" / "chat_history.json"
|
|
22
|
+
self._history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
self._load_history()
|
|
24
|
+
|
|
25
|
+
def _load_history(self) -> None:
|
|
26
|
+
"""Load conversation history from disk."""
|
|
27
|
+
if self._history_file.exists():
|
|
28
|
+
try:
|
|
29
|
+
data = json.loads(self._history_file.read_text(encoding="utf-8"))
|
|
30
|
+
self.history = data.get("history", [])
|
|
31
|
+
except Exception:
|
|
32
|
+
self.history = []
|
|
33
|
+
|
|
34
|
+
def _save_history(self) -> None:
|
|
35
|
+
"""Save conversation history to disk."""
|
|
36
|
+
data = {"history": self.history}
|
|
37
|
+
self._history_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
def clear_history(self) -> None:
|
|
40
|
+
"""Clear conversation history from memory and disk."""
|
|
41
|
+
self.history = []
|
|
42
|
+
if self._history_file.exists():
|
|
43
|
+
self._history_file.unlink()
|
|
44
|
+
|
|
45
|
+
def export_history(self, output_path: Path) -> None:
|
|
46
|
+
"""Export conversation history to a JSON file."""
|
|
47
|
+
data = {"history": self.history, "exported_at": str(Path.cwd())}
|
|
48
|
+
output_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
49
|
+
|
|
50
|
+
def import_history(self, input_path: Path) -> None:
|
|
51
|
+
"""Import conversation history from a JSON file."""
|
|
52
|
+
data = json.loads(input_path.read_text(encoding="utf-8"))
|
|
53
|
+
self.history = data.get("history", [])
|
|
54
|
+
self._save_history()
|
|
55
|
+
|
|
56
|
+
def get_suggestions(self) -> List[str]:
|
|
57
|
+
"""Get context-aware suggestions based on conversation history."""
|
|
58
|
+
if not self.history or not self.client:
|
|
59
|
+
return [
|
|
60
|
+
"Help me organize my files",
|
|
61
|
+
"Compress these images",
|
|
62
|
+
"Extract audio from this video",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
recent_topics = " ".join(
|
|
66
|
+
[msg["content"] for msg in self.history[-4:] if msg.get("role") == "user"]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
prompt = f"""Based on this conversation context: "{recent_topics}"
|
|
70
|
+
|
|
71
|
+
Suggest 3 relevant follow-up commands the user might want to run.
|
|
72
|
+
Keep suggestions brief and related to file management, media processing, or AI features.
|
|
73
|
+
Return as a JSON array of strings."""
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
response = self.client.chat.completions.create(
|
|
77
|
+
model=settings.AI_MODEL,
|
|
78
|
+
messages=[{"role": "user", "content": prompt}],
|
|
79
|
+
)
|
|
80
|
+
result = json.loads(response.choices[0].message.content)
|
|
81
|
+
return result if isinstance(result, list) else []
|
|
82
|
+
except Exception:
|
|
83
|
+
return [
|
|
84
|
+
"Show me what you can do",
|
|
85
|
+
"Help me with files",
|
|
86
|
+
"Process some media",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
def _get_local_context(self) -> str:
|
|
90
|
+
"""Scans current directory to give AI 'eyes'."""
|
|
91
|
+
try:
|
|
92
|
+
files = os.listdir(".")
|
|
93
|
+
# Filter for non-hidden files and limit to 30 for token safety
|
|
94
|
+
visible_files = [f for f in files if not f.startswith(".")][:30]
|
|
95
|
+
|
|
96
|
+
context = "\n[USER'S CURRENT ENVIRONMENT]\n"
|
|
97
|
+
context += f"Path: {os.getcwd()}\n"
|
|
98
|
+
context += f"Files in Folder: {', '.join(visible_files)}\n"
|
|
99
|
+
if len(files) > 30:
|
|
100
|
+
context += f"(...and {len(files) - 30} more files)\n"
|
|
101
|
+
return context
|
|
102
|
+
except Exception:
|
|
103
|
+
return ""
|
|
104
|
+
|
|
105
|
+
def generate_cli_schema(self, app: typer.Typer, parent_name: str = "max") -> str:
|
|
106
|
+
"""
|
|
107
|
+
Dynamically traverses the Typer app to build a documentation string.
|
|
108
|
+
"""
|
|
109
|
+
schema_lines = ["Available Commands:"]
|
|
110
|
+
|
|
111
|
+
# 1. Traverse Registered Groups (Sub-commands like 'max images ...')
|
|
112
|
+
# Typer stores these in .registered_groups
|
|
113
|
+
for group in app.registered_groups:
|
|
114
|
+
if group.hidden or not group.typer_instance:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
group_name = group.name
|
|
118
|
+
|
|
119
|
+
# Access the commands inside the sub-typer
|
|
120
|
+
for cmd_info in group.typer_instance.registered_commands:
|
|
121
|
+
if cmd_info.hidden:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
full_cmd = f"{parent_name} {group_name} {cmd_info.name}"
|
|
125
|
+
description = cmd_info.help or "No description provided."
|
|
126
|
+
|
|
127
|
+
# We strip newlines to keep the prompt clean
|
|
128
|
+
description = description.split("\n")[0]
|
|
129
|
+
|
|
130
|
+
schema_lines.append(f"- {full_cmd}: {description}")
|
|
131
|
+
|
|
132
|
+
return "\n".join(schema_lines)
|
|
133
|
+
|
|
134
|
+
def interpret_intent(
|
|
135
|
+
self, user_prompt: str, app_instance: Any, explain: bool = False
|
|
136
|
+
) -> Dict[str, Any]:
|
|
137
|
+
"""Translates natural language to CLI commands with local context."""
|
|
138
|
+
if not self.client:
|
|
139
|
+
raise MaxError(
|
|
140
|
+
"Missing AI Configuration.\n"
|
|
141
|
+
"Please set OPENAI_API_KEY in your .env file."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
tools = (
|
|
145
|
+
self.generate_cli_schema(app_instance)
|
|
146
|
+
if hasattr(app_instance, "registered_groups")
|
|
147
|
+
else ""
|
|
148
|
+
)
|
|
149
|
+
context = self._get_local_context()
|
|
150
|
+
|
|
151
|
+
# UPDATED PROMPT: Tell Max he CAN chat, but must use the JSON structure.
|
|
152
|
+
system_msg = f"""
|
|
153
|
+
You are "Max", a CLI agent.
|
|
154
|
+
TOOLS: {tools}
|
|
155
|
+
{context}
|
|
156
|
+
|
|
157
|
+
INSTRUCTIONS:
|
|
158
|
+
1. If the user asks a tool-related question, return the "command".
|
|
159
|
+
2. If the user is just chatting (e.g., "hello", "who are you?"), use the "thought" field for your response and leave "command" as null.
|
|
160
|
+
3. ALWAYS return a JSON object. No markdown. No outside text.
|
|
161
|
+
|
|
162
|
+
JSON STRUCTURE:
|
|
163
|
+
{{
|
|
164
|
+
"thought": "Your conversational response or reasoning",
|
|
165
|
+
"command": "The shell command or null",
|
|
166
|
+
"explanation": "Briefly explain what the flags do (only if requested)",
|
|
167
|
+
"dangerous": true/false
|
|
168
|
+
}}
|
|
169
|
+
|
|
170
|
+
If the request is unrelated to the tools or ambiguous, return:
|
|
171
|
+
{{ "error": "I cannot handle this request with current tools. (and you have to explain the reason why not)" }}
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
messages = [{"role": "system", "content": system_msg}]
|
|
176
|
+
messages.extend(self.history)
|
|
177
|
+
messages.append({"role": "user", "content": user_prompt})
|
|
178
|
+
|
|
179
|
+
response = self.client.chat.completions.create(
|
|
180
|
+
model=settings.AI_MODEL,
|
|
181
|
+
messages=messages,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
raw_content = response.choices[0].message.content
|
|
185
|
+
|
|
186
|
+
# --- SAFETY CATCH ---
|
|
187
|
+
try:
|
|
188
|
+
result = json.loads(raw_content)
|
|
189
|
+
except json.JSONDecodeError:
|
|
190
|
+
# If AI fails to send JSON, wrap its text into a result dict manually
|
|
191
|
+
result = {
|
|
192
|
+
"thought": raw_content.strip(),
|
|
193
|
+
"command": None,
|
|
194
|
+
"dangerous": False,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Update history with the response
|
|
198
|
+
self.history.append({"role": "user", "content": user_prompt})
|
|
199
|
+
self.history.append(
|
|
200
|
+
{"role": "assistant", "content": result.get("thought", "")}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return result
|
|
204
|
+
except Exception as e:
|
|
205
|
+
raise MaxError(f"AI Interpretation Error: {e}")
|
|
206
|
+
|
|
207
|
+
def categorize_files(self, file_list: List[str]) -> Dict[str, str]:
|
|
208
|
+
"""AI-powered semantic grouping of files."""
|
|
209
|
+
cache = get_default_cache()
|
|
210
|
+
cache_key = f"categorize:{','.join(sorted(file_list))}"
|
|
211
|
+
cached_result = cache.get(cache_key)
|
|
212
|
+
if cached_result is not None:
|
|
213
|
+
return cached_result
|
|
214
|
+
|
|
215
|
+
prompt = f"Categorize these files into logical folders (e.g., Invoices, Photos, Scripts). Return a JSON map: {{filename: category_name}}\nFiles: {file_list}"
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
response = self.client.chat.completions.create(
|
|
219
|
+
model=settings.AI_MODEL,
|
|
220
|
+
messages=[{"role": "user", "content": prompt}],
|
|
221
|
+
)
|
|
222
|
+
result = json.loads(response.choices[0].message.content)
|
|
223
|
+
cache.set(cache_key, result, ttl=3600)
|
|
224
|
+
return result
|
|
225
|
+
except Exception:
|
|
226
|
+
return {f: "Other" for f in file_list}
|
|
227
|
+
|
|
228
|
+
def analyze_image_content(self, image_path: Path, prompt: str) -> str:
|
|
229
|
+
"""
|
|
230
|
+
Sends an image + prompt to the Vision Model (Gemini/GPT-4o).
|
|
231
|
+
Returns the text description.
|
|
232
|
+
"""
|
|
233
|
+
if not self.client:
|
|
234
|
+
raise MaxError("Missing AI Configuration. Check your .env file.")
|
|
235
|
+
|
|
236
|
+
# 1. Encode Image
|
|
237
|
+
try:
|
|
238
|
+
base64_image = encode_image_to_base64(image_path)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
raise MaxError(f"Failed to process image: {e}")
|
|
241
|
+
|
|
242
|
+
# 2. Build Payload
|
|
243
|
+
# Note: We do NOT force JSON mode here, as we want natural language description.
|
|
244
|
+
messages = [
|
|
245
|
+
{
|
|
246
|
+
"role": "user",
|
|
247
|
+
"content": [
|
|
248
|
+
{"type": "text", "text": prompt},
|
|
249
|
+
{
|
|
250
|
+
"type": "image_url",
|
|
251
|
+
"image_url": {
|
|
252
|
+
# JPEG header usually works for PNG/WEBP in OpenAI/Gemini APIs
|
|
253
|
+
"url": f"data:image/jpeg;base64,{base64_image}"
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
# 3. Call API
|
|
261
|
+
try:
|
|
262
|
+
response = self.client.chat.completions.create(
|
|
263
|
+
model=settings.AI_MODEL,
|
|
264
|
+
messages=messages,
|
|
265
|
+
)
|
|
266
|
+
return response.choices[0].message.content
|
|
267
|
+
except Exception as e:
|
|
268
|
+
raise MaxError(f"AI Vision Error: {str(e)}")
|
|
269
|
+
|
|
270
|
+
def generate_image(self, prompt: str, model: Optional[str] = None) -> str:
|
|
271
|
+
"""
|
|
272
|
+
Generates an image. Uses the dedicated IMAGE_MODEL by default.
|
|
273
|
+
"""
|
|
274
|
+
if not self.client:
|
|
275
|
+
raise MaxError("AI Client not configured.")
|
|
276
|
+
|
|
277
|
+
# If no specific model override is passed in the command, use the config value
|
|
278
|
+
target_model = model or settings.AI_IMAGE_MODEL
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
response = self.client.chat.completions.create(
|
|
282
|
+
model=target_model, messages=[{"role": "user", "content": prompt}]
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
content = response.choices[0].message.content
|
|
286
|
+
return self._extract_image_url(content, response)
|
|
287
|
+
except Exception as e:
|
|
288
|
+
raise MaxError(f"Image Generation Failed using {target_model}: {e}")
|
|
289
|
+
|
|
290
|
+
def edit_image(
|
|
291
|
+
self, image_path: Path, prompt: str, model: Optional[str] = None
|
|
292
|
+
) -> str:
|
|
293
|
+
"""
|
|
294
|
+
Edits an image. Uses the dedicated IMAGE_MODEL by default.
|
|
295
|
+
"""
|
|
296
|
+
if not self.client:
|
|
297
|
+
raise MaxError("AI Client not configured.")
|
|
298
|
+
|
|
299
|
+
target_model = model or settings.AI_IMAGE_MODEL
|
|
300
|
+
base64_img = encode_image_to_base64(image_path)
|
|
301
|
+
|
|
302
|
+
messages = [
|
|
303
|
+
{
|
|
304
|
+
"role": "user",
|
|
305
|
+
"content": [
|
|
306
|
+
{"type": "text", "text": prompt},
|
|
307
|
+
{
|
|
308
|
+
"type": "image_url",
|
|
309
|
+
"image_url": {"url": f"data:image/jpeg;base64,{base64_img}"},
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
response = self.client.chat.completions.create(
|
|
317
|
+
model=target_model, messages=messages
|
|
318
|
+
)
|
|
319
|
+
content = response.choices[0].message.content
|
|
320
|
+
return self._extract_image_url(content, response)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
raise MaxError(f"Image Editing Failed using {target_model}: {e}")
|
|
323
|
+
|
|
324
|
+
def _extract_image_url(self, content: str, raw_response: Any) -> str:
|
|
325
|
+
"""
|
|
326
|
+
Helper to find image URL in Nano Banana response.
|
|
327
|
+
"""
|
|
328
|
+
# 1. Check for standard Markdown URL: 
|
|
329
|
+
import re
|
|
330
|
+
|
|
331
|
+
match = re.search(r"\((https?://[^\s)]+)\)", content)
|
|
332
|
+
if match:
|
|
333
|
+
return match.group(1)
|
|
334
|
+
|
|
335
|
+
# 2. Check for raw URL in text
|
|
336
|
+
url_match = re.search(r"https?://[^\s]+", content)
|
|
337
|
+
if url_match:
|
|
338
|
+
return url_match.group(0)
|
|
339
|
+
|
|
340
|
+
# 3. Check raw response dictionary (Advanced Google implementation)
|
|
341
|
+
raw_dict = raw_response.model_dump()
|
|
342
|
+
if "images" in raw_dict and raw_dict["images"]:
|
|
343
|
+
return raw_dict["images"][0].get("url")
|
|
344
|
+
|
|
345
|
+
raise MaxError("AI generated a response, but no image URL was found.")
|
|
346
|
+
|
|
347
|
+
def run_pipeline(
|
|
348
|
+
self, operations: List[Dict[str, Any]], input_data: Any = None
|
|
349
|
+
) -> List[Dict[str, Any]]:
|
|
350
|
+
"""
|
|
351
|
+
Run a pipeline of AI operations.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
operations: List of operation dicts with 'type' and 'params'
|
|
355
|
+
input_data: Initial input data
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
List of results from each operation
|
|
359
|
+
"""
|
|
360
|
+
if not self.client:
|
|
361
|
+
raise MaxError("AI Client not configured.")
|
|
362
|
+
|
|
363
|
+
current_data = input_data
|
|
364
|
+
results = []
|
|
365
|
+
|
|
366
|
+
for i, op in enumerate(operations):
|
|
367
|
+
op_type = op.get("type", "").lower()
|
|
368
|
+
params = op.get("params", {})
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
if op_type == "categorize":
|
|
372
|
+
files = params.get("files", [])
|
|
373
|
+
result = self.categorize_files(files)
|
|
374
|
+
results.append(
|
|
375
|
+
{"step": i + 1, "operation": "categorize", "result": result}
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
elif op_type == "analyze_image":
|
|
379
|
+
image_path = params.get("image_path")
|
|
380
|
+
prompt = params.get("prompt", "Describe this image")
|
|
381
|
+
if image_path:
|
|
382
|
+
result = self.analyze_image_content(Path(image_path), prompt)
|
|
383
|
+
results.append(
|
|
384
|
+
{
|
|
385
|
+
"step": i + 1,
|
|
386
|
+
"operation": "analyze_image",
|
|
387
|
+
"result": result,
|
|
388
|
+
}
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
elif op_type == "generate_image":
|
|
392
|
+
prompt = params.get("prompt", "")
|
|
393
|
+
if prompt:
|
|
394
|
+
result = self.generate_image(prompt)
|
|
395
|
+
results.append(
|
|
396
|
+
{
|
|
397
|
+
"step": i + 1,
|
|
398
|
+
"operation": "generate_image",
|
|
399
|
+
"result": result,
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
elif op_type == "chat":
|
|
404
|
+
message = params.get("message", "")
|
|
405
|
+
if message:
|
|
406
|
+
result = self.interpret_intent(message, None)
|
|
407
|
+
results.append(
|
|
408
|
+
{"step": i + 1, "operation": "chat", "result": result}
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
elif op_type == "transform":
|
|
412
|
+
transform_prompt = params.get("prompt", "")
|
|
413
|
+
input_text = params.get("input", current_data)
|
|
414
|
+
if transform_prompt and input_text:
|
|
415
|
+
response = self.client.chat.completions.create(
|
|
416
|
+
model=settings.AI_MODEL,
|
|
417
|
+
messages=[
|
|
418
|
+
{
|
|
419
|
+
"role": "user",
|
|
420
|
+
"content": f"{transform_prompt}\n\n{input_text}",
|
|
421
|
+
}
|
|
422
|
+
],
|
|
423
|
+
)
|
|
424
|
+
result = response.choices[0].message.content
|
|
425
|
+
current_data = result
|
|
426
|
+
results.append(
|
|
427
|
+
{"step": i + 1, "operation": "transform", "result": result}
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
else:
|
|
431
|
+
results.append(
|
|
432
|
+
{
|
|
433
|
+
"step": i + 1,
|
|
434
|
+
"operation": op_type,
|
|
435
|
+
"error": f"Unknown operation: {op_type}",
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
except Exception as e:
|
|
440
|
+
results.append({"step": i + 1, "operation": op_type, "error": str(e)})
|
|
441
|
+
|
|
442
|
+
return results
|
|
443
|
+
|
|
444
|
+
def semantic_search(self, query: str, files: List[Path]) -> List[Dict[str, Any]]:
|
|
445
|
+
"""
|
|
446
|
+
Search files by content using AI.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
query: Natural language search query
|
|
450
|
+
files: List of files to search
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
List of matching results with relevance scores
|
|
454
|
+
"""
|
|
455
|
+
if not self.client:
|
|
456
|
+
raise MaxError("AI Client not configured.")
|
|
457
|
+
|
|
458
|
+
results = []
|
|
459
|
+
|
|
460
|
+
for file_path in files:
|
|
461
|
+
try:
|
|
462
|
+
if file_path.suffix.lower() in [
|
|
463
|
+
".txt",
|
|
464
|
+
".md",
|
|
465
|
+
".py",
|
|
466
|
+
".json",
|
|
467
|
+
".yaml",
|
|
468
|
+
".yml",
|
|
469
|
+
]:
|
|
470
|
+
file_content = file_path.read_text(
|
|
471
|
+
encoding="utf-8", errors="ignore"
|
|
472
|
+
)[:5000]
|
|
473
|
+
elif file_path.suffix.lower() == ".pdf":
|
|
474
|
+
continue
|
|
475
|
+
else:
|
|
476
|
+
continue
|
|
477
|
+
|
|
478
|
+
prompt = f"""Search Query: {query}
|
|
479
|
+
|
|
480
|
+
File: {file_path.name}
|
|
481
|
+
|
|
482
|
+
Content:
|
|
483
|
+
{file_content}
|
|
484
|
+
|
|
485
|
+
Does this file match the query? Reply with YES or NO followed by a brief explanation."""
|
|
486
|
+
|
|
487
|
+
response = self.client.chat.completions.create(
|
|
488
|
+
model=settings.AI_MODEL,
|
|
489
|
+
messages=[{"role": "user", "content": prompt}],
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
answer = response.choices[0].message.content or ""
|
|
493
|
+
|
|
494
|
+
if answer.strip().upper().startswith("YES"):
|
|
495
|
+
results.append(
|
|
496
|
+
{"file": str(file_path), "match": True, "reasoning": answer}
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
except Exception:
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
return results
|
|
503
|
+
|
|
504
|
+
def extract_structured_data(
|
|
505
|
+
self, image_path: Path, schema: Dict[str, str]
|
|
506
|
+
) -> Dict[str, Any]:
|
|
507
|
+
"""
|
|
508
|
+
Extract structured data from an image using AI vision.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
image_path: Path to image file
|
|
512
|
+
schema: Dict mapping field names to descriptions
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Extracted structured data
|
|
516
|
+
"""
|
|
517
|
+
if not self.client:
|
|
518
|
+
raise MaxError("AI Client not configured.")
|
|
519
|
+
|
|
520
|
+
schema_text = "\n".join(
|
|
521
|
+
[f"- {field}: {desc}" for field, desc in schema.items()]
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
prompt = f"""Extract structured data from this image.
|
|
525
|
+
|
|
526
|
+
Schema:
|
|
527
|
+
{schema_text}
|
|
528
|
+
|
|
529
|
+
Return a JSON object with the extracted data."""
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
result = self.analyze_image_content(image_path, prompt)
|
|
533
|
+
import json
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
data = json.loads(result)
|
|
537
|
+
return data
|
|
538
|
+
except json.JSONDecodeError:
|
|
539
|
+
return {"raw_text": result, "error": "Could not parse as JSON"}
|
|
540
|
+
except Exception as e:
|
|
541
|
+
raise MaxError(f"Data extraction failed: {e}")
|