pactown 0.1.4__py3-none-any.whl → 0.1.47__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.
- pactown/__init__.py +178 -4
- pactown/cli.py +539 -37
- pactown/config.py +12 -11
- pactown/deploy/__init__.py +17 -3
- pactown/deploy/base.py +35 -33
- pactown/deploy/compose.py +59 -58
- pactown/deploy/docker.py +40 -41
- pactown/deploy/kubernetes.py +43 -42
- pactown/deploy/podman.py +55 -56
- pactown/deploy/quadlet.py +1021 -0
- pactown/deploy/quadlet_api.py +533 -0
- pactown/deploy/quadlet_shell.py +557 -0
- pactown/events.py +1066 -0
- pactown/fast_start.py +514 -0
- pactown/generator.py +31 -30
- pactown/llm.py +450 -0
- pactown/markpact_blocks.py +50 -0
- pactown/network.py +59 -38
- pactown/orchestrator.py +90 -93
- pactown/parallel.py +40 -40
- pactown/platform.py +146 -0
- pactown/registry/__init__.py +1 -1
- pactown/registry/client.py +45 -46
- pactown/registry/models.py +25 -25
- pactown/registry/server.py +24 -24
- pactown/resolver.py +30 -30
- pactown/runner_api.py +458 -0
- pactown/sandbox_manager.py +480 -79
- pactown/security.py +682 -0
- pactown/service_runner.py +1201 -0
- pactown/user_isolation.py +458 -0
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/METADATA +65 -9
- pactown-0.1.47.dist-info/RECORD +36 -0
- pactown-0.1.47.dist-info/entry_points.txt +5 -0
- pactown-0.1.4.dist-info/RECORD +0 -24
- pactown-0.1.4.dist-info/entry_points.txt +0 -3
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/WHEEL +0 -0
- {pactown-0.1.4.dist-info → pactown-0.1.47.dist-info}/licenses/LICENSE +0 -0
pactown/generator.py
CHANGED
|
@@ -5,15 +5,16 @@ from __future__ import annotations
|
|
|
5
5
|
import re
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Optional
|
|
8
|
+
|
|
8
9
|
import yaml
|
|
9
10
|
|
|
10
|
-
from
|
|
11
|
+
from .markpact_blocks import parse_blocks
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def scan_readme(readme_path: Path) -> dict:
|
|
14
15
|
"""
|
|
15
16
|
Scan a README.md and extract service configuration.
|
|
16
|
-
|
|
17
|
+
|
|
17
18
|
Returns dict with:
|
|
18
19
|
- name: service name (from folder or heading)
|
|
19
20
|
- readme: relative path to README
|
|
@@ -24,12 +25,12 @@ def scan_readme(readme_path: Path) -> dict:
|
|
|
24
25
|
"""
|
|
25
26
|
content = readme_path.read_text()
|
|
26
27
|
blocks = parse_blocks(content)
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
# Extract service name from folder or first heading
|
|
29
30
|
folder_name = readme_path.parent.name
|
|
30
31
|
heading_match = re.search(r'^#\s+(.+)$', content, re.MULTILINE)
|
|
31
32
|
name = folder_name
|
|
32
|
-
|
|
33
|
+
|
|
33
34
|
# Detect port from run command
|
|
34
35
|
port = None
|
|
35
36
|
port_patterns = [
|
|
@@ -38,12 +39,12 @@ def scan_readme(readme_path: Path) -> dict:
|
|
|
38
39
|
r':(\d+)',
|
|
39
40
|
r'PORT[=:-]+(\d+)',
|
|
40
41
|
]
|
|
41
|
-
|
|
42
|
+
|
|
42
43
|
# Detect health check endpoint
|
|
43
44
|
health_check = None
|
|
44
45
|
has_run = False
|
|
45
46
|
deps = []
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
for block in blocks:
|
|
48
49
|
if block.kind == "run":
|
|
49
50
|
has_run = True
|
|
@@ -52,17 +53,17 @@ def scan_readme(readme_path: Path) -> dict:
|
|
|
52
53
|
if match:
|
|
53
54
|
port = int(match.group(1))
|
|
54
55
|
break
|
|
55
|
-
|
|
56
|
+
|
|
56
57
|
if block.kind == "deps":
|
|
57
58
|
deps = [d.strip() for d in block.body.strip().split('\n') if d.strip()]
|
|
58
|
-
|
|
59
|
+
|
|
59
60
|
if block.kind == "test":
|
|
60
61
|
# Look for health check in tests
|
|
61
62
|
if "/health" in block.body:
|
|
62
63
|
health_check = "/health"
|
|
63
64
|
elif "GET /" in block.body:
|
|
64
65
|
health_check = "/"
|
|
65
|
-
|
|
66
|
+
|
|
66
67
|
return {
|
|
67
68
|
"name": name,
|
|
68
69
|
"readme": str(readme_path),
|
|
@@ -81,23 +82,23 @@ def scan_folder(
|
|
|
81
82
|
) -> list[dict]:
|
|
82
83
|
"""
|
|
83
84
|
Scan a folder for README.md files and extract service configs.
|
|
84
|
-
|
|
85
|
+
|
|
85
86
|
Args:
|
|
86
87
|
folder: Root folder to scan
|
|
87
88
|
recursive: Whether to scan subdirectories
|
|
88
89
|
pattern: Filename pattern to match
|
|
89
|
-
|
|
90
|
+
|
|
90
91
|
Returns:
|
|
91
92
|
List of service configurations
|
|
92
93
|
"""
|
|
93
94
|
folder = Path(folder)
|
|
94
95
|
services = []
|
|
95
|
-
|
|
96
|
+
|
|
96
97
|
if recursive:
|
|
97
98
|
readme_files = list(folder.rglob(pattern))
|
|
98
99
|
else:
|
|
99
100
|
readme_files = list(folder.glob(pattern))
|
|
100
|
-
|
|
101
|
+
|
|
101
102
|
for readme_path in readme_files:
|
|
102
103
|
try:
|
|
103
104
|
config = scan_readme(readme_path)
|
|
@@ -105,7 +106,7 @@ def scan_folder(
|
|
|
105
106
|
services.append(config)
|
|
106
107
|
except Exception as e:
|
|
107
108
|
print(f"Warning: Failed to parse {readme_path}: {e}")
|
|
108
|
-
|
|
109
|
+
|
|
109
110
|
return services
|
|
110
111
|
|
|
111
112
|
|
|
@@ -117,22 +118,22 @@ def generate_config(
|
|
|
117
118
|
) -> dict:
|
|
118
119
|
"""
|
|
119
120
|
Generate a pactown ecosystem configuration from a folder.
|
|
120
|
-
|
|
121
|
+
|
|
121
122
|
Args:
|
|
122
123
|
folder: Folder to scan for services
|
|
123
124
|
name: Ecosystem name (default: folder name)
|
|
124
125
|
base_port: Starting port for auto-assignment
|
|
125
126
|
output: Optional path to write YAML file
|
|
126
|
-
|
|
127
|
+
|
|
127
128
|
Returns:
|
|
128
129
|
Generated configuration dict
|
|
129
130
|
"""
|
|
130
131
|
folder = Path(folder)
|
|
131
132
|
services = scan_folder(folder)
|
|
132
|
-
|
|
133
|
+
|
|
133
134
|
if not services:
|
|
134
135
|
raise ValueError(f"No runnable services found in {folder}")
|
|
135
|
-
|
|
136
|
+
|
|
136
137
|
# Build config
|
|
137
138
|
config = {
|
|
138
139
|
"name": name or folder.name,
|
|
@@ -146,13 +147,13 @@ def generate_config(
|
|
|
146
147
|
},
|
|
147
148
|
"services": {},
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
+
|
|
150
151
|
# Assign ports and build service configs
|
|
151
152
|
next_port = base_port
|
|
152
153
|
for svc in services:
|
|
153
154
|
port = svc["port"] or next_port
|
|
154
155
|
next_port = max(next_port, port) + 1
|
|
155
|
-
|
|
156
|
+
|
|
156
157
|
# Make readme path relative to output folder
|
|
157
158
|
readme_rel = svc["readme"]
|
|
158
159
|
if output:
|
|
@@ -160,24 +161,24 @@ def generate_config(
|
|
|
160
161
|
readme_rel = str(Path(svc["readme"]).relative_to(output.parent))
|
|
161
162
|
except ValueError:
|
|
162
163
|
pass
|
|
163
|
-
|
|
164
|
+
|
|
164
165
|
service_config = {
|
|
165
166
|
"readme": readme_rel,
|
|
166
167
|
"port": port,
|
|
167
168
|
}
|
|
168
|
-
|
|
169
|
+
|
|
169
170
|
if svc["health_check"]:
|
|
170
171
|
service_config["health_check"] = svc["health_check"]
|
|
171
|
-
|
|
172
|
+
|
|
172
173
|
config["services"][svc["name"]] = service_config
|
|
173
|
-
|
|
174
|
+
|
|
174
175
|
# Write to file if output specified
|
|
175
176
|
if output:
|
|
176
177
|
output = Path(output)
|
|
177
178
|
with open(output, "w") as f:
|
|
178
179
|
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
179
180
|
print(f"Generated: {output}")
|
|
180
|
-
|
|
181
|
+
|
|
181
182
|
return config
|
|
182
183
|
|
|
183
184
|
|
|
@@ -185,21 +186,21 @@ def print_scan_results(folder: Path) -> None:
|
|
|
185
186
|
"""Print scan results in a readable format."""
|
|
186
187
|
from rich.console import Console
|
|
187
188
|
from rich.table import Table
|
|
188
|
-
|
|
189
|
+
|
|
189
190
|
console = Console()
|
|
190
191
|
services = scan_folder(folder)
|
|
191
|
-
|
|
192
|
+
|
|
192
193
|
if not services:
|
|
193
194
|
console.print(f"[yellow]No runnable services found in {folder}[/yellow]")
|
|
194
195
|
return
|
|
195
|
-
|
|
196
|
+
|
|
196
197
|
table = Table(title=f"Services found in {folder}")
|
|
197
198
|
table.add_column("Name", style="cyan")
|
|
198
199
|
table.add_column("Title")
|
|
199
200
|
table.add_column("Port", style="blue")
|
|
200
201
|
table.add_column("Health")
|
|
201
202
|
table.add_column("Deps", style="dim")
|
|
202
|
-
|
|
203
|
+
|
|
203
204
|
for svc in services:
|
|
204
205
|
table.add_row(
|
|
205
206
|
svc["name"],
|
|
@@ -208,5 +209,5 @@ def print_scan_results(folder: Path) -> None:
|
|
|
208
209
|
svc["health_check"] or "-",
|
|
209
210
|
str(len(svc["deps"])),
|
|
210
211
|
)
|
|
211
|
-
|
|
212
|
+
|
|
212
213
|
console.print(table)
|
pactown/llm.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Integration for Pactown.
|
|
3
|
+
|
|
4
|
+
Provides LLM rotation, fallback, and management using the lolm library.
|
|
5
|
+
This module enables AI-powered features in pactown with automatic
|
|
6
|
+
provider rotation when rate limits are hit.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import importlib
|
|
11
|
+
import inspect
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
LOLM_AVAILABLE = False
|
|
16
|
+
LOLM_VERSION: Optional[str] = None
|
|
17
|
+
LOLM_IMPORT_ERROR: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
ROTATION_AVAILABLE = False
|
|
20
|
+
ROTATION_IMPORT_ERROR: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
LLMManager = None
|
|
23
|
+
RotationQueue = None
|
|
24
|
+
ProviderHealth = None
|
|
25
|
+
ProviderState = None
|
|
26
|
+
RateLimitInfo = None
|
|
27
|
+
RateLimitType = None
|
|
28
|
+
LLMRotationManager = None
|
|
29
|
+
LLMRateLimitError = None
|
|
30
|
+
parse_rate_limit_headers = None
|
|
31
|
+
is_rate_limit_error = None
|
|
32
|
+
create_rotation_manager = None
|
|
33
|
+
|
|
34
|
+
get_client = None
|
|
35
|
+
list_available_providers = None
|
|
36
|
+
load_lolm_config = None
|
|
37
|
+
save_lolm_config = None
|
|
38
|
+
LLMConfig = None
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
_lolm = importlib.import_module("lolm")
|
|
42
|
+
LOLM_VERSION = getattr(_lolm, "__version__", None)
|
|
43
|
+
|
|
44
|
+
# Core APIs (exist in older lolm versions too)
|
|
45
|
+
from lolm import LLMManager as _LLMManager # type: ignore
|
|
46
|
+
from lolm import get_client as _get_client # type: ignore
|
|
47
|
+
from lolm import list_available_providers as _list_available_providers # type: ignore
|
|
48
|
+
|
|
49
|
+
LLMManager = _LLMManager
|
|
50
|
+
get_client = _get_client
|
|
51
|
+
list_available_providers = _list_available_providers
|
|
52
|
+
LOLM_AVAILABLE = True
|
|
53
|
+
|
|
54
|
+
# Optional config helpers
|
|
55
|
+
try:
|
|
56
|
+
from lolm import load_config as _load_lolm_config # type: ignore
|
|
57
|
+
from lolm import save_config as _save_lolm_config # type: ignore
|
|
58
|
+
from lolm import LLMConfig as _LLMConfig # type: ignore
|
|
59
|
+
|
|
60
|
+
load_lolm_config = _load_lolm_config
|
|
61
|
+
save_lolm_config = _save_lolm_config
|
|
62
|
+
LLMConfig = _LLMConfig
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
# Rotation APIs (may not exist in older lolm versions)
|
|
67
|
+
try:
|
|
68
|
+
from lolm.rotation import ( # type: ignore
|
|
69
|
+
RotationQueue as _RotationQueue,
|
|
70
|
+
ProviderHealth as _ProviderHealth,
|
|
71
|
+
ProviderState as _ProviderState,
|
|
72
|
+
RateLimitInfo as _RateLimitInfo,
|
|
73
|
+
RateLimitType as _RateLimitType,
|
|
74
|
+
LLMRotationManager as _LLMRotationManager,
|
|
75
|
+
parse_rate_limit_headers as _parse_rate_limit_headers,
|
|
76
|
+
is_rate_limit_error as _is_rate_limit_error,
|
|
77
|
+
create_rotation_manager as _create_rotation_manager,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
RotationQueue = _RotationQueue
|
|
81
|
+
ProviderHealth = _ProviderHealth
|
|
82
|
+
ProviderState = _ProviderState
|
|
83
|
+
RateLimitInfo = _RateLimitInfo
|
|
84
|
+
RateLimitType = _RateLimitType
|
|
85
|
+
LLMRotationManager = _LLMRotationManager
|
|
86
|
+
parse_rate_limit_headers = _parse_rate_limit_headers
|
|
87
|
+
is_rate_limit_error = _is_rate_limit_error
|
|
88
|
+
create_rotation_manager = _create_rotation_manager
|
|
89
|
+
ROTATION_AVAILABLE = True
|
|
90
|
+
except Exception as e:
|
|
91
|
+
ROTATION_AVAILABLE = False
|
|
92
|
+
ROTATION_IMPORT_ERROR = str(e)
|
|
93
|
+
|
|
94
|
+
# Optional rate limit error type
|
|
95
|
+
try:
|
|
96
|
+
from lolm.clients import LLMRateLimitError as _LLMRateLimitError # type: ignore
|
|
97
|
+
|
|
98
|
+
LLMRateLimitError = _LLMRateLimitError
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
LOLM_AVAILABLE = False
|
|
104
|
+
LOLM_IMPORT_ERROR = str(e)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class PactownLLMError(Exception):
|
|
108
|
+
"""Base exception for Pactown LLM errors."""
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class LLMNotAvailableError(PactownLLMError):
|
|
113
|
+
"""Raised when no LLM provider is available."""
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class PactownLLM:
|
|
118
|
+
"""
|
|
119
|
+
Pactown LLM Manager with rotation and fallback support.
|
|
120
|
+
|
|
121
|
+
Integrates with the lolm library for multi-provider LLM management
|
|
122
|
+
with automatic rotation when rate limits are hit.
|
|
123
|
+
|
|
124
|
+
Usage:
|
|
125
|
+
llm = PactownLLM()
|
|
126
|
+
llm.initialize()
|
|
127
|
+
|
|
128
|
+
# Simple generation
|
|
129
|
+
response = llm.generate("Explain this code")
|
|
130
|
+
|
|
131
|
+
# With rotation (automatic failover on rate limits)
|
|
132
|
+
response = llm.generate_with_rotation("Explain this code")
|
|
133
|
+
|
|
134
|
+
# Check status
|
|
135
|
+
status = llm.get_status()
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
_instance: Optional['PactownLLM'] = None
|
|
139
|
+
|
|
140
|
+
def __init__(self, verbose: bool = False):
|
|
141
|
+
if not LOLM_AVAILABLE:
|
|
142
|
+
raise ImportError(
|
|
143
|
+
"lolm library not available. Install with: pip install -U pactown[llm]"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# lolm versions differ; detect whether rotation is supported and whether
|
|
147
|
+
# the LLMManager supports `enable_rotation`.
|
|
148
|
+
enable_rotation = ROTATION_AVAILABLE
|
|
149
|
+
try:
|
|
150
|
+
sig = inspect.signature(LLMManager.__init__) # type: ignore[union-attr]
|
|
151
|
+
if "enable_rotation" in sig.parameters:
|
|
152
|
+
self._manager = LLMManager(verbose=verbose, enable_rotation=enable_rotation)
|
|
153
|
+
else:
|
|
154
|
+
self._manager = LLMManager(verbose=verbose)
|
|
155
|
+
except Exception:
|
|
156
|
+
self._manager = LLMManager(verbose=verbose)
|
|
157
|
+
self._verbose = verbose
|
|
158
|
+
self._initialized = False
|
|
159
|
+
|
|
160
|
+
# Callbacks for events
|
|
161
|
+
self._on_rate_limit: Optional[Callable[[str, Dict], None]] = None
|
|
162
|
+
self._on_rotation: Optional[Callable[[str, str], None]] = None
|
|
163
|
+
self._on_provider_unavailable: Optional[Callable[[str, str], None]] = None
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def get_instance(cls, verbose: bool = False) -> 'PactownLLM':
|
|
167
|
+
"""Get or create the global PactownLLM instance."""
|
|
168
|
+
if cls._instance is None:
|
|
169
|
+
cls._instance = cls(verbose=verbose)
|
|
170
|
+
return cls._instance
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def set_instance(cls, instance: 'PactownLLM') -> None:
|
|
174
|
+
"""Set the global PactownLLM instance."""
|
|
175
|
+
cls._instance = instance
|
|
176
|
+
|
|
177
|
+
def initialize(self) -> None:
|
|
178
|
+
"""Initialize the LLM manager and all providers."""
|
|
179
|
+
if self._initialized:
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
self._manager.initialize()
|
|
183
|
+
|
|
184
|
+
# Set up rotation queue callbacks (only if supported by this lolm)
|
|
185
|
+
get_queue = getattr(self._manager, "get_rotation_queue", None)
|
|
186
|
+
queue = get_queue() if callable(get_queue) else None
|
|
187
|
+
if queue:
|
|
188
|
+
if self._on_rate_limit:
|
|
189
|
+
queue.on_rate_limit(lambda name, info: self._on_rate_limit(name, info.to_dict() if hasattr(info, 'to_dict') else {}))
|
|
190
|
+
if self._on_rotation:
|
|
191
|
+
queue.on_rotation(self._on_rotation)
|
|
192
|
+
if self._on_provider_unavailable:
|
|
193
|
+
queue.on_provider_unavailable(self._on_provider_unavailable)
|
|
194
|
+
|
|
195
|
+
self._initialized = True
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def is_available(self) -> bool:
|
|
199
|
+
"""Check if any LLM provider is available."""
|
|
200
|
+
if not self._initialized:
|
|
201
|
+
self.initialize()
|
|
202
|
+
return self._manager.is_available
|
|
203
|
+
|
|
204
|
+
def generate(
|
|
205
|
+
self,
|
|
206
|
+
prompt: str,
|
|
207
|
+
system: str = None,
|
|
208
|
+
max_tokens: int = 4000,
|
|
209
|
+
provider: str = None
|
|
210
|
+
) -> str:
|
|
211
|
+
"""
|
|
212
|
+
Generate completion using available provider.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
prompt: User prompt
|
|
216
|
+
system: System prompt
|
|
217
|
+
max_tokens: Maximum tokens
|
|
218
|
+
provider: Specific provider to use (optional)
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Generated text
|
|
222
|
+
|
|
223
|
+
Raises:
|
|
224
|
+
LLMNotAvailableError: If no provider is available
|
|
225
|
+
"""
|
|
226
|
+
if not self._initialized:
|
|
227
|
+
self.initialize()
|
|
228
|
+
|
|
229
|
+
if not self.is_available:
|
|
230
|
+
raise LLMNotAvailableError("No LLM provider available. Run: lolm status")
|
|
231
|
+
|
|
232
|
+
return self._manager.generate(prompt, system=system, max_tokens=max_tokens, provider=provider)
|
|
233
|
+
|
|
234
|
+
def generate_with_rotation(
|
|
235
|
+
self,
|
|
236
|
+
prompt: str,
|
|
237
|
+
system: str = None,
|
|
238
|
+
max_tokens: int = 4000,
|
|
239
|
+
max_retries: int = 3
|
|
240
|
+
) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Generate with intelligent rotation based on provider health.
|
|
243
|
+
|
|
244
|
+
Automatically rotates to next available provider when one
|
|
245
|
+
hits rate limits or becomes unavailable.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
prompt: User prompt
|
|
249
|
+
system: System prompt
|
|
250
|
+
max_tokens: Maximum tokens
|
|
251
|
+
max_retries: Maximum number of providers to try
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Generated text
|
|
255
|
+
"""
|
|
256
|
+
if not self._initialized:
|
|
257
|
+
self.initialize()
|
|
258
|
+
|
|
259
|
+
gen_rot = getattr(self._manager, "generate_with_rotation", None)
|
|
260
|
+
if callable(gen_rot):
|
|
261
|
+
return gen_rot(prompt, system=system, max_tokens=max_tokens, max_retries=max_retries)
|
|
262
|
+
|
|
263
|
+
# Older lolm: no rotation method; fall back.
|
|
264
|
+
return self._manager.generate_with_fallback(prompt, system=system, max_tokens=max_tokens)
|
|
265
|
+
|
|
266
|
+
def generate_with_fallback(
|
|
267
|
+
self,
|
|
268
|
+
prompt: str,
|
|
269
|
+
system: str = None,
|
|
270
|
+
max_tokens: int = 4000,
|
|
271
|
+
providers: List[str] = None
|
|
272
|
+
) -> str:
|
|
273
|
+
"""
|
|
274
|
+
Generate with fallback to other providers on failure.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
prompt: User prompt
|
|
278
|
+
system: System prompt
|
|
279
|
+
max_tokens: Maximum tokens
|
|
280
|
+
providers: List of providers to try (in order)
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Generated text from first successful provider
|
|
284
|
+
"""
|
|
285
|
+
if not self._initialized:
|
|
286
|
+
self.initialize()
|
|
287
|
+
|
|
288
|
+
return self._manager.generate_with_fallback(
|
|
289
|
+
prompt, system=system, max_tokens=max_tokens, providers=providers
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def get_status(self) -> Dict[str, Any]:
|
|
293
|
+
"""Get status of all providers including health info."""
|
|
294
|
+
if not self._initialized:
|
|
295
|
+
self.initialize()
|
|
296
|
+
|
|
297
|
+
status = self._manager.get_status()
|
|
298
|
+
get_health = getattr(self._manager, "get_provider_health", None)
|
|
299
|
+
health = get_health() if callable(get_health) else {}
|
|
300
|
+
|
|
301
|
+
# Merge status with health info
|
|
302
|
+
for name in status:
|
|
303
|
+
if name in health:
|
|
304
|
+
status[name]['health'] = health[name]
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
'providers': status,
|
|
308
|
+
'available_providers': list_available_providers() if callable(list_available_providers) else [],
|
|
309
|
+
'is_available': self.is_available,
|
|
310
|
+
'lolm_version': LOLM_VERSION,
|
|
311
|
+
'rotation_available': ROTATION_AVAILABLE,
|
|
312
|
+
'rotation_import_error': ROTATION_IMPORT_ERROR,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
def get_provider_health(self, name: str = None) -> Dict:
|
|
316
|
+
"""Get health info for providers."""
|
|
317
|
+
if not self._initialized:
|
|
318
|
+
self.initialize()
|
|
319
|
+
get_health = getattr(self._manager, "get_provider_health", None)
|
|
320
|
+
if callable(get_health):
|
|
321
|
+
return get_health(name)
|
|
322
|
+
return {}
|
|
323
|
+
|
|
324
|
+
def set_provider_priority(self, name: str, priority: int) -> bool:
|
|
325
|
+
"""Set priority for a provider (lower = higher priority)."""
|
|
326
|
+
if not self._initialized:
|
|
327
|
+
self.initialize()
|
|
328
|
+
return self._manager.set_provider_priority(name, priority)
|
|
329
|
+
|
|
330
|
+
def reset_provider(self, name: str) -> bool:
|
|
331
|
+
"""Reset a provider's health metrics."""
|
|
332
|
+
if not self._initialized:
|
|
333
|
+
self.initialize()
|
|
334
|
+
return self._manager.reset_provider(name)
|
|
335
|
+
|
|
336
|
+
def get_rotation_queue(self) -> Optional[RotationQueue]:
|
|
337
|
+
"""Get the rotation queue for advanced control."""
|
|
338
|
+
return self._manager.get_rotation_queue()
|
|
339
|
+
|
|
340
|
+
# Event handlers
|
|
341
|
+
def on_rate_limit(self, callback: Callable[[str, Dict], None]) -> None:
|
|
342
|
+
"""Set callback for rate limit events."""
|
|
343
|
+
self._on_rate_limit = callback
|
|
344
|
+
|
|
345
|
+
def on_rotation(self, callback: Callable[[str, str], None]) -> None:
|
|
346
|
+
"""Set callback for provider rotation events."""
|
|
347
|
+
self._on_rotation = callback
|
|
348
|
+
|
|
349
|
+
def on_provider_unavailable(self, callback: Callable[[str, str], None]) -> None:
|
|
350
|
+
"""Set callback for when a provider becomes unavailable."""
|
|
351
|
+
self._on_provider_unavailable = callback
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# Global instance accessor
|
|
355
|
+
def get_llm(verbose: bool = False) -> PactownLLM:
|
|
356
|
+
"""Get the global PactownLLM instance."""
|
|
357
|
+
return PactownLLM.get_instance(verbose=verbose)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def is_lolm_available() -> bool:
|
|
361
|
+
"""Check if lolm library is available."""
|
|
362
|
+
return LOLM_AVAILABLE
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def get_lolm_info() -> Dict[str, Any]:
|
|
366
|
+
"""Get diagnostic info about lolm availability/features."""
|
|
367
|
+
return {
|
|
368
|
+
"lolm_installed": LOLM_AVAILABLE,
|
|
369
|
+
"lolm_version": LOLM_VERSION,
|
|
370
|
+
"lolm_import_error": LOLM_IMPORT_ERROR,
|
|
371
|
+
"rotation_available": ROTATION_AVAILABLE,
|
|
372
|
+
"rotation_import_error": ROTATION_IMPORT_ERROR,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# Convenience functions
|
|
377
|
+
def generate(
|
|
378
|
+
prompt: str,
|
|
379
|
+
system: str = None,
|
|
380
|
+
max_tokens: int = 4000,
|
|
381
|
+
with_rotation: bool = True
|
|
382
|
+
) -> str:
|
|
383
|
+
"""
|
|
384
|
+
Generate completion using the global LLM instance.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
prompt: User prompt
|
|
388
|
+
system: System prompt
|
|
389
|
+
max_tokens: Maximum tokens
|
|
390
|
+
with_rotation: Use rotation for automatic failover
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Generated text
|
|
394
|
+
"""
|
|
395
|
+
llm = get_llm()
|
|
396
|
+
if with_rotation:
|
|
397
|
+
return llm.generate_with_rotation(prompt, system=system, max_tokens=max_tokens)
|
|
398
|
+
return llm.generate(prompt, system=system, max_tokens=max_tokens)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def get_llm_status() -> Dict[str, Any]:
|
|
402
|
+
"""Get status of all LLM providers."""
|
|
403
|
+
if not LOLM_AVAILABLE:
|
|
404
|
+
return {
|
|
405
|
+
'lolm_installed': False,
|
|
406
|
+
'is_available': False,
|
|
407
|
+
'error': 'lolm library not available in this environment',
|
|
408
|
+
'install': 'pip install -U pactown[llm] # or: pip install -U lolm',
|
|
409
|
+
'lolm_import_error': LOLM_IMPORT_ERROR,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
llm = get_llm()
|
|
414
|
+
status = llm.get_status()
|
|
415
|
+
status.setdefault('lolm_installed', True)
|
|
416
|
+
status.setdefault('lolm_version', LOLM_VERSION)
|
|
417
|
+
status.setdefault('rotation_available', ROTATION_AVAILABLE)
|
|
418
|
+
status.setdefault('rotation_import_error', ROTATION_IMPORT_ERROR)
|
|
419
|
+
return status
|
|
420
|
+
except Exception as e:
|
|
421
|
+
return {
|
|
422
|
+
'lolm_installed': True,
|
|
423
|
+
'is_available': False,
|
|
424
|
+
'error': str(e),
|
|
425
|
+
'lolm_version': LOLM_VERSION,
|
|
426
|
+
'rotation_available': ROTATION_AVAILABLE,
|
|
427
|
+
'rotation_import_error': ROTATION_IMPORT_ERROR,
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def set_provider_priority(name: str, priority: int) -> bool:
|
|
432
|
+
"""Set priority for an LLM provider."""
|
|
433
|
+
if not LOLM_AVAILABLE:
|
|
434
|
+
return False
|
|
435
|
+
llm = get_llm()
|
|
436
|
+
set_prio = getattr(llm, "set_provider_priority", None)
|
|
437
|
+
if callable(set_prio):
|
|
438
|
+
return set_prio(name, priority)
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def reset_provider(name: str) -> bool:
|
|
443
|
+
"""Reset an LLM provider's health metrics."""
|
|
444
|
+
if not LOLM_AVAILABLE:
|
|
445
|
+
return False
|
|
446
|
+
llm = get_llm()
|
|
447
|
+
reset = getattr(llm, "reset_provider", None)
|
|
448
|
+
if callable(reset):
|
|
449
|
+
return reset(name)
|
|
450
|
+
return False
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
# New format: ```python markpact:file path=main.py
|
|
7
|
+
CODEBLOCK_NEW_RE = re.compile(
|
|
8
|
+
r"```(?P<lang>\w+)\s+markpact:(?P<kind>\w+)(?:[ \t]+(?P<meta>[^\n]*))?\n(?P<body>[\s\S]*?)\n```",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# Old format: ```markpact:file python path=main.py
|
|
12
|
+
CODEBLOCK_OLD_RE = re.compile(
|
|
13
|
+
r"```markpact:(?P<kind>\w+)(?:[ \t]+(?P<meta>[^\n]*))?\n(?P<body>[\s\S]*?)\n```",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Block:
|
|
19
|
+
kind: str
|
|
20
|
+
meta: str
|
|
21
|
+
body: str
|
|
22
|
+
lang: str = ""
|
|
23
|
+
|
|
24
|
+
def get_path(self) -> str | None:
|
|
25
|
+
m = re.search(r"\bpath=(\S+)", self.meta)
|
|
26
|
+
return m[1] if m else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_blocks(text: str) -> list[Block]:
|
|
30
|
+
blocks = []
|
|
31
|
+
|
|
32
|
+
# Parse new format: ```python markpact:file path=main.py
|
|
33
|
+
for m in CODEBLOCK_NEW_RE.finditer(text):
|
|
34
|
+
blocks.append(Block(
|
|
35
|
+
kind=m.group("kind"),
|
|
36
|
+
meta=(m.group("meta") or "").strip(),
|
|
37
|
+
body=m.group("body").strip(),
|
|
38
|
+
lang=(m.group("lang") or "").strip(),
|
|
39
|
+
))
|
|
40
|
+
|
|
41
|
+
# Parse old format: ```markpact:file python path=main.py
|
|
42
|
+
for m in CODEBLOCK_OLD_RE.finditer(text):
|
|
43
|
+
blocks.append(Block(
|
|
44
|
+
kind=m.group("kind"),
|
|
45
|
+
meta=(m.group("meta") or "").strip(),
|
|
46
|
+
body=m.group("body").strip(),
|
|
47
|
+
lang="", # Old format doesn't have separate lang
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
return blocks
|