pari-cli 0.1.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.
Files changed (81) hide show
  1. pari/__init__.py +3 -0
  2. pari/agent/__init__.py +14 -0
  3. pari/agent/loop.py +35 -0
  4. pari/agent/reflection.py +39 -0
  5. pari/agent/runtime.py +91 -0
  6. pari/agent/subagent.py +34 -0
  7. pari/cli.py +132 -0
  8. pari/config.py +23 -0
  9. pari/context/__init__.py +15 -0
  10. pari/context/cache.py +15 -0
  11. pari/context/chunks.py +40 -0
  12. pari/context/compaction.py +21 -0
  13. pari/context/epoch.py +32 -0
  14. pari/context/warmer.py +32 -0
  15. pari/edit/__init__.py +20 -0
  16. pari/edit/cascade.py +22 -0
  17. pari/edit/diff.py +8 -0
  18. pari/edit/dmp.py +15 -0
  19. pari/edit/exact.py +37 -0
  20. pari/edit/git_osr.py +14 -0
  21. pari/edit/indenter.py +12 -0
  22. pari/eval/__init__.py +14 -0
  23. pari/eval/judge.py +21 -0
  24. pari/eval/logging.py +19 -0
  25. pari/eval/metrics.py +22 -0
  26. pari/eval/runner.py +34 -0
  27. pari/fs/__init__.py +16 -0
  28. pari/fs/atomic.py +53 -0
  29. pari/fs/path.py +28 -0
  30. pari/fs/queue.py +22 -0
  31. pari/intelligence/__init__.py +15 -0
  32. pari/intelligence/ast.py +25 -0
  33. pari/intelligence/cache.py +21 -0
  34. pari/intelligence/ppr.py +21 -0
  35. pari/intelligence/repomap.py +21 -0
  36. pari/intelligence/search.py +33 -0
  37. pari/output/__init__.py +14 -0
  38. pari/output/parser.py +14 -0
  39. pari/output/spooler.py +33 -0
  40. pari/output/truncation.py +20 -0
  41. pari/providers/__init__.py +24 -0
  42. pari/providers/anthropic.py +23 -0
  43. pari/providers/base.py +56 -0
  44. pari/providers/deepseek.py +24 -0
  45. pari/providers/mock.py +46 -0
  46. pari/providers/ollama.py +25 -0
  47. pari/providers/openai.py +23 -0
  48. pari/routing/__init__.py +16 -0
  49. pari/routing/anti_verbosity.py +13 -0
  50. pari/routing/classifier.py +24 -0
  51. pari/routing/router.py +23 -0
  52. pari/sandbox/__init__.py +16 -0
  53. pari/sandbox/base.py +74 -0
  54. pari/sandbox/bwrap.py +39 -0
  55. pari/sandbox/dispatcher.py +31 -0
  56. pari/sandbox/e2b.py +32 -0
  57. pari/sandbox/process.py +38 -0
  58. pari/sandbox/seatbelt.py +32 -0
  59. pari/session/__init__.py +15 -0
  60. pari/session/checkpoint.py +24 -0
  61. pari/session/dag.py +34 -0
  62. pari/session/events.py +50 -0
  63. pari/session/logger.py +41 -0
  64. pari/session/undo.py +99 -0
  65. pari/tools/__init__.py +14 -0
  66. pari/tools/base.py +35 -0
  67. pari/tools/bash.py +24 -0
  68. pari/tools/fs.py +53 -0
  69. pari/tools/registry.py +36 -0
  70. pari/tools/search.py +38 -0
  71. pari/tui/__init__.py +28 -0
  72. pari/tui/app.py +355 -0
  73. pari/tui/commands.py +45 -0
  74. pari/tui/diff_view.py +17 -0
  75. pari/tui/status.py +65 -0
  76. pari_cli-0.1.0.dist-info/METADATA +167 -0
  77. pari_cli-0.1.0.dist-info/RECORD +81 -0
  78. pari_cli-0.1.0.dist-info/WHEEL +5 -0
  79. pari_cli-0.1.0.dist-info/entry_points.txt +2 -0
  80. pari_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  81. pari_cli-0.1.0.dist-info/top_level.txt +1 -0
pari/edit/indenter.py ADDED
@@ -0,0 +1,12 @@
1
+ """PARI Layer 5: Relative Indentation Normalizer.
2
+
3
+ Architectural Purpose:
4
+ LLMs frequently emit replacement blocks whose leading indentation is offset by
5
+ 2 or 4 spaces relative to the target site.
6
+ The RelativeIndenter calculates the delta between the indentation of the first line
7
+ of the search block and the replacement block, normalizing subsequent lines.
8
+ """
9
+
10
+ def normalize_relative_indentation(search_block: str, replace_block: str) -> str:
11
+ """Normalize indentation of replacement block to match search block site."""
12
+ raise NotImplementedError
pari/eval/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """PARI Layer 10: Evaluation & Observability Harness.
2
+
3
+ Architectural Purpose:
4
+ Closes the universal problem of "vibe coding" (subjective prompt tweaking without objective regression tests).
5
+ Provides a deterministic local micro-eval suite ($3-$5 per run) and structured observability.
6
+
7
+ Subsystems:
8
+ - runner: Micro-eval benchmark runner executing deterministic test cases
9
+ - judge: Cross-model verification judge (Graptile principle: never evaluate with same model family)
10
+ - metrics: Quantitative benchmarks (pass rates, token costs, turn counts)
11
+ - logging: Structured JSON logging (structlog format) for full execution telemetry
12
+ """
13
+
14
+ # Placeholder imports will be exported once implementations are added
pari/eval/judge.py ADDED
@@ -0,0 +1,21 @@
1
+ """PARI Layer 10: Cross-Model Verification Judge.
2
+
3
+ Architectural Purpose:
4
+ Implements the Graptile Principle:
5
+ By default, PARI-generated code is evaluated by a different model family than the one
6
+ that generated it (e.g. Claude generation evaluated by GPT-4o-mini).
7
+ Prevents self-evaluation confirmation bias.
8
+ """
9
+
10
+ from typing import Any, Dict
11
+
12
+
13
+ class CrossModelJudge:
14
+ """Evaluates patch correctness using an independent model family."""
15
+
16
+ def __init__(self, judge_model: str = "gpt-4o-mini"):
17
+ self.judge_model = judge_model
18
+
19
+ async def evaluate_patch(self, problem_statement: str, patch_diff: str) -> Dict[str, Any]:
20
+ """Ask independent judge model to evaluate if patch correctly resolves the issue."""
21
+ raise NotImplementedError
pari/eval/logging.py ADDED
@@ -0,0 +1,19 @@
1
+ """PARI Layer 10: Structured Observability Logging.
2
+
3
+ Emits structured JSON events for monitoring, analysis, and debugging.
4
+ Logs events such as tool_call, edit_applied, sandbox_exec, turn_complete.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any, Optional
9
+
10
+
11
+ class StructuredLogger:
12
+ """Emits newline-delimited JSON logs to stdout or a designated log file."""
13
+
14
+ def __init__(self, log_file: Optional[Path] = None):
15
+ self.log_file = log_file
16
+
17
+ def emit(self, event_name: str, **kwargs: Any):
18
+ """Emit structured telemetry event."""
19
+ raise NotImplementedError
pari/eval/metrics.py ADDED
@@ -0,0 +1,22 @@
1
+ """PARI Layer 10: Quantitative Evaluation Metrics.
2
+
3
+ Computes pass rate, mean cost per resolved task, mean turns, and duration.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Any, Dict, List
8
+
9
+
10
+ @dataclass
11
+ class EvalMetrics:
12
+ total_tasks: int
13
+ passed_tasks: int
14
+ pass_rate: float
15
+ total_cost_usd: float
16
+ mean_cost_per_task: float
17
+ mean_turns: float
18
+
19
+
20
+ def calculate_eval_metrics(results: List[Dict[str, Any]]) -> EvalMetrics:
21
+ """Compute aggregate benchmark metrics across test runs."""
22
+ raise NotImplementedError
pari/eval/runner.py ADDED
@@ -0,0 +1,34 @@
1
+ """PARI Layer 10: Micro-Eval Benchmark Runner.
2
+
3
+ Runs a local micro-benchmark suite ($3-$5 per run) against isolated git worktrees
4
+ and executes deterministic pytest suites to compute exact pass rates.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional
10
+
11
+
12
+ @dataclass
13
+ class TestCase:
14
+ id: str
15
+ issue_description: str
16
+ test_command: str
17
+ base_commit: Optional[str] = None
18
+
19
+
20
+ @dataclass
21
+ class EvalReport:
22
+ metrics: Any
23
+ case_results: List[Dict[str, Any]]
24
+
25
+
26
+ class MicroEvalRunner:
27
+ """Executes evaluation benchmarks across test cases."""
28
+
29
+ def __init__(self, workspace: Path):
30
+ self.workspace = workspace.resolve()
31
+
32
+ def run_suite(self, test_cases: List[TestCase]) -> EvalReport:
33
+ """Run benchmark suite and compile report."""
34
+ raise NotImplementedError
pari/fs/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """PARI Layer 6: Concurrency and File Safety Engine.
2
+
3
+ Architectural Purpose:
4
+ Closes Claude Code F3 (File write race conditions tearing files during parallel operations)
5
+ and path traversal / prefix collision vulnerabilities.
6
+
7
+ Subsystems:
8
+ - path: Canonical path resolution via Path.resolve().relative_to()
9
+ - atomic: Atomic write using temporary file + os.replace() + directory os.fsync()
10
+ - queue: FileMutationQueue maintaining per-canonical-realpath asyncio locks
11
+ """
12
+
13
+ from pari.fs.path import resolve_safe_path
14
+ from pari.fs.atomic import atomic_write, read_file
15
+
16
+ __all__ = ["resolve_safe_path", "atomic_write", "read_file"]
pari/fs/atomic.py ADDED
@@ -0,0 +1,53 @@
1
+ """PARI Layer 6: Atomic File Write and Sync Protocol.
2
+
3
+ Architectural Purpose:
4
+ Closes Claude Code F1 & F3.
5
+ Writes content to a temporary file in the same directory (guaranteeing same filesystem device),
6
+ calls os.fsync() on the file descriptor, atomically replaces the target via os.replace(),
7
+ and flushes the parent directory entry with fsync on POSIX.
8
+ """
9
+
10
+ import os
11
+ import tempfile
12
+ from pathlib import Path
13
+
14
+
15
+ def atomic_write(target_path: Path, content: str) -> None:
16
+ """Atomically write content to target_path, preventing partial/torn writes."""
17
+ target = target_path.resolve()
18
+ target.parent.mkdir(parents=True, exist_ok=True)
19
+
20
+ # Write to a temporary file in the same directory (same filesystem device)
21
+ fd, temp_path = tempfile.mkstemp(dir=target.parent, prefix=".pari_tmp_")
22
+ try:
23
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
24
+ f.write(content)
25
+ f.flush()
26
+ os.fsync(f.fileno())
27
+
28
+ # Atomic replacement
29
+ os.replace(temp_path, target)
30
+
31
+ # Sync parent directory on POSIX
32
+ if os.name == "posix":
33
+ try:
34
+ parent_fd = os.open(str(target.parent), os.O_RDONLY | os.O_DIRECTORY)
35
+ try:
36
+ os.fsync(parent_fd)
37
+ finally:
38
+ os.close(parent_fd)
39
+ except (OSError, PermissionError):
40
+ pass
41
+ except Exception:
42
+ if os.path.exists(temp_path):
43
+ try:
44
+ os.remove(temp_path)
45
+ except OSError:
46
+ pass
47
+ raise
48
+
49
+
50
+ def read_file(file_path: Path) -> str:
51
+ """Read full text content of a file."""
52
+ return file_path.resolve().read_text(encoding="utf-8")
53
+
pari/fs/path.py ADDED
@@ -0,0 +1,28 @@
1
+ """PARI Layer 6: Safe Canonical Path Resolution.
2
+
3
+ Architectural Purpose:
4
+ Prevents path traversal attacks (e.g. ../../etc/shadow) and path prefix collisions
5
+ (e.g., /workspace_evil matching /workspace in string-prefix checks).
6
+ Enforces that resolved canonical paths reside strictly within authorized workspace roots.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Union
11
+
12
+
13
+ def resolve_safe_path(workspace_root: Union[str, Path], target_path: Union[str, Path]) -> Path:
14
+ """Resolve a target path safely relative to the workspace root."""
15
+ root = Path(workspace_root).resolve()
16
+ target = Path(target_path)
17
+ if not target.is_absolute():
18
+ resolved = (root / target).resolve()
19
+ else:
20
+ resolved = target.resolve()
21
+
22
+ try:
23
+ resolved.relative_to(root)
24
+ except ValueError:
25
+ raise PermissionError(f"Path traversal rejected: '{target_path}' is outside workspace '{workspace_root}'")
26
+
27
+ return resolved
28
+
pari/fs/queue.py ADDED
@@ -0,0 +1,22 @@
1
+ """PARI Layer 6: File Mutation Queue.
2
+
3
+ Architectural Purpose:
4
+ Harvested from Pi's file-mutation-queue.ts.
5
+ Maintains a registry of asyncio.Lock primitives keyed by canonical realpath.
6
+ Serializes concurrent writes to the same physical file while allowing parallel
7
+ writes to distinct files.
8
+ """
9
+
10
+ import asyncio
11
+ from pathlib import Path
12
+
13
+
14
+ class FileMutationQueue:
15
+ """Serializes concurrent write mutations per canonical filesystem path."""
16
+
17
+ def __init__(self):
18
+ pass
19
+
20
+ async def get_lock(self, file_path: Path) -> asyncio.Lock:
21
+ """Get canonical per-path lock."""
22
+ raise NotImplementedError
@@ -0,0 +1,15 @@
1
+ """PARI Layer 3: Codebase Intelligence & Navigation Layer.
2
+
3
+ Architectural Purpose:
4
+ Closes Pi F3 (Blind file traversal loops requiring 8+ grep turns) and
5
+ Aider F4 (Personalized PageRank hub distortion from generic utility sinks).
6
+
7
+ Subsystems:
8
+ - search: Fast file globbing & ripgrep substring search (v1 primary navigation)
9
+ - ast: Tree-sitter AST symbol extractor across Python, TypeScript, Go, Rust
10
+ - ppr: Personalized PageRank graph ranking with hub suppression (>50 inbound refs dampened)
11
+ - repomap: 2,048-token binary-search packing for the static context repo map
12
+ - cache: Worktree-scoped SQLite symbol cache preventing lock collisions
13
+ """
14
+
15
+ # Placeholder imports will be exported once implementations are added
@@ -0,0 +1,25 @@
1
+ """PARI Layer 3: Tree-sitter AST Symbol Extractor.
2
+
3
+ Architectural Purpose:
4
+ Extracts high-signal structural definitions (classes, functions, methods, imports)
5
+ from source code without reading full file bodies into prompt context.
6
+ Degrades gracefully to regex/heuristic parsing when tree-sitter bindings are not installed.
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import List
12
+
13
+
14
+ @dataclass
15
+ class SymbolInfo:
16
+ file_path: str
17
+ symbol_name: str
18
+ kind: str # "class", "function", "method", "variable"
19
+ line_number: int
20
+ signature: str
21
+
22
+
23
+ def extract_symbols(file_path: Path, content: str) -> List[SymbolInfo]:
24
+ """Extract symbols from source code."""
25
+ raise NotImplementedError
@@ -0,0 +1,21 @@
1
+ """PARI Layer 3: Worktree-Scoped Symbol Cache.
2
+
3
+ Architectural Purpose:
4
+ Closes Aider F6 (Diskcache SQLite lock collisions across git worktrees).
5
+ Scans and caches AST tags with SQLite database files scoped explicitly to the
6
+ unique worktree root path, eliminating multi-session database locking errors.
7
+ """
8
+
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+
13
+ class WorktreeSymbolCache:
14
+ """Manages cached symbol index keyed specifically to workspace path and file mtimes."""
15
+
16
+ def __init__(self, workspace: Path, cache_dir: Optional[Path] = None):
17
+ self.workspace = workspace.resolve()
18
+
19
+ def get_db_path(self) -> Path:
20
+ """Return path to workspace-isolated SQLite cache file."""
21
+ raise NotImplementedError
@@ -0,0 +1,21 @@
1
+ """PARI Layer 3: Personalized PageRank with Hub Suppression.
2
+
3
+ Architectural Purpose:
4
+ Closes Aider F4 (PageRank hub distortion).
5
+ In naive PageRank, generic utilities like `utils.py` or `helpers.py` accumulate
6
+ hundreds of inbound edges and crowd out crucial business logic in a 2,048-token budget.
7
+ PARI applies a hub suppression dampener: any node with >50 inbound references
8
+ has its rank dynamically dampened, prioritizing domain-specific classes and interfaces.
9
+ """
10
+
11
+ from typing import Dict, List, Set, Tuple
12
+
13
+
14
+ def compute_personalized_pagerank(
15
+ graph_edges: List[Tuple[str, str]],
16
+ personalized_targets: Set[str],
17
+ damping_factor: float = 0.85,
18
+ hub_threshold: int = 50,
19
+ ) -> Dict[str, float]:
20
+ """Compute personalized PageRank scores with dampening for high-indegree utility hubs."""
21
+ raise NotImplementedError
@@ -0,0 +1,21 @@
1
+ """PARI Layer 3: Repo Map Generator & Token Packer.
2
+
3
+ Architectural Purpose:
4
+ Packs codebase structural symbols into a strict token budget (default 2,048 tokens)
5
+ using binary-search selection based on PPR rankings.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import List, Optional
10
+
11
+
12
+ class RepoMapGenerator:
13
+ """Generates condensed repository map from parsed source symbols."""
14
+
15
+ def __init__(self, workspace: Path, max_tokens: int = 2048):
16
+ self.workspace = workspace.resolve()
17
+ self.max_tokens = max_tokens
18
+
19
+ def generate(self, focus_files: Optional[List[str]] = None) -> str:
20
+ """Scan workspace, extract symbols, and format condensed outline."""
21
+ raise NotImplementedError
@@ -0,0 +1,33 @@
1
+ """PARI Layer 3: Substring & Globbing Search Engine (v1 Baseline Navigation).
2
+
3
+ Architectural Purpose:
4
+ Provides fast, deterministic code search without requiring C-extension compilation.
5
+ Enforces ignore boundaries (.git, .venv, node_modules, build artifacts).
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import List, Optional
11
+
12
+
13
+ @dataclass
14
+ class GrepMatch:
15
+ file_path: str
16
+ line_number: int
17
+ line_content: str
18
+
19
+
20
+ def find_files(workspace_root: Path, pattern: str = "*", max_results: int = 100) -> List[str]:
21
+ """Find files in workspace matching a glob pattern, respecting ignore rules."""
22
+ raise NotImplementedError
23
+
24
+
25
+ def grep_search(
26
+ workspace_root: Path,
27
+ query: str,
28
+ is_regex: bool = False,
29
+ path_filter: str = "*",
30
+ max_matches: int = 50,
31
+ ) -> List[GrepMatch]:
32
+ """Search for query across files in workspace, returning matches with line numbers."""
33
+ raise NotImplementedError
@@ -0,0 +1,14 @@
1
+ """PARI Layer 7: Output Handling and Context Hygiene.
2
+
3
+ Architectural Purpose:
4
+ Closes the #1 failure mode in the Failure Mode Atlas: Unbounded stdout poisoning.
5
+ Prevents large tool outputs (e.g. cat massive.log, npm install, pytest) from flooding
6
+ the model's context window and displacing core instructions.
7
+
8
+ Subsystems:
9
+ - spooler: 50KB / 2k-line disk spooler storing raw outputs in sidecar files
10
+ - truncation: Head/tail extraction preserving stack traces and return headers
11
+ - parser: Structured test failure extractors (pytest, jest, cargo)
12
+ """
13
+
14
+ # Placeholder imports will be exported once implementations are added
pari/output/parser.py ADDED
@@ -0,0 +1,14 @@
1
+ """PARI Layer 7: Structured Test Output Parser.
2
+
3
+ Architectural Purpose:
4
+ Extracts high-signal test failure summaries from raw test output (pytest, jest, cargo),
5
+ allowing the model to immediately grasp failing assertions without digesting
6
+ 100s of lines of framework internals.
7
+ """
8
+
9
+ from typing import List
10
+
11
+
12
+ def parse_test_failures(raw_output: str) -> List[str]:
13
+ """Parse failure lines from test runner output."""
14
+ raise NotImplementedError
pari/output/spooler.py ADDED
@@ -0,0 +1,33 @@
1
+ """PARI Layer 7: Managed Output Disk Spooler.
2
+
3
+ Architectural Purpose:
4
+ Closes Codex F4 (RSS bloat on large outputs) & OpenCode 50KB output cap.
5
+ Streams subprocess output into a temporary disk sidecar file.
6
+ If output exceeds max_bytes (default 50KB) or max_lines (default 2,000 lines),
7
+ it generates a sanitized head/tail summary for prompt injection while preserving
8
+ the full output on disk for on-demand inspection via /spool command.
9
+ """
10
+
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+
16
+ @dataclass
17
+ class SpoolResult:
18
+ context_text: str
19
+ sidecar_path: Path
20
+ total_bytes: int
21
+ total_lines: int
22
+ was_truncated: bool
23
+
24
+
25
+ class ManagedOutputSpooler:
26
+ """Spools process output to disk sidecars and extracts safe context summaries."""
27
+
28
+ def __init__(self, spool_dir: Optional[Path] = None, max_bytes: int = 50 * 1024):
29
+ self.max_bytes = max_bytes
30
+
31
+ def spool(self, raw_output: str, command_tag: str = "cmd") -> SpoolResult:
32
+ """Spool output to disk sidecar and return truncated context text if large."""
33
+ raise NotImplementedError
@@ -0,0 +1,20 @@
1
+ """PARI Layer 7: Head/Tail Truncation with Context Preservation.
2
+
3
+ Architectural Purpose:
4
+ When output exceeds the context budget:
5
+ 1. Keeps the first N lines (command launch, build headers).
6
+ 2. Keeps the last M lines (stack traces, exit assertions, summary errors).
7
+ 3. Injects a clear omission notice linking to the disk sidecar containing full raw output.
8
+ """
9
+
10
+ from typing import Tuple
11
+
12
+
13
+ def extract_head_tail(
14
+ text: str,
15
+ head_lines: int = 50,
16
+ tail_lines: int = 100,
17
+ sidecar_path: str = "",
18
+ ) -> Tuple[str, bool]:
19
+ """Truncate text by extracting head and tail sections if line count is high."""
20
+ raise NotImplementedError
@@ -0,0 +1,24 @@
1
+ """PARI Multi-Provider LLM Integration Layer.
2
+
3
+ Architectural Purpose:
4
+ Closes Antigravity F5 (Hard Google API lock-in) and Claude Code Anthropic exclusivity.
5
+ Provides unified model abstraction across Anthropic Claude, OpenAI, DeepSeek, local Ollama,
6
+ and an offline fallback provider for local testing.
7
+ """
8
+
9
+ from typing import Optional
10
+ from pari.providers.base import LLMProvider, ProviderMessage, ModelResponse, ToolCall
11
+
12
+
13
+ def get_provider(model_name: Optional[str] = None) -> LLMProvider:
14
+ """Instantiate the best available LLM provider based on model name and environment."""
15
+ raise NotImplementedError
16
+
17
+
18
+ __all__ = [
19
+ "LLMProvider",
20
+ "ProviderMessage",
21
+ "ModelResponse",
22
+ "ToolCall",
23
+ "get_provider",
24
+ ]
@@ -0,0 +1,23 @@
1
+ """PARI Providers: Anthropic Claude Adapter.
2
+
3
+ Supports Claude 3.7 Sonnet, Claude 3.5 Sonnet, and Claude 3.5 Haiku with prompt caching.
4
+ """
5
+
6
+ from typing import Any, Dict, List, Optional
7
+ from pari.providers.base import LLMProvider, ModelResponse, ProviderMessage
8
+
9
+
10
+ class AnthropicProvider(LLMProvider):
11
+ """Adapter for Anthropic Messages API."""
12
+
13
+ def __init__(self, model_name: str = "claude-3-7-sonnet-20250219", api_key: Optional[str] = None):
14
+ super().__init__(model_name, api_key)
15
+
16
+ async def complete(
17
+ self,
18
+ messages: List[ProviderMessage],
19
+ tools: Optional[List[Dict[str, Any]]] = None,
20
+ temperature: float = 0.0,
21
+ ) -> ModelResponse:
22
+ """Call Anthropic Messages API with prompt caching."""
23
+ raise NotImplementedError
pari/providers/base.py ADDED
@@ -0,0 +1,56 @@
1
+ """PARI Providers: Base LLM Interface.
2
+
3
+ Defines the contract for calling LLM inference APIs with tool calling and prompt caching.
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Dict, List, Optional
9
+
10
+
11
+ @dataclass
12
+ class ToolCall:
13
+ id: str
14
+ name: str
15
+ arguments: Dict[str, Any]
16
+
17
+
18
+ @dataclass
19
+ class ProviderMessage:
20
+ role: str # "system", "user", "assistant", "tool"
21
+ content: str
22
+ tool_calls: Optional[List[ToolCall]] = None
23
+ tool_call_id: Optional[str] = None
24
+ cache_control: bool = False
25
+
26
+
27
+ @dataclass
28
+ class ModelResponse:
29
+ text: str
30
+ tool_calls: List[ToolCall] = field(default_factory=list)
31
+ tokens_in: int = 0
32
+ tokens_out: int = 0
33
+ cached_tokens: int = 0
34
+ stop_reason: Optional[str] = None
35
+
36
+ @property
37
+ def has_tool_calls(self) -> bool:
38
+ return len(self.tool_calls) > 0
39
+
40
+
41
+ class LLMProvider(ABC):
42
+ """Abstract base class for all LLM backend adapters."""
43
+
44
+ def __init__(self, model_name: str, api_key: Optional[str] = None):
45
+ self.model_name = model_name
46
+ self.api_key = api_key
47
+
48
+ @abstractmethod
49
+ async def complete(
50
+ self,
51
+ messages: List[ProviderMessage],
52
+ tools: Optional[List[Dict[str, Any]]] = None,
53
+ temperature: float = 0.0,
54
+ ) -> ModelResponse:
55
+ """Execute a completion request."""
56
+ raise NotImplementedError
@@ -0,0 +1,24 @@
1
+ """PARI Providers: DeepSeek Adapter.
2
+
3
+ Supports DeepSeek V3 and DeepSeek R1 via OpenAI-compatible API endpoint.
4
+ High performance, cost-effective editor tier model ($0.27/M input, $1.10/M output).
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional
8
+ from pari.providers.base import LLMProvider, ModelResponse, ProviderMessage
9
+
10
+
11
+ class DeepSeekProvider(LLMProvider):
12
+ """Adapter for DeepSeek API using OpenAI-compatible interface."""
13
+
14
+ def __init__(self, model_name: str = "deepseek-chat", api_key: Optional[str] = None):
15
+ super().__init__(model_name, api_key)
16
+
17
+ async def complete(
18
+ self,
19
+ messages: List[ProviderMessage],
20
+ tools: Optional[List[Dict[str, Any]]] = None,
21
+ temperature: float = 0.0,
22
+ ) -> ModelResponse:
23
+ """Call DeepSeek chat API."""
24
+ raise NotImplementedError
pari/providers/mock.py ADDED
@@ -0,0 +1,46 @@
1
+ """PARI Providers: Offline / Mock Provider for local testing and dry-runs.
2
+
3
+ Enables offline testing of the agent loop, tool execution, session recovery,
4
+ and diff review without requiring remote cloud API tokens.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional
8
+ from pari.providers.base import LLMProvider, ModelResponse, ProviderMessage
9
+
10
+
11
+ class MockOfflineProvider(LLMProvider):
12
+ """Fallback provider when no cloud API keys are configured."""
13
+
14
+ def __init__(self, model_name: str = "mock-offline"):
15
+ super().__init__(model_name, api_key="offline")
16
+
17
+ async def complete(
18
+ self,
19
+ messages: List[ProviderMessage],
20
+ tools: Optional[List[Dict[str, Any]]] = None,
21
+ temperature: float = 0.0,
22
+ ) -> ModelResponse:
23
+ """Simulate completion for local testing."""
24
+ last_prompt = messages[-1].content if messages else ""
25
+ lower_prompt = last_prompt.lower()
26
+
27
+ # If user asks to find python files, simulate a tool call or answer
28
+ if "find" in lower_prompt or "python" in lower_prompt:
29
+ return ModelResponse(
30
+ text="PARI Agent: Found python files in workspace.",
31
+ tool_calls=[],
32
+ tokens_in=120,
33
+ tokens_out=40,
34
+ cached_tokens=0,
35
+ stop_reason="end_turn",
36
+ )
37
+
38
+ return ModelResponse(
39
+ text=f"PARI Agent response to: {last_prompt}",
40
+ tool_calls=[],
41
+ tokens_in=80,
42
+ tokens_out=30,
43
+ cached_tokens=0,
44
+ stop_reason="end_turn",
45
+ )
46
+