@voria/cli 0.0.2
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.
- package/README.md +439 -0
- package/bin/voria +730 -0
- package/docs/ARCHITECTURE.md +419 -0
- package/docs/CHANGELOG.md +189 -0
- package/docs/CONTRIBUTING.md +447 -0
- package/docs/DESIGN_DECISIONS.md +380 -0
- package/docs/DEVELOPMENT.md +535 -0
- package/docs/EXAMPLES.md +434 -0
- package/docs/INSTALL.md +335 -0
- package/docs/IPC_PROTOCOL.md +310 -0
- package/docs/LLM_INTEGRATION.md +416 -0
- package/docs/MODULES.md +470 -0
- package/docs/PERFORMANCE.md +346 -0
- package/docs/PLUGINS.md +432 -0
- package/docs/QUICKSTART.md +184 -0
- package/docs/README.md +133 -0
- package/docs/ROADMAP.md +346 -0
- package/docs/SECURITY.md +334 -0
- package/docs/TROUBLESHOOTING.md +565 -0
- package/docs/USER_GUIDE.md +700 -0
- package/package.json +63 -0
- package/python/voria/__init__.py +8 -0
- package/python/voria/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/__pycache__/engine.cpython-312.pyc +0 -0
- package/python/voria/core/__init__.py +1 -0
- package/python/voria/core/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/__pycache__/setup.cpython-312.pyc +0 -0
- package/python/voria/core/agent/__init__.py +9 -0
- package/python/voria/core/agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/agent/__pycache__/loop.cpython-312.pyc +0 -0
- package/python/voria/core/agent/loop.py +343 -0
- package/python/voria/core/executor/__init__.py +19 -0
- package/python/voria/core/executor/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/executor/__pycache__/executor.cpython-312.pyc +0 -0
- package/python/voria/core/executor/executor.py +431 -0
- package/python/voria/core/github/__init__.py +33 -0
- package/python/voria/core/github/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/github/__pycache__/client.cpython-312.pyc +0 -0
- package/python/voria/core/github/client.py +438 -0
- package/python/voria/core/llm/__init__.py +55 -0
- package/python/voria/core/llm/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/base.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/claude_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/gemini_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/modal_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/model_discovery.cpython-312.pyc +0 -0
- package/python/voria/core/llm/__pycache__/openai_provider.cpython-312.pyc +0 -0
- package/python/voria/core/llm/base.py +152 -0
- package/python/voria/core/llm/claude_provider.py +188 -0
- package/python/voria/core/llm/gemini_provider.py +148 -0
- package/python/voria/core/llm/modal_provider.py +228 -0
- package/python/voria/core/llm/model_discovery.py +289 -0
- package/python/voria/core/llm/openai_provider.py +146 -0
- package/python/voria/core/patcher/__init__.py +9 -0
- package/python/voria/core/patcher/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/patcher/__pycache__/patcher.cpython-312.pyc +0 -0
- package/python/voria/core/patcher/patcher.py +375 -0
- package/python/voria/core/planner/__init__.py +1 -0
- package/python/voria/core/setup.py +201 -0
- package/python/voria/core/token_manager/__init__.py +29 -0
- package/python/voria/core/token_manager/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/voria/core/token_manager/__pycache__/manager.cpython-312.pyc +0 -0
- package/python/voria/core/token_manager/manager.py +241 -0
- package/python/voria/engine.py +1185 -0
- package/python/voria/plugins/__init__.py +1 -0
- package/python/voria/plugins/python/__init__.py +1 -0
- package/python/voria/plugins/typescript/__init__.py +1 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code Patcher Module - Apply and rollback code patches
|
|
3
|
+
|
|
4
|
+
Supports unified diff format patches, with ability to:
|
|
5
|
+
- Parse unified diffs
|
|
6
|
+
- Apply patches to files
|
|
7
|
+
- Create backups for rollback
|
|
8
|
+
- Handle merge conflicts gracefully
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Tuple, Optional
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
import logging
|
|
17
|
+
import shutil
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class PatchHunk:
|
|
24
|
+
"""A single hunk from a unified diff"""
|
|
25
|
+
|
|
26
|
+
old_file: str
|
|
27
|
+
new_file: str
|
|
28
|
+
old_start: int
|
|
29
|
+
old_count: int
|
|
30
|
+
new_start: int
|
|
31
|
+
new_count: int
|
|
32
|
+
lines: List[str] # Patch lines (with +/- prefix)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UnifiedDiffParser:
|
|
36
|
+
"""Parse unified diff format"""
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def parse(diff_content: str) -> List[PatchHunk]:
|
|
40
|
+
"""
|
|
41
|
+
Parse unified diff format
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
--- a/file.py
|
|
45
|
+
+++ b/file.py
|
|
46
|
+
@@ -10,5 +10,6 @@
|
|
47
|
+
context line
|
|
48
|
+
-old line
|
|
49
|
+
+new line
|
|
50
|
+
context line
|
|
51
|
+
"""
|
|
52
|
+
hunks = []
|
|
53
|
+
lines = diff_content.split("\n")
|
|
54
|
+
|
|
55
|
+
i = 0
|
|
56
|
+
while i < len(lines):
|
|
57
|
+
line = lines[i]
|
|
58
|
+
|
|
59
|
+
# Check for file headers
|
|
60
|
+
if line.startswith("--- "):
|
|
61
|
+
old_file = line[4:].split("\t")[0]
|
|
62
|
+
i += 1
|
|
63
|
+
|
|
64
|
+
if i < len(lines) and lines[i].startswith("+++ "):
|
|
65
|
+
new_file = lines[i][4:].split("\t")[0]
|
|
66
|
+
i += 1
|
|
67
|
+
|
|
68
|
+
# Parse hunks for this file
|
|
69
|
+
while i < len(lines) and lines[i].startswith("@@"):
|
|
70
|
+
match = re.match(
|
|
71
|
+
r"@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", lines[i]
|
|
72
|
+
)
|
|
73
|
+
if match:
|
|
74
|
+
old_start = int(match.group(1))
|
|
75
|
+
old_count = int(match.group(2) or 1)
|
|
76
|
+
new_start = int(match.group(3))
|
|
77
|
+
new_count = int(match.group(4) or 1)
|
|
78
|
+
i += 1
|
|
79
|
+
|
|
80
|
+
# Read hunk lines
|
|
81
|
+
hunk_lines = []
|
|
82
|
+
while (
|
|
83
|
+
i < len(lines)
|
|
84
|
+
and not lines[i].startswith("@@")
|
|
85
|
+
and not lines[i].startswith("--- ")
|
|
86
|
+
and not lines[i].startswith("+++")
|
|
87
|
+
and lines[i]
|
|
88
|
+
and lines[i][0] in [" ", "-", "+", "\\"]
|
|
89
|
+
):
|
|
90
|
+
hunk_lines.append(lines[i])
|
|
91
|
+
i += 1
|
|
92
|
+
|
|
93
|
+
hunks.append(
|
|
94
|
+
PatchHunk(
|
|
95
|
+
old_file=old_file.lstrip("a/"),
|
|
96
|
+
new_file=new_file.lstrip("b/"),
|
|
97
|
+
old_start=old_start,
|
|
98
|
+
old_count=old_count,
|
|
99
|
+
new_start=new_start,
|
|
100
|
+
new_count=new_count,
|
|
101
|
+
lines=hunk_lines,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
i += 1
|
|
106
|
+
else:
|
|
107
|
+
i += 1
|
|
108
|
+
|
|
109
|
+
return hunks
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class CodePatcher:
|
|
113
|
+
"""Apply and manage code patches"""
|
|
114
|
+
|
|
115
|
+
BACKUP_DIR = Path.home() / ".voria" / "backups"
|
|
116
|
+
|
|
117
|
+
def __init__(self, repo_path: str = "."):
|
|
118
|
+
"""Initialize patcher with repo path"""
|
|
119
|
+
self.repo_path = Path(repo_path)
|
|
120
|
+
self.backup_dir = self.BACKUP_DIR
|
|
121
|
+
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
|
|
123
|
+
async def apply_patch(
|
|
124
|
+
self, patch_content: str, strategy: str = "strict"
|
|
125
|
+
) -> Dict[str, any]:
|
|
126
|
+
"""
|
|
127
|
+
Apply a unified diff patch
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
patch_content: Unified diff format patch
|
|
131
|
+
strategy: "strict" (fail on conflict) or "fuzzy" (try best effort)
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dict with status, modified_files, errors
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
logger.info("Parsing patch...")
|
|
138
|
+
hunks = UnifiedDiffParser.parse(patch_content)
|
|
139
|
+
|
|
140
|
+
if not hunks:
|
|
141
|
+
return {
|
|
142
|
+
"status": "error",
|
|
143
|
+
"message": "No valid hunks found in patch",
|
|
144
|
+
"modified_files": [],
|
|
145
|
+
"errors": ["Empty patch"],
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
modified_files = {}
|
|
149
|
+
errors = []
|
|
150
|
+
|
|
151
|
+
# Group hunks by file
|
|
152
|
+
hunks_by_file = {}
|
|
153
|
+
for hunk in hunks:
|
|
154
|
+
key = hunk.new_file or hunk.old_file
|
|
155
|
+
if key not in hunks_by_file:
|
|
156
|
+
hunks_by_file[key] = []
|
|
157
|
+
hunks_by_file[key].append(hunk)
|
|
158
|
+
|
|
159
|
+
# Apply patches to each file
|
|
160
|
+
for file_path, file_hunks in hunks_by_file.items():
|
|
161
|
+
try:
|
|
162
|
+
result = await self._apply_file_patch(
|
|
163
|
+
file_path, file_hunks, strategy
|
|
164
|
+
)
|
|
165
|
+
modified_files[file_path] = result
|
|
166
|
+
except Exception as e:
|
|
167
|
+
errors.append(f"{file_path}: {str(e)}")
|
|
168
|
+
logger.error(f"Failed to patch {file_path}: {e}")
|
|
169
|
+
|
|
170
|
+
# Check results
|
|
171
|
+
failed_count = sum(1 for r in modified_files.values() if not r["success"])
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"status": "success" if failed_count == 0 else "partial",
|
|
175
|
+
"message": f"Patched {len(modified_files)} files, {failed_count} failed",
|
|
176
|
+
"modified_files": modified_files,
|
|
177
|
+
"errors": errors,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Patch application failed: {e}")
|
|
182
|
+
return {
|
|
183
|
+
"status": "error",
|
|
184
|
+
"message": str(e),
|
|
185
|
+
"modified_files": {},
|
|
186
|
+
"errors": [str(e)],
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async def _apply_file_patch(
|
|
190
|
+
self, file_path: str, hunks: List[PatchHunk], strategy: str
|
|
191
|
+
) -> Dict[str, any]:
|
|
192
|
+
"""Apply patch hunks to a single file"""
|
|
193
|
+
|
|
194
|
+
full_path = self.repo_path / file_path
|
|
195
|
+
|
|
196
|
+
# Check file exists
|
|
197
|
+
if not full_path.exists():
|
|
198
|
+
logger.warning(f"File not found, creating: {file_path}")
|
|
199
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
200
|
+
full_path.write_text("")
|
|
201
|
+
|
|
202
|
+
# Create backup
|
|
203
|
+
backup_path = await self._create_backup(full_path)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
# Read current content
|
|
207
|
+
content = full_path.read_text()
|
|
208
|
+
lines = content.split("\n")
|
|
209
|
+
|
|
210
|
+
# Apply hunks (process in reverse to preserve line numbers)
|
|
211
|
+
for hunk in reversed(sorted(hunks, key=lambda h: h.old_start)):
|
|
212
|
+
lines = await self._apply_hunk(lines, hunk, strategy)
|
|
213
|
+
|
|
214
|
+
# Write patched content
|
|
215
|
+
full_path.write_text("\n".join(lines))
|
|
216
|
+
|
|
217
|
+
logger.info(f"Successfully patched: {file_path}")
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"success": True,
|
|
221
|
+
"file": file_path,
|
|
222
|
+
"backup": str(backup_path),
|
|
223
|
+
"hunks_applied": len(hunks),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
# Restore backup on failure
|
|
228
|
+
logger.error(f"Patch failed, restoring backup: {e}")
|
|
229
|
+
if backup_path.exists():
|
|
230
|
+
shutil.copy(backup_path, full_path)
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
"success": False,
|
|
234
|
+
"file": file_path,
|
|
235
|
+
"error": str(e),
|
|
236
|
+
"backup_restored": True,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async def _apply_hunk(
|
|
240
|
+
self, lines: List[str], hunk: PatchHunk, strategy: str
|
|
241
|
+
) -> List[str]:
|
|
242
|
+
"""Apply a single hunk to lines"""
|
|
243
|
+
|
|
244
|
+
# Parse hunk lines
|
|
245
|
+
context_before = []
|
|
246
|
+
removals = []
|
|
247
|
+
additions = []
|
|
248
|
+
context_after = []
|
|
249
|
+
|
|
250
|
+
current = None
|
|
251
|
+
|
|
252
|
+
for line in hunk.lines:
|
|
253
|
+
if line.startswith(" "):
|
|
254
|
+
content = line[1:]
|
|
255
|
+
if not removals and not additions:
|
|
256
|
+
context_before.append(content)
|
|
257
|
+
else:
|
|
258
|
+
context_after.append(content)
|
|
259
|
+
elif line.startswith("-"):
|
|
260
|
+
removals.append(line[1:])
|
|
261
|
+
current = "removal"
|
|
262
|
+
elif line.startswith("+"):
|
|
263
|
+
additions.append(line[1:])
|
|
264
|
+
current = "addition"
|
|
265
|
+
elif line.startswith("\\"):
|
|
266
|
+
# ""
|
|
267
|
+
pass
|
|
268
|
+
|
|
269
|
+
# Find hunk location in lines
|
|
270
|
+
start_idx = hunk.old_start - 1
|
|
271
|
+
|
|
272
|
+
# Try to match context
|
|
273
|
+
if strategy == "strict":
|
|
274
|
+
# Verify exact match
|
|
275
|
+
for i, ctx_line in enumerate(context_before):
|
|
276
|
+
if start_idx + i >= len(lines) or lines[start_idx + i] != ctx_line:
|
|
277
|
+
raise ValueError(
|
|
278
|
+
f"Context mismatch at line {hunk.old_start + i}: "
|
|
279
|
+
f"expected '{ctx_line}', got '{lines[start_idx + i] if start_idx + i < len(lines) else 'EOF'}'"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Apply changes
|
|
283
|
+
# Remove old lines
|
|
284
|
+
end_idx = start_idx + hunk.old_count
|
|
285
|
+
new_lines = lines[:start_idx] + additions + lines[end_idx:]
|
|
286
|
+
|
|
287
|
+
return new_lines
|
|
288
|
+
|
|
289
|
+
async def _create_backup(self, file_path: Path) -> Path:
|
|
290
|
+
"""Create backup of file before patching"""
|
|
291
|
+
|
|
292
|
+
import time
|
|
293
|
+
|
|
294
|
+
timestamp = int(time.time())
|
|
295
|
+
backup_name = f"{file_path.stem}_{timestamp}.bak"
|
|
296
|
+
backup_path = self.backup_dir / backup_name
|
|
297
|
+
|
|
298
|
+
shutil.copy(file_path, backup_path)
|
|
299
|
+
logger.info(f"Backup created: {backup_path}")
|
|
300
|
+
|
|
301
|
+
return backup_path
|
|
302
|
+
|
|
303
|
+
async def rollback_patch(self, file_path: str, backup_path: str) -> bool:
|
|
304
|
+
"""Rollback a patch using backup"""
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
full_path = self.repo_path / file_path
|
|
308
|
+
backup = Path(backup_path)
|
|
309
|
+
|
|
310
|
+
if not backup.exists():
|
|
311
|
+
logger.error(f"Backup not found: {backup_path}")
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
shutil.copy(backup, full_path)
|
|
315
|
+
logger.info(f"Rolled back: {file_path}")
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.error(f"Rollback failed: {e}")
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
def cleanup_backups(self, keep_count: int = 10) -> int:
|
|
323
|
+
"""Clean up old backups, keeping most recent"""
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
if not self.backup_dir.exists():
|
|
327
|
+
return 0
|
|
328
|
+
|
|
329
|
+
# Get all backups sorted by modification time
|
|
330
|
+
backups = sorted(
|
|
331
|
+
self.backup_dir.glob("*.bak"),
|
|
332
|
+
key=lambda p: p.stat().st_mtime,
|
|
333
|
+
reverse=True,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Remove old ones
|
|
337
|
+
removed = 0
|
|
338
|
+
for backup in backups[keep_count:]:
|
|
339
|
+
backup.unlink()
|
|
340
|
+
removed += 1
|
|
341
|
+
|
|
342
|
+
logger.info(f"Cleaned {removed} old backups")
|
|
343
|
+
return removed
|
|
344
|
+
|
|
345
|
+
except Exception as e:
|
|
346
|
+
logger.error(f"Backup cleanup failed: {e}")
|
|
347
|
+
return 0
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
async def test_patcher():
|
|
351
|
+
"""Test the code patcher"""
|
|
352
|
+
|
|
353
|
+
# Example patch
|
|
354
|
+
patch = """--- a/example.py
|
|
355
|
+
+++ b/example.py
|
|
356
|
+
@@ -1,5 +1,6 @@
|
|
357
|
+
def hello():
|
|
358
|
+
- print("old")
|
|
359
|
+
+ print("new")
|
|
360
|
+
return True
|
|
361
|
+
|
|
362
|
+
hello()
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
patcher = CodePatcher()
|
|
366
|
+
result = await patcher.apply_patch(patch)
|
|
367
|
+
|
|
368
|
+
print(f"Status: {result['status']}")
|
|
369
|
+
print(f"Message: {result['message']}")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
if __name__ == "__main__":
|
|
373
|
+
import asyncio
|
|
374
|
+
|
|
375
|
+
asyncio.run(test_patcher())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Planner module."""
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive setup CLI for configuring LLM providers.
|
|
3
|
+
Guides users through choosing provider, API key, and model selection.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from voria.core.llm import LLMProviderFactory
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProviderSetup:
|
|
19
|
+
"""Interactive provider configuration setup"""
|
|
20
|
+
|
|
21
|
+
CONFIG_DIR = Path.home() / ".voria"
|
|
22
|
+
CONFIG_FILE = CONFIG_DIR / "providers.json"
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
"""Initialize setup helper"""
|
|
26
|
+
self.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
self.providers = {}
|
|
28
|
+
self._load_config()
|
|
29
|
+
|
|
30
|
+
def _load_config(self):
|
|
31
|
+
"""Load saved provider configurations"""
|
|
32
|
+
if self.CONFIG_FILE.exists():
|
|
33
|
+
with open(self.CONFIG_FILE, "r") as f:
|
|
34
|
+
self.providers = json.load(f)
|
|
35
|
+
|
|
36
|
+
def _save_config(self):
|
|
37
|
+
"""Save provider configurations"""
|
|
38
|
+
with open(self.CONFIG_FILE, "w") as f:
|
|
39
|
+
json.dump(self.providers, f, indent=2)
|
|
40
|
+
os.chmod(self.CONFIG_FILE, 0o600) # Restrict permissions
|
|
41
|
+
|
|
42
|
+
async def setup_provider(
|
|
43
|
+
self,
|
|
44
|
+
provider_name: Optional[str] = None,
|
|
45
|
+
api_key: Optional[str] = None,
|
|
46
|
+
) -> Dict[str, str]:
|
|
47
|
+
"""
|
|
48
|
+
Interactive setup for a provider
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
provider_name: Provider to setup (optional, will prompt if None)
|
|
52
|
+
api_key: API key (optional, will prompt if None)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dict with provider_name, api_key, and selected model
|
|
56
|
+
"""
|
|
57
|
+
# Step 1: Choose provider
|
|
58
|
+
if not provider_name:
|
|
59
|
+
provider_name = await self._choose_provider()
|
|
60
|
+
provider_name = provider_name.lower()
|
|
61
|
+
|
|
62
|
+
# Step 2: Get API key
|
|
63
|
+
if not api_key:
|
|
64
|
+
api_key = await self._get_api_key(provider_name)
|
|
65
|
+
|
|
66
|
+
# Step 3: Discover models
|
|
67
|
+
print(f"\nš Fetching available {provider_name} models...")
|
|
68
|
+
try:
|
|
69
|
+
models = await LLMProviderFactory.discover_models(provider_name, api_key)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(f"ā Failed to fetch models: {e}")
|
|
72
|
+
print("Using fallback models...")
|
|
73
|
+
from voria.core.llm import ModelDiscovery
|
|
74
|
+
|
|
75
|
+
if provider_name == "modal":
|
|
76
|
+
models = await ModelDiscovery._get_modal_fallback()
|
|
77
|
+
elif provider_name == "openai":
|
|
78
|
+
models = await ModelDiscovery._get_openai_fallback()
|
|
79
|
+
elif provider_name == "gemini":
|
|
80
|
+
models = await ModelDiscovery._get_gemini_fallback()
|
|
81
|
+
elif provider_name == "claude":
|
|
82
|
+
models = await ModelDiscovery._get_claude_fallback()
|
|
83
|
+
|
|
84
|
+
# Step 4: Choose model
|
|
85
|
+
chosen_model = await self._choose_model(models)
|
|
86
|
+
|
|
87
|
+
# Step 5: Save configuration
|
|
88
|
+
print(f"\nš¾ Saving configuration...")
|
|
89
|
+
self.providers[provider_name] = {
|
|
90
|
+
"api_key": api_key,
|
|
91
|
+
"model": chosen_model.name,
|
|
92
|
+
"model_name": chosen_model.display_name,
|
|
93
|
+
}
|
|
94
|
+
self._save_config()
|
|
95
|
+
|
|
96
|
+
print(f"ā
{provider_name} configured successfully!")
|
|
97
|
+
print(f" Model: {chosen_model.display_name}")
|
|
98
|
+
print(f" Config saved to: {self.CONFIG_FILE}")
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"provider": provider_name,
|
|
102
|
+
"api_key": api_key,
|
|
103
|
+
"model": chosen_model.name,
|
|
104
|
+
"model_name": chosen_model.display_name,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async def _choose_provider(self) -> str:
|
|
108
|
+
"""Interactive provider selection"""
|
|
109
|
+
providers = LLMProviderFactory.list_providers()
|
|
110
|
+
|
|
111
|
+
print("\nš¤ Select LLM Provider:")
|
|
112
|
+
for i, provider in enumerate(providers, 1):
|
|
113
|
+
status = (
|
|
114
|
+
"ā
Configured" if provider in self.providers else "ā Not configured"
|
|
115
|
+
)
|
|
116
|
+
print(f" {i}. {provider.upper()} {status}")
|
|
117
|
+
|
|
118
|
+
while True:
|
|
119
|
+
try:
|
|
120
|
+
choice = input("\nEnter number (1-4): ").strip()
|
|
121
|
+
idx = int(choice) - 1
|
|
122
|
+
if 0 <= idx < len(providers):
|
|
123
|
+
return providers[idx]
|
|
124
|
+
print("Invalid choice. Try again.")
|
|
125
|
+
except ValueError:
|
|
126
|
+
print("Please enter a valid number.")
|
|
127
|
+
|
|
128
|
+
async def _get_api_key(self, provider_name: str) -> str:
|
|
129
|
+
"""Get API key from user or environment"""
|
|
130
|
+
# Check environment variables
|
|
131
|
+
env_vars = {
|
|
132
|
+
"modal": ["MODAL_API_KEY"],
|
|
133
|
+
"openai": ["OPENAI_API_KEY"],
|
|
134
|
+
"gemini": ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
|
135
|
+
"claude": ["ANTHROPIC_API_KEY", "CLAUDE_API_KEY"],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for env_var in env_vars.get(provider_name, []):
|
|
139
|
+
if env_var in os.environ:
|
|
140
|
+
print(f"ā
Using API key from ${env_var}")
|
|
141
|
+
return os.environ[env_var]
|
|
142
|
+
|
|
143
|
+
# Prompt user
|
|
144
|
+
while True:
|
|
145
|
+
api_key = input(f"\nš Enter {provider_name.upper()} API key: ").strip()
|
|
146
|
+
if api_key:
|
|
147
|
+
# Ask to save to environment
|
|
148
|
+
save = input("Save to environment variable? (y/n): ").lower().strip()
|
|
149
|
+
if save == "y":
|
|
150
|
+
env_var = env_vars[provider_name][0]
|
|
151
|
+
print(f"Add to ~/.bashrc or ~/.zshrc:")
|
|
152
|
+
print(f"export {env_var}='{api_key}'")
|
|
153
|
+
return api_key
|
|
154
|
+
print("API key cannot be empty.")
|
|
155
|
+
|
|
156
|
+
async def _choose_model(self, models: list) -> object:
|
|
157
|
+
"""Interactive model selection from discovered models"""
|
|
158
|
+
print("\nš¦ Available Models:")
|
|
159
|
+
for i, model in enumerate(models, 1):
|
|
160
|
+
print(f" {i}. {model.display_name}")
|
|
161
|
+
if model.description:
|
|
162
|
+
print(f" {model.description}")
|
|
163
|
+
|
|
164
|
+
while True:
|
|
165
|
+
try:
|
|
166
|
+
choice = input("\nSelect model number: ").strip()
|
|
167
|
+
idx = int(choice) - 1
|
|
168
|
+
if 0 <= idx < len(models):
|
|
169
|
+
return models[idx]
|
|
170
|
+
print("Invalid choice. Try again.")
|
|
171
|
+
except ValueError:
|
|
172
|
+
print("Please enter a valid number.")
|
|
173
|
+
|
|
174
|
+
def get_provider_config(self, provider_name: str) -> Optional[Dict]:
|
|
175
|
+
"""Get saved configuration for a provider"""
|
|
176
|
+
return self.providers.get(provider_name.lower())
|
|
177
|
+
|
|
178
|
+
def list_configured(self) -> Dict:
|
|
179
|
+
"""List all configured providers"""
|
|
180
|
+
return self.providers.copy()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def interactive_setup():
|
|
184
|
+
"""Run interactive setup for first-time users"""
|
|
185
|
+
setup = ProviderSetup()
|
|
186
|
+
|
|
187
|
+
print("\n" + "=" * 50)
|
|
188
|
+
print("š voria LLM Provider Setup")
|
|
189
|
+
print("=" * 50)
|
|
190
|
+
|
|
191
|
+
config = await setup.setup_provider()
|
|
192
|
+
|
|
193
|
+
print("\n" + "=" * 50)
|
|
194
|
+
print("ā
Setup Complete!")
|
|
195
|
+
print("=" * 50)
|
|
196
|
+
|
|
197
|
+
return config
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
asyncio.run(interactive_setup())
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Token Manager Module
|
|
2
|
+
|
|
3
|
+
Tracks LLM token usage across all providers and manages budget limits.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from voria.core.token_manager import get_token_manager
|
|
7
|
+
|
|
8
|
+
manager = get_token_manager()
|
|
9
|
+
manager.record_usage("openai", "gpt-4", 500, 1000)
|
|
10
|
+
summary = manager.get_usage_summary()
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .manager import (
|
|
14
|
+
TokenManager,
|
|
15
|
+
TokenBudget,
|
|
16
|
+
TokenUsageRecord,
|
|
17
|
+
get_token_manager,
|
|
18
|
+
init_token_manager,
|
|
19
|
+
PRICING,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"TokenManager",
|
|
24
|
+
"TokenBudget",
|
|
25
|
+
"TokenUsageRecord",
|
|
26
|
+
"get_token_manager",
|
|
27
|
+
"init_token_manager",
|
|
28
|
+
"PRICING",
|
|
29
|
+
]
|