poliora 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.
- poliora/__init__.py +7 -0
- poliora/app_launcher.py +83 -0
- poliora/config.py +103 -0
- poliora/cost/__init__.py +134 -0
- poliora/cost/antigravity.py +182 -0
- poliora/cost/budget.py +52 -0
- poliora/cost/capture.py +287 -0
- poliora/cost/catalog.py +196 -0
- poliora/cost/codex_exec.py +177 -0
- poliora/cost/companion.py +171 -0
- poliora/cost/dashboard.py +416 -0
- poliora/cost/decisions.py +226 -0
- poliora/cost/detection.py +108 -0
- poliora/cost/importer.py +415 -0
- poliora/cost/local_usage.py +482 -0
- poliora/cost/pricing.py +487 -0
- poliora/cost/recommendations.py +141 -0
- poliora/cost/reports.py +306 -0
- poliora/cost/scanner.py +92 -0
- poliora/cost/scenarios.py +95 -0
- poliora/cost/sdk.py +202 -0
- poliora/cost/simulation.py +144 -0
- poliora/cost/sync.py +164 -0
- poliora/cost/usage.py +183 -0
- poliora/cost/workspace.py +133 -0
- poliora/main.py +1062 -0
- poliora/trainer/__init__.py +5 -0
- poliora/trainer/core.py +637 -0
- poliora/utils/__init__.py +20 -0
- poliora/utils/benchmark.py +365 -0
- poliora/utils/carbon.py +257 -0
- poliora/utils/eco_tips.py +131 -0
- poliora/utils/electricity.py +146 -0
- poliora/web.py +1517 -0
- poliora-0.2.0.dist-info/METADATA +415 -0
- poliora-0.2.0.dist-info/RECORD +39 -0
- poliora-0.2.0.dist-info/WHEEL +4 -0
- poliora-0.2.0.dist-info/entry_points.txt +3 -0
- poliora-0.2.0.dist-info/licenses/LICENSE +21 -0
poliora/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Poliora - AI cost, carbon, and fine-tuning efficiency toolkit."""
|
|
2
|
+
|
|
3
|
+
from poliora.cost import log_openai_response, log_usage, track_anthropic_client, track_openai_client
|
|
4
|
+
|
|
5
|
+
__version__ = "0.2.0"
|
|
6
|
+
|
|
7
|
+
__all__ = ["__version__", "log_openai_response", "log_usage", "track_anthropic_client", "track_openai_client"]
|
poliora/app_launcher.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Standalone Desktop App launcher for Poliora."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import socket
|
|
8
|
+
import sys
|
|
9
|
+
import webbrowser
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from threading import Timer
|
|
12
|
+
|
|
13
|
+
from poliora.cost import init_workspace, load_workspace
|
|
14
|
+
from poliora.web import run_dashboard
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _available_port(preferred_port: int) -> int:
|
|
18
|
+
"""Return the preferred loopback port, or the next available one."""
|
|
19
|
+
for port in range(preferred_port, preferred_port + 20):
|
|
20
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
|
21
|
+
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
22
|
+
if probe.connect_ex(("127.0.0.1", port)) != 0:
|
|
23
|
+
return port
|
|
24
|
+
raise RuntimeError("No local dashboard port was available in the configured range.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _desktop_workspace_root() -> Path:
|
|
28
|
+
"""Choose the normal per-user application-data location for the desktop app."""
|
|
29
|
+
if sys.platform == "win32":
|
|
30
|
+
return Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local")) / "Poliora" / "workspace"
|
|
31
|
+
if sys.platform == "darwin":
|
|
32
|
+
return Path.home() / "Library" / "Application Support" / "Poliora" / "workspace"
|
|
33
|
+
data_home = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
|
|
34
|
+
return data_home / "poliora" / "workspace"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main() -> None:
|
|
38
|
+
"""Launch Poliora standalone GUI application."""
|
|
39
|
+
parser = argparse.ArgumentParser(description="Launch the Poliora local dashboard.")
|
|
40
|
+
parser.add_argument("--version", action="store_true", help="Show the standalone app version and exit.")
|
|
41
|
+
parser.add_argument("--port", type=int, default=8787, help="Preferred local dashboard port.")
|
|
42
|
+
parser.add_argument("--no-open", action="store_true", help="Do not open a browser window.")
|
|
43
|
+
arguments = parser.parse_args()
|
|
44
|
+
if arguments.version:
|
|
45
|
+
from poliora import __version__
|
|
46
|
+
|
|
47
|
+
print(f"poliora {__version__}")
|
|
48
|
+
return
|
|
49
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
50
|
+
try:
|
|
51
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
52
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
print("=" * 60)
|
|
57
|
+
print(" [Poliora] Starting Desktop AI Cost Optimizer...")
|
|
58
|
+
print("=" * 60)
|
|
59
|
+
|
|
60
|
+
workspace_root = _desktop_workspace_root()
|
|
61
|
+
workspace = load_workspace(workspace_root)
|
|
62
|
+
if not workspace.config_path.exists():
|
|
63
|
+
init_workspace(workspace_root, project="Desktop", monthly_budget_usd=1000.0)
|
|
64
|
+
|
|
65
|
+
port = _available_port(arguments.port)
|
|
66
|
+
url = f"http://127.0.0.1:{port}"
|
|
67
|
+
print(f"\n[+] Poliora engine running at {url}")
|
|
68
|
+
print("[+] Opening Desktop Application interface...")
|
|
69
|
+
|
|
70
|
+
# Open browser window automatically after 0.5s
|
|
71
|
+
if not arguments.no_open:
|
|
72
|
+
Timer(0.5, webbrowser.open_new_tab, args=(url,)).start()
|
|
73
|
+
|
|
74
|
+
# Serve local dashboard server
|
|
75
|
+
try:
|
|
76
|
+
run_dashboard(workspace_root, host="127.0.0.1", port=port)
|
|
77
|
+
except KeyboardInterrupt:
|
|
78
|
+
print("\nPoliora closed. Goodbye!")
|
|
79
|
+
sys.exit(0)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
main()
|
poliora/config.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Poliora configuration — Pydantic settings for every tunable knob."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal, Optional
|
|
7
|
+
|
|
8
|
+
from pydantic import Field, field_validator
|
|
9
|
+
from pydantic_settings import BaseSettings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PolioraConfig(BaseSettings):
|
|
13
|
+
"""Central configuration for a Poliora training run.
|
|
14
|
+
|
|
15
|
+
Values can be set via constructor kwargs, environment variables prefixed
|
|
16
|
+
with ``POLIORA_`` (e.g. ``POLIORA_MODEL_NAME``), or a ``.env`` file.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# ── Model & Data ─────────────────────────────────────────────────────
|
|
20
|
+
model_name: str = Field(
|
|
21
|
+
default="microsoft/phi-3-mini-4k-instruct",
|
|
22
|
+
description="Hugging Face model identifier or local path.",
|
|
23
|
+
)
|
|
24
|
+
dataset_path: Path = Field(
|
|
25
|
+
default=Path("data.csv"),
|
|
26
|
+
description="Path to a CSV dataset with at least a 'text' column.",
|
|
27
|
+
)
|
|
28
|
+
output_dir: Path = Field(
|
|
29
|
+
default=Path("tuned_model"),
|
|
30
|
+
description="Directory where the trained model will be saved.",
|
|
31
|
+
)
|
|
32
|
+
text_column: str = Field(
|
|
33
|
+
default="text",
|
|
34
|
+
description="Name of the text column in the CSV dataset.",
|
|
35
|
+
)
|
|
36
|
+
label_column: Optional[str] = Field(
|
|
37
|
+
default=None,
|
|
38
|
+
description="Optional name of the label column (for evaluation).",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# ── Training hyper-parameters ────────────────────────────────────────
|
|
42
|
+
epochs: int = Field(default=3, ge=1, description="Number of training epochs.")
|
|
43
|
+
batch_size: int = Field(default=4, ge=1, description="Per-device batch size.")
|
|
44
|
+
learning_rate: float = Field(default=2e-4, gt=0, description="Learning rate.")
|
|
45
|
+
max_seq_length: int = Field(default=512, ge=32, description="Maximum token sequence length.")
|
|
46
|
+
eval_split: float = Field(
|
|
47
|
+
default=0.1, ge=0.0, le=0.5,
|
|
48
|
+
description="Fraction of data to hold out for evaluation (0 = skip eval).",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# ── Optimisations ────────────────────────────────────────────────────
|
|
52
|
+
use_lora: bool = Field(default=True, description="Apply LoRA adapters for parameter-efficient training.")
|
|
53
|
+
lora_rank: int = Field(default=8, ge=1, description="LoRA rank (r).")
|
|
54
|
+
lora_alpha: int = Field(default=16, ge=1, description="LoRA alpha scaling factor.")
|
|
55
|
+
lora_dropout: float = Field(default=0.05, ge=0.0, le=1.0, description="LoRA dropout probability.")
|
|
56
|
+
|
|
57
|
+
use_quantization: bool = Field(default=True, description="Load model in quantised mode (bitsandbytes).")
|
|
58
|
+
quant_bits: Literal[4, 8] = Field(default=4, description="Quantisation bitwidth — 4 or 8.")
|
|
59
|
+
low_memory: bool = Field(default=False, description="Enable aggressive memory-saving tweaks.")
|
|
60
|
+
|
|
61
|
+
# ── Carbon tracking ──────────────────────────────────────────────────
|
|
62
|
+
track_carbon: bool = Field(default=True, description="Track energy & CO2 with CodeCarbon.")
|
|
63
|
+
country_iso_code: str = Field(default="USA", description="ISO 3166-1 alpha-3 country code for carbon intensity.")
|
|
64
|
+
|
|
65
|
+
# ── API keys (optional) ──────────────────────────────────────────────
|
|
66
|
+
electricity_maps_key: Optional[str] = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description="Electricity Maps API key for real-time grid carbon intensity.",
|
|
69
|
+
)
|
|
70
|
+
electricity_zone: str = Field(
|
|
71
|
+
default="US",
|
|
72
|
+
description="Electricity Maps zone code (e.g. 'US-CAL-CISO', 'DE', 'FR').",
|
|
73
|
+
)
|
|
74
|
+
grok_api_key: Optional[str] = Field(
|
|
75
|
+
default=None,
|
|
76
|
+
description="Grok / xAI API key for LLM-powered eco-tips.",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# ── Benchmark ────────────────────────────────────────────────────────
|
|
80
|
+
benchmark_output_dir: Path = Field(
|
|
81
|
+
default=Path("benchmark_output"),
|
|
82
|
+
description="Directory for benchmark reports.",
|
|
83
|
+
)
|
|
84
|
+
benchmark_export_json: Optional[Path] = Field(
|
|
85
|
+
default=None,
|
|
86
|
+
description="Path to write the benchmark JSON report.",
|
|
87
|
+
)
|
|
88
|
+
benchmark_export_csv: Optional[Path] = Field(
|
|
89
|
+
default=None,
|
|
90
|
+
description="Path to write the benchmark CSV report.",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
model_config = {"env_prefix": "POLIORA_", "env_file": ".env", "extra": "ignore"}
|
|
94
|
+
|
|
95
|
+
# ── Validators ───────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
@field_validator("dataset_path")
|
|
98
|
+
@classmethod
|
|
99
|
+
def _dataset_exists(cls, v: Path) -> Path:
|
|
100
|
+
"""Warn early if the dataset path doesn't exist."""
|
|
101
|
+
if not v.exists():
|
|
102
|
+
raise FileNotFoundError(f"Dataset not found: {v}")
|
|
103
|
+
return v
|
poliora/cost/__init__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""AI spend tracking primitives for Poliora."""
|
|
2
|
+
|
|
3
|
+
from poliora.cost.antigravity import (
|
|
4
|
+
AntigravityPluginInstall,
|
|
5
|
+
install_antigravity_plugin,
|
|
6
|
+
record_antigravity_hook_event,
|
|
7
|
+
)
|
|
8
|
+
from poliora.cost.budget import BudgetCheck, check_budget
|
|
9
|
+
from poliora.cost.capture import (
|
|
10
|
+
CapturedCall,
|
|
11
|
+
track_anthropic_call,
|
|
12
|
+
track_anthropic_client,
|
|
13
|
+
track_gemini_call,
|
|
14
|
+
track_gemini_client,
|
|
15
|
+
track_openai_call,
|
|
16
|
+
track_openai_client,
|
|
17
|
+
track_openai_compatible_call,
|
|
18
|
+
track_openai_compatible_client,
|
|
19
|
+
)
|
|
20
|
+
from poliora.cost.catalog import CatalogModel, ModelCatalog
|
|
21
|
+
from poliora.cost.codex_exec import CodexCli, find_codex_cli, record_codex_exec_event
|
|
22
|
+
from poliora.cost.companion import ConnectorConnection, ConnectorDefinition, ConnectorStore, connector_catalog
|
|
23
|
+
from poliora.cost.dashboard import ReportBranding, render_html_report, write_html_report
|
|
24
|
+
from poliora.cost.decisions import (
|
|
25
|
+
DECISION_STATUSES,
|
|
26
|
+
QUALITY_STATUSES,
|
|
27
|
+
DecisionStore,
|
|
28
|
+
SavingsDecision,
|
|
29
|
+
SavingsLedgerSummary,
|
|
30
|
+
summarize_decisions,
|
|
31
|
+
)
|
|
32
|
+
from poliora.cost.detection import DetectedTool, detect_local_tools
|
|
33
|
+
from poliora.cost.importer import (
|
|
34
|
+
CsvImportPreview,
|
|
35
|
+
CsvImportResult,
|
|
36
|
+
CsvRowIssue,
|
|
37
|
+
import_usage_csv,
|
|
38
|
+
import_usage_csv_text,
|
|
39
|
+
preview_usage_csv,
|
|
40
|
+
preview_usage_csv_text,
|
|
41
|
+
)
|
|
42
|
+
from poliora.cost.local_usage import (
|
|
43
|
+
DetectedPlan,
|
|
44
|
+
LocalUsageScan,
|
|
45
|
+
read_claude_code_usage,
|
|
46
|
+
read_codex_usage,
|
|
47
|
+
scan_local_usage,
|
|
48
|
+
)
|
|
49
|
+
from poliora.cost.pricing import ModelPricing, PricingRegistry, estimate_cost_usd
|
|
50
|
+
from poliora.cost.recommendations import Recommendation, generate_recommendations
|
|
51
|
+
from poliora.cost.reports import DailySpendRow, SpendAnomaly, UsageReport, build_usage_report
|
|
52
|
+
from poliora.cost.scanner import SystemScanReport, ToolScanResult, scan_system_ai_environment
|
|
53
|
+
from poliora.cost.scenarios import SavedScenario, ScenarioStore
|
|
54
|
+
from poliora.cost.sdk import log_anthropic_response, log_gemini_response, log_openai_response, log_usage
|
|
55
|
+
from poliora.cost.simulation import ModelSwitchSimulation, simulate_model_switch
|
|
56
|
+
from poliora.cost.sync import ModelSyncResult, sync_provider_models
|
|
57
|
+
from poliora.cost.usage import JsonlUsageStore, UsageEvent
|
|
58
|
+
from poliora.cost.workspace import PolioraWorkspace, init_workspace, load_workspace
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"CapturedCall",
|
|
62
|
+
"AntigravityPluginInstall",
|
|
63
|
+
"CatalogModel",
|
|
64
|
+
"CodexCli",
|
|
65
|
+
"ConnectorConnection",
|
|
66
|
+
"ConnectorDefinition",
|
|
67
|
+
"ConnectorStore",
|
|
68
|
+
"BudgetCheck",
|
|
69
|
+
"CsvImportResult",
|
|
70
|
+
"CsvImportPreview",
|
|
71
|
+
"CsvRowIssue",
|
|
72
|
+
"DECISION_STATUSES",
|
|
73
|
+
"DailySpendRow",
|
|
74
|
+
"DecisionStore",
|
|
75
|
+
"DetectedTool",
|
|
76
|
+
"PolioraWorkspace",
|
|
77
|
+
"JsonlUsageStore",
|
|
78
|
+
"ModelPricing",
|
|
79
|
+
"ModelCatalog",
|
|
80
|
+
"ModelSwitchSimulation",
|
|
81
|
+
"ModelSyncResult",
|
|
82
|
+
"PricingRegistry",
|
|
83
|
+
"Recommendation",
|
|
84
|
+
"ReportBranding",
|
|
85
|
+
"QUALITY_STATUSES",
|
|
86
|
+
"SavedScenario",
|
|
87
|
+
"SavingsDecision",
|
|
88
|
+
"SavingsLedgerSummary",
|
|
89
|
+
"ScenarioStore",
|
|
90
|
+
"SpendAnomaly",
|
|
91
|
+
"SystemScanReport",
|
|
92
|
+
"ToolScanResult",
|
|
93
|
+
"UsageEvent",
|
|
94
|
+
"UsageReport",
|
|
95
|
+
"build_usage_report",
|
|
96
|
+
"check_budget",
|
|
97
|
+
"connector_catalog",
|
|
98
|
+
"detect_local_tools",
|
|
99
|
+
"DetectedPlan",
|
|
100
|
+
"LocalUsageScan",
|
|
101
|
+
"read_claude_code_usage",
|
|
102
|
+
"read_codex_usage",
|
|
103
|
+
"scan_local_usage",
|
|
104
|
+
"estimate_cost_usd",
|
|
105
|
+
"find_codex_cli",
|
|
106
|
+
"generate_recommendations",
|
|
107
|
+
"init_workspace",
|
|
108
|
+
"install_antigravity_plugin",
|
|
109
|
+
"import_usage_csv",
|
|
110
|
+
"import_usage_csv_text",
|
|
111
|
+
"load_workspace",
|
|
112
|
+
"log_anthropic_response",
|
|
113
|
+
"log_gemini_response",
|
|
114
|
+
"log_openai_response",
|
|
115
|
+
"log_usage",
|
|
116
|
+
"render_html_report",
|
|
117
|
+
"preview_usage_csv",
|
|
118
|
+
"preview_usage_csv_text",
|
|
119
|
+
"record_codex_exec_event",
|
|
120
|
+
"record_antigravity_hook_event",
|
|
121
|
+
"scan_system_ai_environment",
|
|
122
|
+
"simulate_model_switch",
|
|
123
|
+
"sync_provider_models",
|
|
124
|
+
"summarize_decisions",
|
|
125
|
+
"track_anthropic_call",
|
|
126
|
+
"track_anthropic_client",
|
|
127
|
+
"track_gemini_call",
|
|
128
|
+
"track_gemini_client",
|
|
129
|
+
"track_openai_call",
|
|
130
|
+
"track_openai_client",
|
|
131
|
+
"track_openai_compatible_call",
|
|
132
|
+
"track_openai_compatible_client",
|
|
133
|
+
"write_html_report",
|
|
134
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Supported Antigravity activity capture and plugin installation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shlex
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from poliora.cost.usage import JsonlUsageStore, UsageEvent
|
|
16
|
+
from poliora.cost.workspace import load_workspace
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class AntigravityPluginInstall:
|
|
21
|
+
"""Location and scope of an installed Antigravity plugin."""
|
|
22
|
+
|
|
23
|
+
path: Path
|
|
24
|
+
scope: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def record_antigravity_hook_event(
|
|
28
|
+
payload: dict[str, Any],
|
|
29
|
+
*,
|
|
30
|
+
event_name: str,
|
|
31
|
+
root: str | Path | None = None,
|
|
32
|
+
) -> UsageEvent | None:
|
|
33
|
+
"""Record metadata exposed by an official Antigravity lifecycle hook.
|
|
34
|
+
|
|
35
|
+
Antigravity hooks do not currently expose model IDs or token totals. The
|
|
36
|
+
resulting event is therefore activity-only and is excluded from spend.
|
|
37
|
+
"""
|
|
38
|
+
if event_name != "pre-invocation":
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
workspace_root = Path(root).resolve() if root is not None else _workspace_root(payload)
|
|
42
|
+
workspace = load_workspace(workspace_root)
|
|
43
|
+
invocation_num = _non_negative_int(payload.get("invocationNum"), "invocationNum")
|
|
44
|
+
conversation_id = str(payload.get("conversationId") or "")
|
|
45
|
+
trace_id = _anonymous_trace_id(conversation_id) if conversation_id else None
|
|
46
|
+
|
|
47
|
+
event = UsageEvent(
|
|
48
|
+
provider="google",
|
|
49
|
+
model="antigravity-managed",
|
|
50
|
+
input_tokens=0,
|
|
51
|
+
output_tokens=0,
|
|
52
|
+
cost_usd=0.0,
|
|
53
|
+
operation="antigravity-agent",
|
|
54
|
+
project=workspace.project,
|
|
55
|
+
trace_id=trace_id,
|
|
56
|
+
metadata={
|
|
57
|
+
"source": "antigravity-pre-invocation-hook",
|
|
58
|
+
"billing_basis": "antigravity-subscription-activity",
|
|
59
|
+
"invocation_num": invocation_num,
|
|
60
|
+
"content_collected": False,
|
|
61
|
+
"token_usage_available": False,
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
JsonlUsageStore(workspace.usage_path).append(event)
|
|
65
|
+
return event
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def install_antigravity_plugin(
|
|
69
|
+
root: str | Path = ".",
|
|
70
|
+
*,
|
|
71
|
+
scope: str = "workspace",
|
|
72
|
+
force: bool = False,
|
|
73
|
+
) -> AntigravityPluginInstall:
|
|
74
|
+
"""Install Poliora into one of Antigravity's documented plugin locations."""
|
|
75
|
+
plugin_path = _plugin_path(Path(root).resolve(), scope)
|
|
76
|
+
manifest_path = plugin_path / "plugin.json"
|
|
77
|
+
if manifest_path.exists() and not force:
|
|
78
|
+
try:
|
|
79
|
+
existing = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
80
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
81
|
+
raise ValueError(f"Refusing to replace an unreadable plugin at {plugin_path}.") from error
|
|
82
|
+
if existing.get("name") != "poliora":
|
|
83
|
+
raise ValueError(f"A different plugin already exists at {plugin_path}. Use --force to replace it.")
|
|
84
|
+
|
|
85
|
+
files = {
|
|
86
|
+
"plugin.json": json.dumps({"name": "poliora"}, indent=2) + "\n",
|
|
87
|
+
"hooks.json": json.dumps(_hook_config(), indent=2) + "\n",
|
|
88
|
+
"skills/poliora-cost/SKILL.md": _skill_markdown(),
|
|
89
|
+
"rules/poliora-privacy.md": _privacy_rule_markdown(),
|
|
90
|
+
}
|
|
91
|
+
for relative_path, content in files.items():
|
|
92
|
+
destination = plugin_path / relative_path
|
|
93
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
destination.write_text(content, encoding="utf-8")
|
|
95
|
+
return AntigravityPluginInstall(path=plugin_path, scope=scope)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _workspace_root(payload: dict[str, Any]) -> Path:
|
|
99
|
+
paths = payload.get("workspacePaths")
|
|
100
|
+
if isinstance(paths, list):
|
|
101
|
+
candidates = [Path(item).resolve() for item in paths if isinstance(item, str) and item.strip()]
|
|
102
|
+
for candidate in candidates:
|
|
103
|
+
if (candidate / ".poliora" / "config.json").exists():
|
|
104
|
+
return candidate
|
|
105
|
+
if candidates:
|
|
106
|
+
return candidates[0]
|
|
107
|
+
return Path.cwd().resolve()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _plugin_path(root: Path, scope: str) -> Path:
|
|
111
|
+
if scope == "workspace":
|
|
112
|
+
return root / ".agents" / "plugins" / "poliora"
|
|
113
|
+
if scope == "editor-global":
|
|
114
|
+
return Path.home() / ".gemini" / "config" / "plugins" / "poliora"
|
|
115
|
+
if scope == "cli-global":
|
|
116
|
+
return Path.home() / ".gemini" / "antigravity-cli" / "plugins" / "poliora"
|
|
117
|
+
raise ValueError("scope must be workspace, editor-global, or cli-global")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _hook_config() -> dict[str, object]:
|
|
121
|
+
return {
|
|
122
|
+
"poliora-activity": {
|
|
123
|
+
"PreInvocation": [
|
|
124
|
+
{
|
|
125
|
+
"type": "command",
|
|
126
|
+
"command": _hook_command(),
|
|
127
|
+
"timeout": 10,
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _hook_command() -> str:
|
|
135
|
+
arguments = [sys.executable, "-m", "poliora.main", "antigravity-hook", "--event", "pre-invocation"]
|
|
136
|
+
if os.name == "nt":
|
|
137
|
+
return subprocess.list2cmdline(arguments)
|
|
138
|
+
return shlex.join(arguments)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _skill_markdown() -> str:
|
|
142
|
+
return """---
|
|
143
|
+
name: poliora-cost
|
|
144
|
+
description: Inspect and improve local AI usage with Poliora.
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
# Poliora Cost Operations
|
|
148
|
+
|
|
149
|
+
Use Poliora when the user asks about AI spend, model usage, budgets, or routing savings.
|
|
150
|
+
|
|
151
|
+
- Run `poliora report` for the local usage summary.
|
|
152
|
+
- Run `poliora recommend` for prioritized savings opportunities.
|
|
153
|
+
- Run `poliora dashboard` only when the user asks to open the local dashboard.
|
|
154
|
+
- For Gemini API code, wrap `google.genai.Client` with `poliora.cost.track_gemini_client`.
|
|
155
|
+
- Never infer token counts, model IDs, or dollar cost from Antigravity activity-only events.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _privacy_rule_markdown() -> str:
|
|
160
|
+
return """# Poliora Privacy
|
|
161
|
+
|
|
162
|
+
Poliora may record lifecycle metadata and provider-reported usage totals. Do not send prompt text,
|
|
163
|
+
responses, source files, commands, transcript contents, credentials, or file-change content to Poliora.
|
|
164
|
+
Antigravity subscription activity must remain separate from measured or estimated API spend.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _anonymous_trace_id(conversation_id: str) -> str:
|
|
169
|
+
digest = hashlib.sha256(conversation_id.encode("utf-8")).hexdigest()
|
|
170
|
+
return f"antigravity-{digest[:16]}"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _non_negative_int(value: Any, field: str) -> int:
|
|
174
|
+
if isinstance(value, bool):
|
|
175
|
+
raise ValueError(f"Antigravity {field} must be a non-negative integer.")
|
|
176
|
+
try:
|
|
177
|
+
parsed = int(value)
|
|
178
|
+
except (TypeError, ValueError) as error:
|
|
179
|
+
raise ValueError(f"Antigravity {field} must be a non-negative integer.") from error
|
|
180
|
+
if parsed < 0:
|
|
181
|
+
raise ValueError(f"Antigravity {field} must be a non-negative integer.")
|
|
182
|
+
return parsed
|
poliora/cost/budget.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Budget checks for local reports and CI gates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
|
|
7
|
+
from poliora.cost.reports import UsageReport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class BudgetCheck:
|
|
12
|
+
"""Result of comparing projected spend against a budget."""
|
|
13
|
+
|
|
14
|
+
passed: bool
|
|
15
|
+
projected_monthly_usd: float
|
|
16
|
+
limit_usd: float
|
|
17
|
+
used_pct: float
|
|
18
|
+
remaining_usd: float
|
|
19
|
+
message: str
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> dict[str, object]:
|
|
22
|
+
"""Serialize budget check."""
|
|
23
|
+
return asdict(self)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def check_budget(report: UsageReport, *, limit_usd: float, warn_at_pct: float = 80.0) -> BudgetCheck:
|
|
27
|
+
"""Check whether projected monthly spend is within a budget."""
|
|
28
|
+
if limit_usd <= 0:
|
|
29
|
+
raise ValueError("Budget limit must be greater than zero.")
|
|
30
|
+
if warn_at_pct <= 0:
|
|
31
|
+
raise ValueError("warn_at_pct must be greater than zero.")
|
|
32
|
+
|
|
33
|
+
projected = report.projected_monthly_usd
|
|
34
|
+
used_pct = round(projected / limit_usd * 100, 2)
|
|
35
|
+
remaining = round(limit_usd - projected, 2)
|
|
36
|
+
passed = projected <= limit_usd
|
|
37
|
+
|
|
38
|
+
if not passed:
|
|
39
|
+
message = f"Projected AI spend is ${projected:,.2f}, above the ${limit_usd:,.2f} limit."
|
|
40
|
+
elif used_pct >= warn_at_pct:
|
|
41
|
+
message = f"Projected AI spend is at {used_pct:.1f}% of budget. Watch this closely."
|
|
42
|
+
else:
|
|
43
|
+
message = f"Projected AI spend is within budget with ${remaining:,.2f} remaining."
|
|
44
|
+
|
|
45
|
+
return BudgetCheck(
|
|
46
|
+
passed=passed,
|
|
47
|
+
projected_monthly_usd=projected,
|
|
48
|
+
limit_usd=limit_usd,
|
|
49
|
+
used_pct=used_pct,
|
|
50
|
+
remaining_usd=remaining,
|
|
51
|
+
message=message,
|
|
52
|
+
)
|