shotgun-sh 0.2.17__py3-none-any.whl → 0.3.3.dev1__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.
- shotgun/agents/agent_manager.py +28 -14
- shotgun/agents/common.py +1 -1
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/manager.py +323 -53
- shotgun/agents/config/models.py +85 -21
- shotgun/agents/config/provider.py +51 -13
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/analyzer.py +6 -2
- shotgun/agents/conversation/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +27 -1
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/runner.py +230 -0
- shotgun/agents/tools/web_search/openai.py +1 -1
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +1 -1
- shotgun/cli/compact.py +5 -3
- shotgun/cli/context.py +44 -1
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +17 -9
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/codebase/core/ingestor.py +153 -7
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +325 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +42 -0
- shotgun/main.py +4 -0
- shotgun/posthog_telemetry.py +1 -1
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -3
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/plan.j2 +16 -0
- shotgun/prompts/agents/research.j2 +16 -3
- shotgun/prompts/agents/specify.j2 +54 -1
- shotgun/prompts/agents/state/system_state.j2 +0 -2
- shotgun/prompts/agents/tasks.j2 +16 -0
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/settings.py +5 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/tui/app.py +73 -9
- shotgun/tui/containers.py +1 -1
- shotgun/tui/layout.py +5 -0
- shotgun/tui/screens/chat/chat_screen.py +372 -95
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
- shotgun/tui/screens/chat_screen/command_providers.py +13 -2
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/confirmation_dialog.py +40 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +10 -3
- shotgun/tui/screens/github_issue.py +11 -2
- shotgun/tui/screens/model_picker.py +28 -8
- shotgun/tui/screens/onboarding.py +149 -0
- shotgun/tui/screens/pipx_migration.py +58 -6
- shotgun/tui/screens/provider_config.py +66 -8
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +110 -16
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +123 -0
- shotgun/tui/services/conversation_service.py +5 -2
- shotgun/tui/widgets/widget_coordinator.py +1 -1
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/METADATA +9 -2
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/RECORD +112 -77
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,6 +6,7 @@ import os
|
|
|
6
6
|
import time
|
|
7
7
|
import uuid
|
|
8
8
|
from collections import defaultdict
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
@@ -198,11 +199,21 @@ class Ingestor:
|
|
|
198
199
|
return True
|
|
199
200
|
return False
|
|
200
201
|
|
|
201
|
-
def flush_nodes(
|
|
202
|
-
|
|
202
|
+
def flush_nodes(
|
|
203
|
+
self,
|
|
204
|
+
progress_callback: Callable[[int, int], None] | None = None,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Flush pending node insertions to the database.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
progress_callback: Optional callback(current, total) for progress reporting
|
|
210
|
+
"""
|
|
203
211
|
if not self.node_buffer:
|
|
204
212
|
return
|
|
205
213
|
|
|
214
|
+
total_nodes = len(self.node_buffer)
|
|
215
|
+
processed = 0
|
|
216
|
+
|
|
206
217
|
# Group nodes by label
|
|
207
218
|
nodes_by_label: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
208
219
|
for label, properties in self.node_buffer:
|
|
@@ -239,9 +250,18 @@ class Ingestor:
|
|
|
239
250
|
params = dict(zip(prop_names, prop_values, strict=False))
|
|
240
251
|
self.conn.execute(query, params)
|
|
241
252
|
|
|
253
|
+
# Report progress
|
|
254
|
+
processed += 1
|
|
255
|
+
if progress_callback and processed % 10 == 0:
|
|
256
|
+
progress_callback(processed, total_nodes)
|
|
257
|
+
|
|
242
258
|
except Exception as e:
|
|
243
259
|
logger.error(f"Failed to insert {label} nodes: {e}")
|
|
244
260
|
|
|
261
|
+
# Final progress report
|
|
262
|
+
if progress_callback:
|
|
263
|
+
progress_callback(total_nodes, total_nodes)
|
|
264
|
+
|
|
245
265
|
# Log node counts by type
|
|
246
266
|
node_type_counts: dict[str, int] = {}
|
|
247
267
|
for label, _ in self.node_buffer:
|
|
@@ -280,11 +300,21 @@ class Ingestor:
|
|
|
280
300
|
|
|
281
301
|
# Don't auto-flush relationships - wait for explicit flush_all() to ensure nodes exist first
|
|
282
302
|
|
|
283
|
-
def flush_relationships(
|
|
284
|
-
|
|
303
|
+
def flush_relationships(
|
|
304
|
+
self,
|
|
305
|
+
progress_callback: Callable[[int, int], None] | None = None,
|
|
306
|
+
) -> None:
|
|
307
|
+
"""Flush pending relationship insertions to the database.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
progress_callback: Optional callback(current, total) for progress reporting
|
|
311
|
+
"""
|
|
285
312
|
if not self.relationship_buffer:
|
|
286
313
|
return
|
|
287
314
|
|
|
315
|
+
total_rels = len(self.relationship_buffer)
|
|
316
|
+
processed = 0
|
|
317
|
+
|
|
288
318
|
# Group relationships by type
|
|
289
319
|
rels_by_type: dict[
|
|
290
320
|
str, list[tuple[str, str, Any, str, str, str, Any, dict[str, Any] | None]]
|
|
@@ -299,7 +329,7 @@ class Ingestor:
|
|
|
299
329
|
to_label,
|
|
300
330
|
to_key,
|
|
301
331
|
to_value,
|
|
302
|
-
|
|
332
|
+
_properties,
|
|
303
333
|
) = rel_data
|
|
304
334
|
|
|
305
335
|
# Determine actual table name
|
|
@@ -323,7 +353,7 @@ class Ingestor:
|
|
|
323
353
|
to_label,
|
|
324
354
|
to_key,
|
|
325
355
|
to_value,
|
|
326
|
-
|
|
356
|
+
_properties,
|
|
327
357
|
) = rel_data
|
|
328
358
|
|
|
329
359
|
# Build MATCH and MERGE query (use MERGE to avoid duplicate relationships)
|
|
@@ -337,6 +367,11 @@ class Ingestor:
|
|
|
337
367
|
try:
|
|
338
368
|
self.conn.execute(query, params)
|
|
339
369
|
success_count += 1
|
|
370
|
+
|
|
371
|
+
# Report progress
|
|
372
|
+
processed += 1
|
|
373
|
+
if progress_callback and processed % 10 == 0:
|
|
374
|
+
progress_callback(processed, total_rels)
|
|
340
375
|
except Exception as e:
|
|
341
376
|
logger.error(
|
|
342
377
|
f"Failed to create single relationship {table_name}: {from_label}({from_value}) -> {to_label}({to_value})"
|
|
@@ -360,6 +395,10 @@ class Ingestor:
|
|
|
360
395
|
# Don't swallow the exception - let it propagate
|
|
361
396
|
raise
|
|
362
397
|
|
|
398
|
+
# Final progress report
|
|
399
|
+
if progress_callback:
|
|
400
|
+
progress_callback(total_rels, total_rels)
|
|
401
|
+
|
|
363
402
|
# Log summary of flushed relationships
|
|
364
403
|
logger.info(
|
|
365
404
|
f"Flushed {len(self.relationship_buffer)} relationships: {relationship_counts}"
|
|
@@ -586,6 +625,9 @@ class SimpleGraphBuilder:
|
|
|
586
625
|
self.ignore_dirs = self.ignore_dirs.union(set(exclude_patterns))
|
|
587
626
|
self.progress_callback = progress_callback
|
|
588
627
|
|
|
628
|
+
# Generate unique session ID for correlating timing events in PostHog
|
|
629
|
+
self._index_session_id = str(uuid.uuid4())[:8]
|
|
630
|
+
|
|
589
631
|
# Caches
|
|
590
632
|
self.structural_elements: dict[Path, str | None] = {}
|
|
591
633
|
self.ast_cache: dict[Path, tuple[Node, str]] = {}
|
|
@@ -621,25 +663,129 @@ class SimpleGraphBuilder:
|
|
|
621
663
|
# Don't let progress callback errors crash the build
|
|
622
664
|
logger.debug(f"Progress callback error: {e}")
|
|
623
665
|
|
|
666
|
+
def _log_timing(
|
|
667
|
+
self,
|
|
668
|
+
phase: str,
|
|
669
|
+
duration: float,
|
|
670
|
+
items: int,
|
|
671
|
+
extra_props: dict[str, Any] | None = None,
|
|
672
|
+
) -> None:
|
|
673
|
+
"""Log timing data to PostHog for analysis."""
|
|
674
|
+
from shotgun.posthog_telemetry import track_event
|
|
675
|
+
|
|
676
|
+
properties: dict[str, Any] = {
|
|
677
|
+
"session_id": self._index_session_id,
|
|
678
|
+
"phase": phase,
|
|
679
|
+
"duration_seconds": round(duration, 3),
|
|
680
|
+
"item_count": items,
|
|
681
|
+
}
|
|
682
|
+
if extra_props:
|
|
683
|
+
properties.update(extra_props)
|
|
684
|
+
|
|
685
|
+
track_event("codebase_index_phase_completed", properties)
|
|
686
|
+
|
|
687
|
+
def _log_summary(
|
|
688
|
+
self,
|
|
689
|
+
total_duration: float,
|
|
690
|
+
total_files: int,
|
|
691
|
+
total_nodes: int,
|
|
692
|
+
total_relationships: int,
|
|
693
|
+
) -> None:
|
|
694
|
+
"""Log indexing summary event to PostHog."""
|
|
695
|
+
from shotgun.posthog_telemetry import track_event
|
|
696
|
+
|
|
697
|
+
track_event(
|
|
698
|
+
"codebase_index_completed",
|
|
699
|
+
{
|
|
700
|
+
"session_id": self._index_session_id,
|
|
701
|
+
"total_duration_seconds": round(total_duration, 3),
|
|
702
|
+
"total_files": total_files,
|
|
703
|
+
"total_nodes": total_nodes,
|
|
704
|
+
"total_relationships": total_relationships,
|
|
705
|
+
},
|
|
706
|
+
)
|
|
707
|
+
|
|
624
708
|
async def run(self) -> None:
|
|
625
709
|
"""Run the three-pass graph building process."""
|
|
626
710
|
logger.info(f"Building graph for project: {self.project_name}")
|
|
627
711
|
|
|
628
712
|
# Pass 1: Structure
|
|
629
713
|
logger.info("Pass 1: Identifying packages and folders...")
|
|
714
|
+
t0 = time.time()
|
|
630
715
|
self._identify_structure()
|
|
716
|
+
t1 = time.time()
|
|
717
|
+
self._log_timing("structure", t1 - t0, len(self.structural_elements))
|
|
631
718
|
|
|
632
719
|
# Pass 2: Definitions
|
|
633
720
|
logger.info("Pass 2: Processing files and extracting definitions...")
|
|
721
|
+
t2 = time.time()
|
|
634
722
|
await self._process_files()
|
|
723
|
+
t3 = time.time()
|
|
724
|
+
self._log_timing(
|
|
725
|
+
"definitions",
|
|
726
|
+
t3 - t2,
|
|
727
|
+
len(self.ast_cache),
|
|
728
|
+
{"file_count": len(self.ast_cache)},
|
|
729
|
+
)
|
|
635
730
|
|
|
636
731
|
# Pass 3: Relationships
|
|
637
732
|
logger.info("Pass 3: Processing relationships (calls, imports)...")
|
|
733
|
+
t4 = time.time()
|
|
638
734
|
self._process_relationships()
|
|
735
|
+
t5 = time.time()
|
|
736
|
+
self._log_timing("relationships", t5 - t4, len(self.ast_cache))
|
|
639
737
|
|
|
640
738
|
# Flush all pending operations
|
|
641
739
|
logger.info("Flushing all data to database...")
|
|
642
|
-
|
|
740
|
+
t6 = time.time()
|
|
741
|
+
node_count = len(self.ingestor.node_buffer)
|
|
742
|
+
|
|
743
|
+
# Create progress callback for flush_nodes
|
|
744
|
+
def node_progress(current: int, total: int) -> None:
|
|
745
|
+
self._report_progress(
|
|
746
|
+
"flush_nodes", "Flushing nodes to database", current, total
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
self.ingestor.flush_nodes(progress_callback=node_progress)
|
|
750
|
+
self._report_progress(
|
|
751
|
+
"flush_nodes", "Flushing nodes to database", node_count, node_count, True
|
|
752
|
+
)
|
|
753
|
+
t7 = time.time()
|
|
754
|
+
self._log_timing("flush_nodes", t7 - t6, node_count, {"node_count": node_count})
|
|
755
|
+
|
|
756
|
+
rel_count = len(self.ingestor.relationship_buffer)
|
|
757
|
+
|
|
758
|
+
# Create progress callback for flush_relationships
|
|
759
|
+
def rel_progress(current: int, total: int) -> None:
|
|
760
|
+
self._report_progress(
|
|
761
|
+
"flush_relationships",
|
|
762
|
+
"Flushing relationships to database",
|
|
763
|
+
current,
|
|
764
|
+
total,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
self.ingestor.flush_relationships(progress_callback=rel_progress)
|
|
768
|
+
self._report_progress(
|
|
769
|
+
"flush_relationships",
|
|
770
|
+
"Flushing relationships to database",
|
|
771
|
+
rel_count,
|
|
772
|
+
rel_count,
|
|
773
|
+
True,
|
|
774
|
+
)
|
|
775
|
+
t8 = time.time()
|
|
776
|
+
self._log_timing(
|
|
777
|
+
"flush_relationships", t8 - t7, rel_count, {"relationship_count": rel_count}
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
# Track summary event with totals (no PII - only numeric metadata)
|
|
781
|
+
total_duration = t8 - t0
|
|
782
|
+
self._log_summary(
|
|
783
|
+
total_duration=total_duration,
|
|
784
|
+
total_files=len(self.ast_cache),
|
|
785
|
+
total_nodes=node_count,
|
|
786
|
+
total_relationships=rel_count,
|
|
787
|
+
)
|
|
788
|
+
|
|
643
789
|
logger.info("Graph building complete!")
|
|
644
790
|
|
|
645
791
|
def _identify_structure(self) -> None:
|
shotgun/codebase/models.py
CHANGED
|
@@ -29,6 +29,8 @@ class ProgressPhase(StrEnum):
|
|
|
29
29
|
STRUCTURE = "structure" # Identifying packages and folders
|
|
30
30
|
DEFINITIONS = "definitions" # Processing files and extracting definitions
|
|
31
31
|
RELATIONSHIPS = "relationships" # Processing relationships (calls, imports)
|
|
32
|
+
FLUSH_NODES = "flush_nodes" # Flushing nodes to database
|
|
33
|
+
FLUSH_RELATIONSHIPS = "flush_relationships" # Flushing relationships to database
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
class IndexProgress(BaseModel):
|
shotgun/exceptions.py
CHANGED
|
@@ -1,13 +1,57 @@
|
|
|
1
1
|
"""General exceptions for Shotgun application."""
|
|
2
2
|
|
|
3
|
+
from shotgun.utils import get_shotgun_home
|
|
4
|
+
|
|
5
|
+
# Shotgun Account signup URL for BYOK users
|
|
6
|
+
SHOTGUN_SIGNUP_URL = "https://shotgun.sh"
|
|
7
|
+
SHOTGUN_CONTACT_EMAIL = "contact@shotgun.sh"
|
|
8
|
+
|
|
3
9
|
|
|
4
10
|
class ErrorNotPickedUpBySentry(Exception): # noqa: N818
|
|
5
11
|
"""Base for user-actionable errors that shouldn't be sent to Sentry.
|
|
6
12
|
|
|
7
13
|
These errors represent expected user conditions requiring action
|
|
8
14
|
rather than bugs that need tracking.
|
|
15
|
+
|
|
16
|
+
All subclasses should implement to_markdown() and to_plain_text() methods
|
|
17
|
+
for consistent error message formatting.
|
|
9
18
|
"""
|
|
10
19
|
|
|
20
|
+
def to_markdown(self) -> str:
|
|
21
|
+
"""Generate markdown-formatted error message for TUI.
|
|
22
|
+
|
|
23
|
+
Subclasses should override this method.
|
|
24
|
+
"""
|
|
25
|
+
return f"⚠️ {str(self)}"
|
|
26
|
+
|
|
27
|
+
def to_plain_text(self) -> str:
|
|
28
|
+
"""Generate plain text error message for CLI.
|
|
29
|
+
|
|
30
|
+
Subclasses should override this method.
|
|
31
|
+
"""
|
|
32
|
+
return f"⚠️ {str(self)}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ============================================================================
|
|
36
|
+
# User Action Required Errors
|
|
37
|
+
# ============================================================================
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AgentCancelledException(ErrorNotPickedUpBySentry):
|
|
41
|
+
"""Raised when user cancels an agent operation."""
|
|
42
|
+
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
"""Initialize the exception."""
|
|
45
|
+
super().__init__("Operation cancelled by user")
|
|
46
|
+
|
|
47
|
+
def to_markdown(self) -> str:
|
|
48
|
+
"""Generate markdown-formatted error message for TUI."""
|
|
49
|
+
return "⚠️ Operation cancelled by user"
|
|
50
|
+
|
|
51
|
+
def to_plain_text(self) -> str:
|
|
52
|
+
"""Generate plain text error message for CLI."""
|
|
53
|
+
return "⚠️ Operation cancelled by user"
|
|
54
|
+
|
|
11
55
|
|
|
12
56
|
class ContextSizeLimitExceeded(ErrorNotPickedUpBySentry):
|
|
13
57
|
"""Raised when conversation context exceeds the model's limits.
|
|
@@ -30,3 +74,284 @@ class ContextSizeLimitExceeded(ErrorNotPickedUpBySentry):
|
|
|
30
74
|
super().__init__(
|
|
31
75
|
f"Context too large for {model_name} (limit: {max_tokens:,} tokens)"
|
|
32
76
|
)
|
|
77
|
+
|
|
78
|
+
def to_markdown(self) -> str:
|
|
79
|
+
"""Generate markdown-formatted error message for TUI."""
|
|
80
|
+
return (
|
|
81
|
+
f"⚠️ **Context too large for {self.model_name}**\n\n"
|
|
82
|
+
f"Your conversation history exceeds this model's limit ({self.max_tokens:,} tokens).\n\n"
|
|
83
|
+
f"**Choose an action:**\n\n"
|
|
84
|
+
f"1. Switch to a larger model (`Ctrl+P` → Change Model)\n"
|
|
85
|
+
f"2. Switch to a larger model, compact (`/compact`), then switch back to {self.model_name}\n"
|
|
86
|
+
f"3. Clear conversation (`/clear`)\n"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def to_plain_text(self) -> str:
|
|
90
|
+
"""Generate plain text error message for CLI."""
|
|
91
|
+
return (
|
|
92
|
+
f"⚠️ Context too large for {self.model_name}\n\n"
|
|
93
|
+
f"Your conversation history exceeds this model's limit ({self.max_tokens:,} tokens).\n\n"
|
|
94
|
+
f"Choose an action:\n"
|
|
95
|
+
f"1. Switch to a larger model\n"
|
|
96
|
+
f"2. Switch to a larger model, compact, then switch back to {self.model_name}\n"
|
|
97
|
+
f"3. Clear conversation\n"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ============================================================================
|
|
102
|
+
# Shotgun Account Errors (show contact email in TUI)
|
|
103
|
+
# ============================================================================
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ShotgunAccountException(ErrorNotPickedUpBySentry):
|
|
107
|
+
"""Base class for Shotgun Account service errors.
|
|
108
|
+
|
|
109
|
+
TUI will check isinstance() of this class to show contact email UI.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class BudgetExceededException(ShotgunAccountException):
|
|
114
|
+
"""Raised when Shotgun Account budget has been exceeded.
|
|
115
|
+
|
|
116
|
+
This is a user-actionable error - they need to contact support
|
|
117
|
+
to increase their budget limit. This is a temporary exception
|
|
118
|
+
until self-service budget increases are implemented.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
current_cost: float | None = None,
|
|
124
|
+
max_budget: float | None = None,
|
|
125
|
+
message: str | None = None,
|
|
126
|
+
):
|
|
127
|
+
"""Initialize the exception.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
current_cost: Current total spend/cost (optional)
|
|
131
|
+
max_budget: Maximum budget limit (optional)
|
|
132
|
+
message: Optional custom error message from API
|
|
133
|
+
"""
|
|
134
|
+
self.current_cost = current_cost
|
|
135
|
+
self.max_budget = max_budget
|
|
136
|
+
self.api_message = message
|
|
137
|
+
|
|
138
|
+
if message:
|
|
139
|
+
error_msg = message
|
|
140
|
+
elif current_cost is not None and max_budget is not None:
|
|
141
|
+
error_msg = f"Budget exceeded: ${current_cost:.2f} / ${max_budget:.2f}"
|
|
142
|
+
else:
|
|
143
|
+
error_msg = "Budget exceeded"
|
|
144
|
+
|
|
145
|
+
super().__init__(error_msg)
|
|
146
|
+
|
|
147
|
+
def to_markdown(self) -> str:
|
|
148
|
+
"""Generate markdown-formatted error message for TUI.
|
|
149
|
+
|
|
150
|
+
Note: TUI will detect ShotgunAccountException and automatically
|
|
151
|
+
show email contact UI component.
|
|
152
|
+
"""
|
|
153
|
+
return (
|
|
154
|
+
"⚠️ **Your Shotgun Account budget has been exceeded!**\n\n"
|
|
155
|
+
"Your account has reached its spending limit and cannot process more requests.\n\n"
|
|
156
|
+
"**Action Required:** Top up your account to continue using Shotgun.\n\n"
|
|
157
|
+
"👉 **[Top Up Now at https://app.shotgun.sh/dashboard](https://app.shotgun.sh/dashboard)**\n\n"
|
|
158
|
+
"**Need help?** Contact us if you have questions about your budget.\n\n"
|
|
159
|
+
f"_Error details: {str(self)}_"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def to_plain_text(self) -> str:
|
|
163
|
+
"""Generate plain text error message for CLI."""
|
|
164
|
+
return (
|
|
165
|
+
"⚠️ Your Shotgun Account budget has been exceeded!\n\n"
|
|
166
|
+
"Your account has reached its spending limit and cannot process more requests.\n\n"
|
|
167
|
+
"Action Required: Top up your account to continue using Shotgun.\n\n"
|
|
168
|
+
"→ Top Up Now: https://app.shotgun.sh/dashboard\n\n"
|
|
169
|
+
f"Need help? Contact: {SHOTGUN_CONTACT_EMAIL}\n\n"
|
|
170
|
+
f"Error details: {str(self)}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class ShotgunServiceOverloadException(ShotgunAccountException):
|
|
175
|
+
"""Raised when Shotgun Account AI service is overloaded."""
|
|
176
|
+
|
|
177
|
+
def __init__(self, message: str | None = None):
|
|
178
|
+
"""Initialize the exception.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
message: Optional custom error message from API
|
|
182
|
+
"""
|
|
183
|
+
super().__init__(message or "Service temporarily overloaded")
|
|
184
|
+
|
|
185
|
+
def to_markdown(self) -> str:
|
|
186
|
+
"""Generate markdown-formatted error message for TUI."""
|
|
187
|
+
return "⚠️ The AI service is temporarily overloaded. Please wait a moment and try again."
|
|
188
|
+
|
|
189
|
+
def to_plain_text(self) -> str:
|
|
190
|
+
"""Generate plain text error message for CLI."""
|
|
191
|
+
return "⚠️ The AI service is temporarily overloaded. Please wait a moment and try again."
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ShotgunRateLimitException(ShotgunAccountException):
|
|
195
|
+
"""Raised when Shotgun Account rate limit is reached."""
|
|
196
|
+
|
|
197
|
+
def __init__(self, message: str | None = None):
|
|
198
|
+
"""Initialize the exception.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
message: Optional custom error message from API
|
|
202
|
+
"""
|
|
203
|
+
super().__init__(message or "Rate limit reached")
|
|
204
|
+
|
|
205
|
+
def to_markdown(self) -> str:
|
|
206
|
+
"""Generate markdown-formatted error message for TUI."""
|
|
207
|
+
return "⚠️ Rate limit reached. Please wait before trying again."
|
|
208
|
+
|
|
209
|
+
def to_plain_text(self) -> str:
|
|
210
|
+
"""Generate plain text error message for CLI."""
|
|
211
|
+
return "⚠️ Rate limit reached. Please wait before trying again."
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ============================================================================
|
|
215
|
+
# BYOK (Bring Your Own Key) API Errors
|
|
216
|
+
# ============================================================================
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class BYOKAPIException(ErrorNotPickedUpBySentry):
|
|
220
|
+
"""Base class for BYOK API errors.
|
|
221
|
+
|
|
222
|
+
All BYOK errors suggest using Shotgun Account to avoid the issue.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def __init__(self, message: str, specific_error: str = "API error"):
|
|
226
|
+
"""Initialize the exception.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
message: The error message from the API
|
|
230
|
+
specific_error: Human-readable error type label
|
|
231
|
+
"""
|
|
232
|
+
self.api_message = message
|
|
233
|
+
self.specific_error = specific_error
|
|
234
|
+
super().__init__(message)
|
|
235
|
+
|
|
236
|
+
def to_markdown(self) -> str:
|
|
237
|
+
"""Generate markdown-formatted error message for TUI."""
|
|
238
|
+
return (
|
|
239
|
+
f"⚠️ **{self.specific_error}**: {self.api_message}\n\n"
|
|
240
|
+
f"_This could be avoided with a [Shotgun Account]({SHOTGUN_SIGNUP_URL})._"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def to_plain_text(self) -> str:
|
|
244
|
+
"""Generate plain text error message for CLI."""
|
|
245
|
+
return (
|
|
246
|
+
f"⚠️ {self.specific_error}: {self.api_message}\n\n"
|
|
247
|
+
f"This could be avoided with a Shotgun Account: {SHOTGUN_SIGNUP_URL}"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class BYOKRateLimitException(BYOKAPIException):
|
|
252
|
+
"""Raised when BYOK user hits rate limit."""
|
|
253
|
+
|
|
254
|
+
def __init__(self, message: str):
|
|
255
|
+
"""Initialize the exception.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
message: The error message from the API
|
|
259
|
+
"""
|
|
260
|
+
super().__init__(message, specific_error="Rate limit reached")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class BYOKQuotaBillingException(BYOKAPIException):
|
|
264
|
+
"""Raised when BYOK user has quota or billing issues."""
|
|
265
|
+
|
|
266
|
+
def __init__(self, message: str):
|
|
267
|
+
"""Initialize the exception.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
message: The error message from the API
|
|
271
|
+
"""
|
|
272
|
+
super().__init__(message, specific_error="Quota or billing issue")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class BYOKAuthenticationException(BYOKAPIException):
|
|
276
|
+
"""Raised when BYOK authentication fails."""
|
|
277
|
+
|
|
278
|
+
def __init__(self, message: str):
|
|
279
|
+
"""Initialize the exception.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
message: The error message from the API
|
|
283
|
+
"""
|
|
284
|
+
super().__init__(message, specific_error="Authentication error")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class BYOKServiceOverloadException(BYOKAPIException):
|
|
288
|
+
"""Raised when BYOK service is overloaded."""
|
|
289
|
+
|
|
290
|
+
def __init__(self, message: str):
|
|
291
|
+
"""Initialize the exception.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
message: The error message from the API
|
|
295
|
+
"""
|
|
296
|
+
super().__init__(message, specific_error="Service overloaded")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class BYOKGenericAPIException(BYOKAPIException):
|
|
300
|
+
"""Raised for generic BYOK API errors."""
|
|
301
|
+
|
|
302
|
+
def __init__(self, message: str):
|
|
303
|
+
"""Initialize the exception.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
message: The error message from the API
|
|
307
|
+
"""
|
|
308
|
+
super().__init__(message, specific_error="API error")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ============================================================================
|
|
312
|
+
# Generic Errors
|
|
313
|
+
# ============================================================================
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class GenericAPIStatusException(ErrorNotPickedUpBySentry):
|
|
317
|
+
"""Raised for generic API status errors that don't fit other categories."""
|
|
318
|
+
|
|
319
|
+
def __init__(self, message: str):
|
|
320
|
+
"""Initialize the exception.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
message: The error message from the API
|
|
324
|
+
"""
|
|
325
|
+
self.api_message = message
|
|
326
|
+
super().__init__(message)
|
|
327
|
+
|
|
328
|
+
def to_markdown(self) -> str:
|
|
329
|
+
"""Generate markdown-formatted error message for TUI."""
|
|
330
|
+
return f"⚠️ AI service error: {self.api_message}"
|
|
331
|
+
|
|
332
|
+
def to_plain_text(self) -> str:
|
|
333
|
+
"""Generate plain text error message for CLI."""
|
|
334
|
+
return f"⚠️ AI service error: {self.api_message}"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class UnknownAgentException(ErrorNotPickedUpBySentry):
|
|
338
|
+
"""Raised for unknown/unclassified agent errors."""
|
|
339
|
+
|
|
340
|
+
def __init__(self, original_exception: Exception):
|
|
341
|
+
"""Initialize the exception.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
original_exception: The original exception that was caught
|
|
345
|
+
"""
|
|
346
|
+
self.original_exception = original_exception
|
|
347
|
+
super().__init__(str(original_exception))
|
|
348
|
+
|
|
349
|
+
def to_markdown(self) -> str:
|
|
350
|
+
"""Generate markdown-formatted error message for TUI."""
|
|
351
|
+
log_path = get_shotgun_home() / "logs" / "shotgun.log"
|
|
352
|
+
return f"⚠️ An error occurred: {str(self.original_exception)}\n\nCheck logs at {log_path}"
|
|
353
|
+
|
|
354
|
+
def to_plain_text(self) -> str:
|
|
355
|
+
"""Generate plain text error message for CLI."""
|
|
356
|
+
log_path = get_shotgun_home() / "logs" / "shotgun.log"
|
|
357
|
+
return f"⚠️ An error occurred: {str(self.original_exception)}\n\nCheck logs at {log_path}"
|
shotgun/llm_proxy/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""LiteLLM proxy client utilities and configuration."""
|
|
2
2
|
|
|
3
|
+
from .client import LiteLLMProxyClient, get_budget_info
|
|
3
4
|
from .clients import (
|
|
4
5
|
create_anthropic_proxy_provider,
|
|
5
6
|
create_litellm_provider,
|
|
@@ -9,6 +10,14 @@ from .constants import (
|
|
|
9
10
|
LITELLM_PROXY_BASE_URL,
|
|
10
11
|
LITELLM_PROXY_OPENAI_BASE,
|
|
11
12
|
)
|
|
13
|
+
from .models import (
|
|
14
|
+
BudgetInfo,
|
|
15
|
+
BudgetSource,
|
|
16
|
+
KeyInfoData,
|
|
17
|
+
KeyInfoResponse,
|
|
18
|
+
TeamInfoData,
|
|
19
|
+
TeamInfoResponse,
|
|
20
|
+
)
|
|
12
21
|
|
|
13
22
|
__all__ = [
|
|
14
23
|
"LITELLM_PROXY_BASE_URL",
|
|
@@ -16,4 +25,12 @@ __all__ = [
|
|
|
16
25
|
"LITELLM_PROXY_OPENAI_BASE",
|
|
17
26
|
"create_litellm_provider",
|
|
18
27
|
"create_anthropic_proxy_provider",
|
|
28
|
+
"LiteLLMProxyClient",
|
|
29
|
+
"get_budget_info",
|
|
30
|
+
"BudgetInfo",
|
|
31
|
+
"BudgetSource",
|
|
32
|
+
"KeyInfoData",
|
|
33
|
+
"KeyInfoResponse",
|
|
34
|
+
"TeamInfoData",
|
|
35
|
+
"TeamInfoResponse",
|
|
19
36
|
]
|