diffron 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.
- diffron/__init__.py +26 -0
- diffron/__init__.pyi +28 -0
- diffron/cli.py +347 -0
- diffron/cli.pyi +30 -0
- diffron/client.py +175 -0
- diffron/client.pyi +80 -0
- diffron/commit_gen.py +151 -0
- diffron/commit_gen.pyi +41 -0
- diffron/git_hooks.py +292 -0
- diffron/git_hooks.pyi +52 -0
- diffron/lemonade.py +192 -0
- diffron/lemonade.pyi +66 -0
- diffron/pr_gen.py +224 -0
- diffron/pr_gen.pyi +56 -0
- diffron/py.typed +0 -0
- diffron/utils.py +236 -0
- diffron/utils.pyi +58 -0
- diffron-0.1.0.dist-info/METADATA +373 -0
- diffron-0.1.0.dist-info/RECORD +28 -0
- diffron-0.1.0.dist-info/WHEEL +5 -0
- diffron-0.1.0.dist-info/entry_points.txt +5 -0
- diffron-0.1.0.dist-info/licenses/LICENSE +21 -0
- diffron-0.1.0.dist-info/top_level.txt +2 -0
- docs/HOOKS.md +442 -0
- docs/PLAN.md +378 -0
- docs/PYPI_RELEASE.md +487 -0
- docs/SETUP.md +524 -0
- docs/USAGE.md +515 -0
diffron/lemonade.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lemonade integration for Diffron.
|
|
3
|
+
|
|
4
|
+
Provides automatic port detection and API client for Lemonade LLM server.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional, List
|
|
9
|
+
from openai import OpenAI
|
|
10
|
+
|
|
11
|
+
from .utils import scan_ports, is_port_open
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_HOST = "localhost"
|
|
15
|
+
DEFAULT_PORT = 8020 # Default Lemonade port
|
|
16
|
+
DEFAULT_PORTS = [8020, 8000, 8001, 8080, 8081, 5000, 5001]
|
|
17
|
+
LEMONADE_API_KEY = "lemonade" # Default API key for Lemonade
|
|
18
|
+
DEFAULT_MODEL = "qwen2.5-it-3b-FLM" # Default Lemonade model
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def detect_lemonade_port(
|
|
22
|
+
host: Optional[str] = None,
|
|
23
|
+
ports: Optional[List[int]] = None
|
|
24
|
+
) -> Optional[int]:
|
|
25
|
+
"""
|
|
26
|
+
Auto-detect the port where Lemonade is running.
|
|
27
|
+
|
|
28
|
+
Checks LEMONADE_SERVER_URL environment variable first, then scans common ports.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
host: Host to check. Defaults to DIFFRON_LEMONADE_HOST or localhost.
|
|
32
|
+
ports: List of ports to scan. Defaults to common Lemonade ports.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Port number if found, None otherwise.
|
|
36
|
+
"""
|
|
37
|
+
# Check LEMONADE_SERVER_URL environment variable first (standard Lemonade env)
|
|
38
|
+
server_url = os.environ.get("LEMONADE_SERVER_URL")
|
|
39
|
+
if server_url:
|
|
40
|
+
try:
|
|
41
|
+
# Parse URL like "http://localhost:8020"
|
|
42
|
+
from urllib.parse import urlparse
|
|
43
|
+
parsed = urlparse(server_url)
|
|
44
|
+
if parsed.port:
|
|
45
|
+
port = parsed.port
|
|
46
|
+
if is_port_open(parsed.hostname or DEFAULT_HOST, port):
|
|
47
|
+
return port
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
# Check DIFFRON_LEMONADE_PORT (Diffron-specific)
|
|
52
|
+
env_port = os.environ.get("DIFFRON_LEMONADE_PORT")
|
|
53
|
+
if env_port:
|
|
54
|
+
try:
|
|
55
|
+
port = int(env_port)
|
|
56
|
+
if is_port_open(host or DEFAULT_HOST, port):
|
|
57
|
+
return port
|
|
58
|
+
except ValueError:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
# Scan common ports
|
|
62
|
+
host = host or os.environ.get("DIFFRON_LEMONADE_HOST", DEFAULT_HOST)
|
|
63
|
+
ports_to_scan = ports or DEFAULT_PORTS
|
|
64
|
+
|
|
65
|
+
open_ports = scan_ports(ports_to_scan, host)
|
|
66
|
+
|
|
67
|
+
if open_ports:
|
|
68
|
+
return open_ports[0] # Return first open port
|
|
69
|
+
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_lemonade_url() -> str:
|
|
74
|
+
"""
|
|
75
|
+
Get the Lemonade server URL from environment or default.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Lemonade server URL.
|
|
79
|
+
"""
|
|
80
|
+
# Check LEMONADE_SERVER_URL first (standard)
|
|
81
|
+
server_url = os.environ.get("LEMONADE_SERVER_URL")
|
|
82
|
+
if server_url:
|
|
83
|
+
return server_url.strip().rstrip("/")
|
|
84
|
+
|
|
85
|
+
# Build URL from host/port
|
|
86
|
+
host = os.environ.get("DIFFRON_LEMONADE_HOST", DEFAULT_HOST)
|
|
87
|
+
port = detect_lemonade_port(host)
|
|
88
|
+
|
|
89
|
+
if port:
|
|
90
|
+
return f"http://{host}:{port}"
|
|
91
|
+
|
|
92
|
+
# Fallback to default
|
|
93
|
+
return f"http://{DEFAULT_HOST}:{DEFAULT_PORT}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def is_lemonade_running(
|
|
97
|
+
host: Optional[str] = None,
|
|
98
|
+
port: Optional[int] = None
|
|
99
|
+
) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Check if Lemonade is running.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
host: Host to check. Defaults to DIFFRON_LEMONADE_HOST or localhost.
|
|
105
|
+
port: Port to check. Auto-detects if not provided.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if Lemonade is running, False otherwise.
|
|
109
|
+
"""
|
|
110
|
+
if port is None:
|
|
111
|
+
port = detect_lemonade_port(host)
|
|
112
|
+
if port is None:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
host = host or os.environ.get("DIFFRON_LEMONADE_HOST", DEFAULT_HOST)
|
|
116
|
+
return is_port_open(host, port)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class LemonadeClient:
|
|
120
|
+
"""
|
|
121
|
+
Client for interacting with Lemonade LLM server.
|
|
122
|
+
|
|
123
|
+
Automatically detects the running Lemonade instance and provides
|
|
124
|
+
a convenient interface for chat completions.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
base_url: Optional[str] = None,
|
|
130
|
+
api_key: Optional[str] = None,
|
|
131
|
+
model: Optional[str] = None,
|
|
132
|
+
):
|
|
133
|
+
"""
|
|
134
|
+
Initialize the Lemonade client.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
base_url: Lemonade server URL. Auto-detects from LEMONADE_SERVER_URL or defaults to http://localhost:8020.
|
|
138
|
+
api_key: API key. Defaults to "lemonade".
|
|
139
|
+
model: Model name to use. Defaults to qwen2.5-it-3b-FLM.
|
|
140
|
+
"""
|
|
141
|
+
# Get base URL from parameter, environment, or default
|
|
142
|
+
if base_url:
|
|
143
|
+
self.base_url = base_url.rstrip("/")
|
|
144
|
+
else:
|
|
145
|
+
self.base_url = get_lemonade_url()
|
|
146
|
+
|
|
147
|
+
# Parse host and port from base_url
|
|
148
|
+
from urllib.parse import urlparse
|
|
149
|
+
parsed = urlparse(self.base_url)
|
|
150
|
+
self.host = parsed.hostname or DEFAULT_HOST
|
|
151
|
+
self.port = parsed.port or DEFAULT_PORT
|
|
152
|
+
|
|
153
|
+
self.api_key = api_key or LEMONADE_API_KEY
|
|
154
|
+
|
|
155
|
+
# Model: parameter > environment > default
|
|
156
|
+
self.model = model or os.environ.get("DIFFRON_MODEL") or DEFAULT_MODEL
|
|
157
|
+
|
|
158
|
+
self.client = OpenAI(
|
|
159
|
+
base_url=f"{self.base_url}/api/v1",
|
|
160
|
+
api_key=self.api_key,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def chat_completion(
|
|
164
|
+
self,
|
|
165
|
+
messages: List[dict],
|
|
166
|
+
max_tokens: int = 100,
|
|
167
|
+
temperature: float = 0.2,
|
|
168
|
+
**kwargs
|
|
169
|
+
) -> str:
|
|
170
|
+
"""
|
|
171
|
+
Generate a chat completion.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
messages: List of message dicts with 'role' and 'content'.
|
|
175
|
+
max_tokens: Maximum tokens to generate.
|
|
176
|
+
temperature: Sampling temperature.
|
|
177
|
+
**kwargs: Additional arguments to pass to the API.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Generated response content.
|
|
181
|
+
"""
|
|
182
|
+
response = self.client.chat.completions.create(
|
|
183
|
+
model=self.model,
|
|
184
|
+
messages=messages,
|
|
185
|
+
max_tokens=max_tokens,
|
|
186
|
+
temperature=temperature,
|
|
187
|
+
**kwargs,
|
|
188
|
+
)
|
|
189
|
+
return response.choices[0].message.content
|
|
190
|
+
|
|
191
|
+
def __repr__(self) -> str:
|
|
192
|
+
return f"LemonadeClient(base_url='{self.base_url}', model='{self.model}')"
|
diffron/lemonade.pyi
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lemonade integration for Diffron.
|
|
3
|
+
|
|
4
|
+
Provides automatic port detection and API client for Lemonade LLM server.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULT_HOST: str
|
|
11
|
+
DEFAULT_PORTS: List[int]
|
|
12
|
+
LEMONADE_API_KEY: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def detect_lemonade_port(
|
|
16
|
+
host: Optional[str] = None,
|
|
17
|
+
ports: Optional[List[int]] = None
|
|
18
|
+
) -> Optional[int]:
|
|
19
|
+
"""Auto-detect the port where Lemonade is running."""
|
|
20
|
+
...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_lemonade_running(
|
|
24
|
+
host: Optional[str] = None,
|
|
25
|
+
port: Optional[int] = None
|
|
26
|
+
) -> bool:
|
|
27
|
+
"""Check if Lemonade is running."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LemonadeClient:
|
|
32
|
+
"""Client for interacting with Lemonade LLM server."""
|
|
33
|
+
|
|
34
|
+
host: str
|
|
35
|
+
port: int
|
|
36
|
+
api_key: str
|
|
37
|
+
model: str
|
|
38
|
+
base_url: str
|
|
39
|
+
client: OpenAI
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
host: Optional[str] = None,
|
|
44
|
+
port: Optional[int] = None,
|
|
45
|
+
api_key: Optional[str] = None,
|
|
46
|
+
model: Optional[str] = None,
|
|
47
|
+
):
|
|
48
|
+
"""Initialize the Lemonade client."""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
def _detect_model(self) -> str:
|
|
52
|
+
"""Auto-detect available models from Lemonade."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
def chat_completion(
|
|
56
|
+
self,
|
|
57
|
+
messages: List[dict],
|
|
58
|
+
max_tokens: int = 100,
|
|
59
|
+
temperature: float = 0.2,
|
|
60
|
+
**kwargs
|
|
61
|
+
) -> str:
|
|
62
|
+
"""Generate a chat completion."""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
def __repr__(self) -> str:
|
|
66
|
+
...
|
diffron/pr_gen.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PR description generation for Diffron.
|
|
3
|
+
|
|
4
|
+
Generates PR titles and descriptions from branch diffs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from .lemonade import LemonadeClient
|
|
12
|
+
from .utils import get_branch_diff, get_commit_log, get_current_branch, find_default_branch
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
DEFAULT_MAX_CHARS = 5000
|
|
16
|
+
DEFAULT_MAX_TOKENS = 300
|
|
17
|
+
DEFAULT_TEMPERATURE = 0.3
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PRDescription:
|
|
22
|
+
"""Represents a generated PR title and description."""
|
|
23
|
+
|
|
24
|
+
title: str
|
|
25
|
+
description: str
|
|
26
|
+
|
|
27
|
+
def format_output(self) -> str:
|
|
28
|
+
"""
|
|
29
|
+
Format as TITLE + DESCRIPTION output.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Formatted string with TITLE and DESCRIPTION sections.
|
|
33
|
+
"""
|
|
34
|
+
return f"TITLE: {self.title}\n\nDESCRIPTION:\n{self.description}"
|
|
35
|
+
|
|
36
|
+
def to_github_cli(self) -> Tuple[str, str]:
|
|
37
|
+
"""
|
|
38
|
+
Get title and body for GitHub CLI.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Tuple of (title, body).
|
|
42
|
+
"""
|
|
43
|
+
return self.title, self.description
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate_pr_description(
|
|
47
|
+
branch: Optional[str] = None,
|
|
48
|
+
base: Optional[str] = None,
|
|
49
|
+
max_chars: Optional[int] = None,
|
|
50
|
+
max_tokens: Optional[int] = None,
|
|
51
|
+
temperature: Optional[float] = None,
|
|
52
|
+
client: Optional[LemonadeClient] = None,
|
|
53
|
+
) -> PRDescription:
|
|
54
|
+
"""
|
|
55
|
+
Generate a PR title and description from branch changes.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
branch: Branch name to analyze. Defaults to current branch.
|
|
59
|
+
base: Base branch to compare against. Defaults to main/master.
|
|
60
|
+
max_chars: Maximum characters of diff to send. Defaults to 5000.
|
|
61
|
+
max_tokens: Maximum tokens to generate. Defaults to 300.
|
|
62
|
+
temperature: Sampling temperature. Defaults to 0.3.
|
|
63
|
+
client: LemonadeClient instance. Creates new one if not provided.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
PRDescription with generated title and description.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If no differences found between branches.
|
|
70
|
+
ConnectionError: If Lemonade is not running.
|
|
71
|
+
"""
|
|
72
|
+
max_chars = max_chars or int(os.environ.get("DIFFRON_MAX_DIFF_CHARS", DEFAULT_MAX_CHARS))
|
|
73
|
+
max_tokens = max_tokens or DEFAULT_MAX_TOKENS
|
|
74
|
+
temperature = temperature if temperature is not None else DEFAULT_TEMPERATURE
|
|
75
|
+
|
|
76
|
+
# Get branch name if not provided
|
|
77
|
+
if branch is None:
|
|
78
|
+
branch = get_current_branch()
|
|
79
|
+
if branch is None:
|
|
80
|
+
raise ValueError("Not in a git repository or no current branch.")
|
|
81
|
+
|
|
82
|
+
# Get base branch if not provided
|
|
83
|
+
if base is None:
|
|
84
|
+
base = find_default_branch()
|
|
85
|
+
|
|
86
|
+
# Get commit log and diff
|
|
87
|
+
commit_log = get_commit_log(branch, base)
|
|
88
|
+
diff = get_branch_diff(branch, base, max_chars=max_chars)
|
|
89
|
+
|
|
90
|
+
if not diff.strip() and not commit_log.strip():
|
|
91
|
+
raise ValueError(f"No differences found between {base} and {branch}.")
|
|
92
|
+
|
|
93
|
+
# Create client if not provided
|
|
94
|
+
if client is None:
|
|
95
|
+
client = LemonadeClient()
|
|
96
|
+
|
|
97
|
+
# Build prompt
|
|
98
|
+
prompt = (
|
|
99
|
+
"Generate a GitHub PR title and description based on these changes.\n"
|
|
100
|
+
"Format strictly as:\n"
|
|
101
|
+
"TITLE: <concise, descriptive title>\n"
|
|
102
|
+
"DESCRIPTION: <detailed description with bullet points>\n\n"
|
|
103
|
+
"Requirements:\n"
|
|
104
|
+
"- TITLE should be under 80 characters\n"
|
|
105
|
+
"- DESCRIPTION should summarize the main changes\n"
|
|
106
|
+
"- Use bullet points for key changes\n"
|
|
107
|
+
"- Mention any breaking changes if applicable\n\n"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if commit_log:
|
|
111
|
+
prompt += f"Commits:\n{commit_log}\n\n"
|
|
112
|
+
|
|
113
|
+
if diff:
|
|
114
|
+
prompt += f"Diff:\n{diff}"
|
|
115
|
+
|
|
116
|
+
messages = [{"role": "user", "content": prompt}]
|
|
117
|
+
|
|
118
|
+
# Generate completion
|
|
119
|
+
response = client.chat_completion(
|
|
120
|
+
messages=messages,
|
|
121
|
+
max_tokens=max_tokens,
|
|
122
|
+
temperature=temperature,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Parse response
|
|
126
|
+
title, description = _parse_pr_response(response)
|
|
127
|
+
|
|
128
|
+
return PRDescription(title=title, description=description)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _parse_pr_response(response: str) -> Tuple[str, str]:
|
|
132
|
+
"""
|
|
133
|
+
Parse PR response into title and description.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
response: Raw response from LLM.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Tuple of (title, description).
|
|
140
|
+
"""
|
|
141
|
+
lines = response.strip().split("\n")
|
|
142
|
+
|
|
143
|
+
title = ""
|
|
144
|
+
description_lines = []
|
|
145
|
+
in_description = False
|
|
146
|
+
|
|
147
|
+
for line in lines:
|
|
148
|
+
if line.startswith("TITLE:"):
|
|
149
|
+
title = line.replace("TITLE:", "").strip()
|
|
150
|
+
elif line.startswith("DESCRIPTION:"):
|
|
151
|
+
in_description = True
|
|
152
|
+
desc_part = line.replace("DESCRIPTION:", "").strip()
|
|
153
|
+
if desc_part:
|
|
154
|
+
description_lines.append(desc_part)
|
|
155
|
+
elif in_description:
|
|
156
|
+
description_lines.append(line)
|
|
157
|
+
|
|
158
|
+
# Fallback: if no TITLE found, use first line
|
|
159
|
+
if not title and lines:
|
|
160
|
+
title = lines[0].strip()
|
|
161
|
+
description_lines = lines[1:]
|
|
162
|
+
|
|
163
|
+
# Fallback: if no DESCRIPTION found, use remaining lines
|
|
164
|
+
if not description_lines and len(lines) > 1:
|
|
165
|
+
description_lines = lines[1:]
|
|
166
|
+
|
|
167
|
+
description = "\n".join(description_lines).strip()
|
|
168
|
+
|
|
169
|
+
# Clean up markdown code blocks
|
|
170
|
+
if description.startswith("```"):
|
|
171
|
+
lines = description.split("\n")
|
|
172
|
+
description = "\n".join(
|
|
173
|
+
line for line in lines
|
|
174
|
+
if not line.startswith("```")
|
|
175
|
+
).strip()
|
|
176
|
+
|
|
177
|
+
return title, description
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def create_github_pr(
|
|
181
|
+
branch: Optional[str] = None,
|
|
182
|
+
base: Optional[str] = None,
|
|
183
|
+
auto_submit: bool = False,
|
|
184
|
+
) -> PRDescription:
|
|
185
|
+
"""
|
|
186
|
+
Generate PR and optionally create it via GitHub CLI.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
branch: Branch name. Defaults to current branch.
|
|
190
|
+
base: Base branch. Defaults to main/master.
|
|
191
|
+
auto_submit: If True, creates PR via gh CLI automatically.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Generated PRDescription.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
RuntimeError: If gh CLI is not available when auto_submit=True.
|
|
198
|
+
"""
|
|
199
|
+
import subprocess
|
|
200
|
+
|
|
201
|
+
# Generate PR description
|
|
202
|
+
pr = generate_pr_description(branch=branch, base=base)
|
|
203
|
+
|
|
204
|
+
if auto_submit:
|
|
205
|
+
# Check if gh CLI is available
|
|
206
|
+
try:
|
|
207
|
+
result = subprocess.run(
|
|
208
|
+
["gh", "--version"],
|
|
209
|
+
capture_output=True,
|
|
210
|
+
text=True,
|
|
211
|
+
timeout=10,
|
|
212
|
+
)
|
|
213
|
+
if result.returncode != 0:
|
|
214
|
+
raise RuntimeError("GitHub CLI (gh) is not installed or not in PATH.")
|
|
215
|
+
except FileNotFoundError:
|
|
216
|
+
raise RuntimeError("GitHub CLI (gh) is not installed or not in PATH.")
|
|
217
|
+
|
|
218
|
+
# Create PR
|
|
219
|
+
subprocess.run(
|
|
220
|
+
["gh", "pr", "create", "--title", pr.title, "--body", pr.description],
|
|
221
|
+
timeout=60,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return pr
|
diffron/pr_gen.pyi
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PR description generation for Diffron.
|
|
3
|
+
|
|
4
|
+
Generates PR titles and descriptions from branch diffs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional, Tuple
|
|
9
|
+
from .lemonade import LemonadeClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEFAULT_MAX_CHARS: int
|
|
13
|
+
DEFAULT_MAX_TOKENS: int
|
|
14
|
+
DEFAULT_TEMPERATURE: float
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class PRDescription:
|
|
19
|
+
"""Represents a generated PR title and description."""
|
|
20
|
+
|
|
21
|
+
title: str
|
|
22
|
+
description: str
|
|
23
|
+
|
|
24
|
+
def format_output(self) -> str:
|
|
25
|
+
"""Format as TITLE + DESCRIPTION output."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
def to_github_cli(self) -> Tuple[str, str]:
|
|
29
|
+
"""Get title and body for GitHub CLI."""
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_pr_description(
|
|
34
|
+
branch: Optional[str] = None,
|
|
35
|
+
base: Optional[str] = None,
|
|
36
|
+
max_chars: Optional[int] = None,
|
|
37
|
+
max_tokens: Optional[int] = None,
|
|
38
|
+
temperature: Optional[float] = None,
|
|
39
|
+
client: Optional[LemonadeClient] = None,
|
|
40
|
+
) -> PRDescription:
|
|
41
|
+
"""Generate a PR title and description from branch changes."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_pr_response(response: str) -> Tuple[str, str]:
|
|
46
|
+
"""Parse PR response into title and description."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def create_github_pr(
|
|
51
|
+
branch: Optional[str] = None,
|
|
52
|
+
base: Optional[str] = None,
|
|
53
|
+
auto_submit: bool = False,
|
|
54
|
+
) -> PRDescription:
|
|
55
|
+
"""Generate PR and optionally create it via GitHub CLI."""
|
|
56
|
+
...
|
diffron/py.typed
ADDED
|
File without changes
|