repobrief 0.1.1__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.
- repobrief/__init__.py +3 -0
- repobrief/backends/__init__.py +0 -0
- repobrief/backends/base.py +70 -0
- repobrief/backends/cloud.py +227 -0
- repobrief/backends/ollama.py +209 -0
- repobrief/backends/prompts.py +45 -0
- repobrief/cli.py +515 -0
- repobrief/config/__init__.py +0 -0
- repobrief/config/settings.py +121 -0
- repobrief/errors.py +66 -0
- repobrief/packer/__init__.py +0 -0
- repobrief/packer/formatter.py +298 -0
- repobrief/packer/tree.py +87 -0
- repobrief/scanner/__init__.py +0 -0
- repobrief/scanner/git_utils.py +64 -0
- repobrief/scanner/ignore.py +146 -0
- repobrief/scanner/models.py +31 -0
- repobrief/scanner/walker.py +157 -0
- repobrief/scoring/__init__.py +0 -0
- repobrief/scoring/scorer.py +331 -0
- repobrief/scoring/selector.py +79 -0
- repobrief/scoring/tokenizer.py +64 -0
- repobrief/security/__init__.py +0 -0
- repobrief/security/secret_scanner.py +266 -0
- repobrief/utils/__init__.py +0 -0
- repobrief/utils/clipboard.py +26 -0
- repobrief/utils/console.py +151 -0
- repobrief/utils/github.py +83 -0
- repobrief-0.1.1.dist-info/METADATA +256 -0
- repobrief-0.1.1.dist-info/RECORD +33 -0
- repobrief-0.1.1.dist-info/WHEEL +4 -0
- repobrief-0.1.1.dist-info/entry_points.txt +2 -0
- repobrief-0.1.1.dist-info/licenses/LICENSE +21 -0
repobrief/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Abstract base class for LLM backends.
|
|
2
|
+
|
|
3
|
+
All backends (cloud, Ollama, future local runtimes) must implement this interface.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ChatMessage:
|
|
15
|
+
"""A single message in a conversation.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
role: 'user', 'assistant', or 'system'.
|
|
19
|
+
content: The message text.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
role: str # "user" | "assistant" | "system"
|
|
23
|
+
content: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LLMBackend(ABC):
|
|
27
|
+
"""Abstract interface for LLM backends.
|
|
28
|
+
|
|
29
|
+
Implementing classes must provide:
|
|
30
|
+
- generate(): Send a prompt and stream back response tokens
|
|
31
|
+
- validate(): Check that the backend is properly configured and reachable
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def generate(
|
|
36
|
+
self,
|
|
37
|
+
system_prompt: str,
|
|
38
|
+
user_message: str,
|
|
39
|
+
history: list[ChatMessage] | None = None,
|
|
40
|
+
) -> Iterator[str]:
|
|
41
|
+
"""Send a prompt and yield response tokens as they stream in.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
system_prompt: System-level instructions (includes the repo digest).
|
|
45
|
+
user_message: The user's question.
|
|
46
|
+
history: Previous conversation messages (for REPL mode).
|
|
47
|
+
|
|
48
|
+
Yields:
|
|
49
|
+
String chunks of the response as they arrive.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ConnectionError: If the backend is unreachable.
|
|
53
|
+
PermissionError: If authentication fails (bad API key).
|
|
54
|
+
RuntimeError: For other backend errors.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def validate(self) -> tuple[bool, str]:
|
|
59
|
+
"""Check if the backend is properly configured and reachable.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Tuple of (is_valid, message).
|
|
63
|
+
- (True, "Ready") if everything is configured.
|
|
64
|
+
- (False, "Missing API key...") if there's a problem.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def name(self) -> str:
|
|
70
|
+
"""Human-readable name of this backend (e.g., 'Anthropic Claude')."""
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Cloud LLM backend -- supports Anthropic (Claude) and OpenAI (GPT) APIs.
|
|
2
|
+
|
|
3
|
+
Determines which provider to use based on the model name:
|
|
4
|
+
- Models starting with 'claude' -> Anthropic API
|
|
5
|
+
- Models starting with 'gpt' or 'o1' -> OpenAI API
|
|
6
|
+
- Explicit provider prefix: 'anthropic:model-name' or 'openai:model-name'
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
|
|
14
|
+
from repobrief.backends.base import ChatMessage, LLMBackend
|
|
15
|
+
|
|
16
|
+
# -- Provider Detection --------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _detect_provider(model: str) -> str:
|
|
20
|
+
"""Detect the API provider from the model name.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
model: Model name string.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
'anthropic' or 'openai'.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: If the provider cannot be determined.
|
|
30
|
+
"""
|
|
31
|
+
model_lower = model.lower()
|
|
32
|
+
|
|
33
|
+
# Explicit prefix
|
|
34
|
+
if model_lower.startswith("anthropic:"):
|
|
35
|
+
return "anthropic"
|
|
36
|
+
if model_lower.startswith("openai:"):
|
|
37
|
+
return "openai"
|
|
38
|
+
|
|
39
|
+
# Infer from model name
|
|
40
|
+
if model_lower.startswith("claude"):
|
|
41
|
+
return "anthropic"
|
|
42
|
+
if model_lower.startswith(("gpt", "o1", "o3")):
|
|
43
|
+
return "openai"
|
|
44
|
+
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"Cannot determine provider for model '{model}'. "
|
|
47
|
+
f"Use a prefix: 'anthropic:{model}' or 'openai:{model}'"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _strip_provider_prefix(model: str) -> str:
|
|
52
|
+
"""Remove the provider prefix from a model name."""
|
|
53
|
+
if ":" in model:
|
|
54
|
+
return model.split(":", 1)[1]
|
|
55
|
+
return model
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CloudBackend(LLMBackend):
|
|
59
|
+
"""Cloud LLM backend using Anthropic or OpenAI APIs.
|
|
60
|
+
|
|
61
|
+
Usage:
|
|
62
|
+
backend = CloudBackend(model="claude-sonnet-4-20250514", api_key="sk-...")
|
|
63
|
+
for chunk in backend.generate(system_prompt, question):
|
|
64
|
+
print(chunk, end="")
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
model: str = "claude-sonnet-4-20250514",
|
|
70
|
+
api_key: str | None = None,
|
|
71
|
+
max_output_tokens: int = 4096,
|
|
72
|
+
):
|
|
73
|
+
"""Initialize the cloud backend.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
model: Model identifier (e.g., 'claude-sonnet-4-20250514', 'gpt-4o').
|
|
77
|
+
api_key: API key. If None, reads from environment variables.
|
|
78
|
+
max_output_tokens: Maximum tokens in the response.
|
|
79
|
+
"""
|
|
80
|
+
self._model = _strip_provider_prefix(model)
|
|
81
|
+
self._provider = _detect_provider(model)
|
|
82
|
+
self._max_output_tokens = max_output_tokens
|
|
83
|
+
|
|
84
|
+
# Resolve API key
|
|
85
|
+
if api_key:
|
|
86
|
+
self._api_key = api_key
|
|
87
|
+
elif self._provider == "anthropic":
|
|
88
|
+
self._api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
89
|
+
elif self._provider == "openai":
|
|
90
|
+
self._api_key = os.environ.get("OPENAI_API_KEY")
|
|
91
|
+
else:
|
|
92
|
+
self._api_key = None
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def name(self) -> str:
|
|
96
|
+
provider_name = "Anthropic Claude" if self._provider == "anthropic" else "OpenAI"
|
|
97
|
+
return f"{provider_name} ({self._model})"
|
|
98
|
+
|
|
99
|
+
def validate(self) -> tuple[bool, str]:
|
|
100
|
+
"""Check that the API key is set and the SDK is installed."""
|
|
101
|
+
if not self._api_key:
|
|
102
|
+
env_var = "ANTHROPIC_API_KEY" if self._provider == "anthropic" else "OPENAI_API_KEY"
|
|
103
|
+
return (
|
|
104
|
+
False,
|
|
105
|
+
f"No API key found. Set the {env_var} environment variable, "
|
|
106
|
+
f"or pass it in .repobrief.yml under 'api_key'.\n"
|
|
107
|
+
f"Example: export {env_var}=sk-your-key-here",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Check if the SDK is installed
|
|
111
|
+
try:
|
|
112
|
+
if self._provider == "anthropic":
|
|
113
|
+
import anthropic # noqa: F401
|
|
114
|
+
else:
|
|
115
|
+
import openai # noqa: F401
|
|
116
|
+
except ImportError:
|
|
117
|
+
sdk = "anthropic" if self._provider == "anthropic" else "openai"
|
|
118
|
+
return (
|
|
119
|
+
False,
|
|
120
|
+
f"The '{sdk}' package is not installed. "
|
|
121
|
+
f"Install it with: pip install repobrief[cloud]",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return (True, f"Ready -- using {self.name}")
|
|
125
|
+
|
|
126
|
+
def generate(
|
|
127
|
+
self,
|
|
128
|
+
system_prompt: str,
|
|
129
|
+
user_message: str,
|
|
130
|
+
history: list[ChatMessage] | None = None,
|
|
131
|
+
) -> Iterator[str]:
|
|
132
|
+
"""Send a prompt to the cloud API and stream back response tokens.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
system_prompt: System instructions (includes the repo digest).
|
|
136
|
+
user_message: The user's question.
|
|
137
|
+
history: Previous conversation messages.
|
|
138
|
+
|
|
139
|
+
Yields:
|
|
140
|
+
String chunks of the response.
|
|
141
|
+
"""
|
|
142
|
+
if self._provider == "anthropic":
|
|
143
|
+
yield from self._generate_anthropic(system_prompt, user_message, history)
|
|
144
|
+
else:
|
|
145
|
+
yield from self._generate_openai(system_prompt, user_message, history)
|
|
146
|
+
|
|
147
|
+
def _generate_anthropic(
|
|
148
|
+
self,
|
|
149
|
+
system_prompt: str,
|
|
150
|
+
user_message: str,
|
|
151
|
+
history: list[ChatMessage] | None = None,
|
|
152
|
+
) -> Iterator[str]:
|
|
153
|
+
"""Generate using the Anthropic API."""
|
|
154
|
+
import anthropic
|
|
155
|
+
|
|
156
|
+
client = anthropic.Anthropic(api_key=self._api_key)
|
|
157
|
+
|
|
158
|
+
# Build messages list
|
|
159
|
+
messages = []
|
|
160
|
+
if history:
|
|
161
|
+
for msg in history:
|
|
162
|
+
if msg.role in ("user", "assistant"):
|
|
163
|
+
messages.append({"role": msg.role, "content": msg.content})
|
|
164
|
+
messages.append({"role": "user", "content": user_message})
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
with client.messages.stream(
|
|
168
|
+
model=self._model,
|
|
169
|
+
max_tokens=self._max_output_tokens,
|
|
170
|
+
system=system_prompt,
|
|
171
|
+
messages=messages,
|
|
172
|
+
) as stream:
|
|
173
|
+
yield from stream.text_stream
|
|
174
|
+
except anthropic.AuthenticationError as err:
|
|
175
|
+
raise PermissionError(
|
|
176
|
+
"Invalid Anthropic API key. Check your ANTHROPIC_API_KEY."
|
|
177
|
+
) from err
|
|
178
|
+
except anthropic.RateLimitError as err:
|
|
179
|
+
raise RuntimeError(
|
|
180
|
+
"Anthropic API rate limit exceeded. Wait a moment and try again."
|
|
181
|
+
) from err
|
|
182
|
+
except anthropic.APIConnectionError as err:
|
|
183
|
+
raise ConnectionError(
|
|
184
|
+
"Could not connect to the Anthropic API. Check your internet connection."
|
|
185
|
+
) from err
|
|
186
|
+
|
|
187
|
+
def _generate_openai(
|
|
188
|
+
self,
|
|
189
|
+
system_prompt: str,
|
|
190
|
+
user_message: str,
|
|
191
|
+
history: list[ChatMessage] | None = None,
|
|
192
|
+
) -> Iterator[str]:
|
|
193
|
+
"""Generate using the OpenAI API."""
|
|
194
|
+
import openai
|
|
195
|
+
|
|
196
|
+
client = openai.OpenAI(api_key=self._api_key)
|
|
197
|
+
|
|
198
|
+
# Build messages list
|
|
199
|
+
messages = [{"role": "system", "content": system_prompt}]
|
|
200
|
+
if history:
|
|
201
|
+
for msg in history:
|
|
202
|
+
if msg.role in ("user", "assistant"):
|
|
203
|
+
messages.append({"role": msg.role, "content": msg.content})
|
|
204
|
+
messages.append({"role": "user", "content": user_message})
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
stream = client.chat.completions.create(
|
|
208
|
+
model=self._model,
|
|
209
|
+
messages=messages,
|
|
210
|
+
max_tokens=self._max_output_tokens,
|
|
211
|
+
stream=True,
|
|
212
|
+
)
|
|
213
|
+
for chunk in stream:
|
|
214
|
+
if chunk.choices and chunk.choices[0].delta.content:
|
|
215
|
+
yield chunk.choices[0].delta.content
|
|
216
|
+
except openai.AuthenticationError as err:
|
|
217
|
+
raise PermissionError(
|
|
218
|
+
"Invalid OpenAI API key. Check your OPENAI_API_KEY."
|
|
219
|
+
) from err
|
|
220
|
+
except openai.RateLimitError as err:
|
|
221
|
+
raise RuntimeError(
|
|
222
|
+
"OpenAI API rate limit exceeded. Wait a moment and try again."
|
|
223
|
+
) from err
|
|
224
|
+
except openai.APIConnectionError as err:
|
|
225
|
+
raise ConnectionError(
|
|
226
|
+
"Could not connect to the OpenAI API. Check your internet connection."
|
|
227
|
+
) from err
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Ollama local backend -- fully offline LLM chat via Ollama's REST API.
|
|
2
|
+
|
|
3
|
+
Communicates with a locally running Ollama server (default: http://localhost:11434).
|
|
4
|
+
No data leaves the user's machine. Requires Ollama to be installed and running
|
|
5
|
+
with at least one model pulled.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
from repobrief.backends.base import ChatMessage, LLMBackend
|
|
16
|
+
|
|
17
|
+
# Default Ollama server URL
|
|
18
|
+
DEFAULT_OLLAMA_HOST = "http://localhost:11434"
|
|
19
|
+
|
|
20
|
+
# Timeout for health checks and model listing (seconds)
|
|
21
|
+
CHECK_TIMEOUT = 5
|
|
22
|
+
|
|
23
|
+
# Timeout for generation requests -- set high because local models can be slow
|
|
24
|
+
GENERATE_TIMEOUT = 300 # 5 minutes
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OllamaBackend(LLMBackend):
|
|
28
|
+
"""Ollama local backend.
|
|
29
|
+
|
|
30
|
+
Connects to a locally running Ollama server and sends chat requests.
|
|
31
|
+
All processing happens on the user's machine -- no data is sent externally.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
backend = OllamaBackend(model="llama3.1:8b")
|
|
35
|
+
is_valid, msg = backend.validate()
|
|
36
|
+
if is_valid:
|
|
37
|
+
for chunk in backend.generate(system_prompt, question):
|
|
38
|
+
print(chunk, end="")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
model: str = "llama3.2:3b",
|
|
44
|
+
host: str = DEFAULT_OLLAMA_HOST,
|
|
45
|
+
):
|
|
46
|
+
"""Initialize the Ollama backend.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
model: Name of the Ollama model to use (e.g., 'llama3.1:8b').
|
|
50
|
+
host: Ollama server URL (default: http://localhost:11434).
|
|
51
|
+
"""
|
|
52
|
+
self._model = model
|
|
53
|
+
self._host = host.rstrip("/")
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def name(self) -> str:
|
|
57
|
+
return f"Ollama ({self._model})"
|
|
58
|
+
|
|
59
|
+
# -- Server Communication --------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def _is_server_running(self) -> bool:
|
|
62
|
+
"""Check if the Ollama server is responding."""
|
|
63
|
+
try:
|
|
64
|
+
resp = requests.get(f"{self._host}/", timeout=CHECK_TIMEOUT)
|
|
65
|
+
return resp.status_code == 200
|
|
66
|
+
except (requests.ConnectionError, requests.Timeout):
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def _list_models(self) -> list[str]:
|
|
70
|
+
"""Get list of locally available models.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of model name strings (e.g., ['llama3.1:8b', 'phi3:mini']).
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
resp = requests.get(f"{self._host}/api/tags", timeout=CHECK_TIMEOUT)
|
|
77
|
+
if resp.status_code == 200:
|
|
78
|
+
data = resp.json()
|
|
79
|
+
models = data.get("models", [])
|
|
80
|
+
return [m["name"] for m in models]
|
|
81
|
+
except (requests.ConnectionError, requests.Timeout, ValueError, KeyError):
|
|
82
|
+
pass
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
def _is_model_available(self) -> bool:
|
|
86
|
+
"""Check if the configured model is available locally."""
|
|
87
|
+
available = self._list_models()
|
|
88
|
+
# Check exact match and partial match (with/without tag)
|
|
89
|
+
model_base = self._model.split(":")[0]
|
|
90
|
+
return (
|
|
91
|
+
self._model in available
|
|
92
|
+
or any(m.startswith(model_base) for m in available)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# -- LLMBackend Interface --------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def validate(self) -> tuple[bool, str]:
|
|
98
|
+
"""Check if Ollama is running and the model is available.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
(True, "Ready") if everything is configured.
|
|
102
|
+
(False, "detailed error message") if there's a problem.
|
|
103
|
+
"""
|
|
104
|
+
# Check 1: Is Ollama running?
|
|
105
|
+
if not self._is_server_running():
|
|
106
|
+
return (
|
|
107
|
+
False,
|
|
108
|
+
f"Ollama server is not running at {self._host}.\n"
|
|
109
|
+
f"\n"
|
|
110
|
+
f"To fix this:\n"
|
|
111
|
+
f" 1. Install Ollama: https://ollama.com/download\n"
|
|
112
|
+
f" 2. Start the server: ollama serve\n"
|
|
113
|
+
f" 3. Pull a model: ollama pull {self._model}\n"
|
|
114
|
+
f" 4. Try again",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Check 2: Is the model available?
|
|
118
|
+
if not self._is_model_available():
|
|
119
|
+
available = self._list_models()
|
|
120
|
+
available_str = ", ".join(available[:5]) if available else "(none)"
|
|
121
|
+
return (
|
|
122
|
+
False,
|
|
123
|
+
f"Model '{self._model}' is not available locally.\n"
|
|
124
|
+
f"\n"
|
|
125
|
+
f"Available models: {available_str}\n"
|
|
126
|
+
f"\n"
|
|
127
|
+
f"To fix this:\n"
|
|
128
|
+
f" Pull the model: ollama pull {self._model}\n"
|
|
129
|
+
f"\n"
|
|
130
|
+
f"Recommended models for limited RAM:\n"
|
|
131
|
+
f" - llama3.2:3b (~2GB RAM) -- fast, good quality\n"
|
|
132
|
+
f" - phi3:mini (~2.3GB RAM) -- fast, good for code\n"
|
|
133
|
+
f" - llama3.1:8b (~4.5GB RAM) -- better quality, needs more RAM",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return (True, f"Ready -- using {self.name} (local, fully offline)")
|
|
137
|
+
|
|
138
|
+
def generate(
|
|
139
|
+
self,
|
|
140
|
+
system_prompt: str,
|
|
141
|
+
user_message: str,
|
|
142
|
+
history: list[ChatMessage] | None = None,
|
|
143
|
+
) -> Iterator[str]:
|
|
144
|
+
"""Send a prompt to the local Ollama server and stream back response tokens.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
system_prompt: System instructions (includes the repo digest).
|
|
148
|
+
user_message: The user's question.
|
|
149
|
+
history: Previous conversation messages.
|
|
150
|
+
|
|
151
|
+
Yields:
|
|
152
|
+
String chunks of the response as they arrive.
|
|
153
|
+
"""
|
|
154
|
+
# Build messages array
|
|
155
|
+
messages: list[dict[str, str]] = [
|
|
156
|
+
{"role": "system", "content": system_prompt},
|
|
157
|
+
]
|
|
158
|
+
if history:
|
|
159
|
+
for msg in history:
|
|
160
|
+
if msg.role in ("user", "assistant"):
|
|
161
|
+
messages.append({"role": msg.role, "content": msg.content})
|
|
162
|
+
messages.append({"role": "user", "content": user_message})
|
|
163
|
+
|
|
164
|
+
# Send request with streaming
|
|
165
|
+
payload = {
|
|
166
|
+
"model": self._model,
|
|
167
|
+
"messages": messages,
|
|
168
|
+
"stream": True,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
resp = requests.post(
|
|
173
|
+
f"{self._host}/api/chat",
|
|
174
|
+
json=payload,
|
|
175
|
+
stream=True,
|
|
176
|
+
timeout=GENERATE_TIMEOUT,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if resp.status_code != 200:
|
|
180
|
+
error_text = resp.text[:500]
|
|
181
|
+
raise RuntimeError(
|
|
182
|
+
f"Ollama returned HTTP {resp.status_code}: {error_text}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Parse streaming response (newline-delimited JSON)
|
|
186
|
+
for line in resp.iter_lines(decode_unicode=True):
|
|
187
|
+
if not line:
|
|
188
|
+
continue
|
|
189
|
+
try:
|
|
190
|
+
data = json.loads(line)
|
|
191
|
+
content = data.get("message", {}).get("content", "")
|
|
192
|
+
if content:
|
|
193
|
+
yield content
|
|
194
|
+
if data.get("done", False):
|
|
195
|
+
break
|
|
196
|
+
except json.JSONDecodeError:
|
|
197
|
+
continue # Skip malformed lines
|
|
198
|
+
|
|
199
|
+
except requests.ConnectionError as err:
|
|
200
|
+
raise ConnectionError(
|
|
201
|
+
f"Lost connection to Ollama server at {self._host}. "
|
|
202
|
+
f"Make sure Ollama is still running."
|
|
203
|
+
) from err
|
|
204
|
+
except requests.Timeout as err:
|
|
205
|
+
raise RuntimeError(
|
|
206
|
+
f"Ollama request timed out after {GENERATE_TIMEOUT} seconds. "
|
|
207
|
+
f"The model may be too large for your system, or the server "
|
|
208
|
+
f"is overloaded."
|
|
209
|
+
) from err
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""System prompt templates for chat mode.
|
|
2
|
+
|
|
3
|
+
The system prompt includes the packed repo digest and instructions
|
|
4
|
+
for how the LLM should behave when answering questions about the codebase.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
SYSTEM_PROMPT_TEMPLATE = """\
|
|
8
|
+
You are RepoBrief, an expert code assistant. You have been given the \
|
|
9
|
+
contents of a software repository and your job is to answer questions \
|
|
10
|
+
about it accurately and helpfully.
|
|
11
|
+
|
|
12
|
+
## Instructions
|
|
13
|
+
|
|
14
|
+
1. **Answer based on the code provided.** Only reference files and code \
|
|
15
|
+
that appear in the digest below. If the answer isn't in the provided \
|
|
16
|
+
code, say so clearly -- do not guess or make up code that isn't there.
|
|
17
|
+
|
|
18
|
+
2. **Be specific.** When referencing code, mention the exact file path \
|
|
19
|
+
and relevant line content. Use code blocks for code snippets.
|
|
20
|
+
|
|
21
|
+
3. **Be concise but thorough.** Give a direct answer first, then explain \
|
|
22
|
+
the reasoning if needed.
|
|
23
|
+
|
|
24
|
+
4. **Suggest improvements when relevant.** If you notice bugs, \
|
|
25
|
+
anti-patterns, or potential improvements, mention them briefly.
|
|
26
|
+
|
|
27
|
+
5. **Respect scope.** You can see only the files included in the digest. \
|
|
28
|
+
There may be other files excluded due to token budget limits.
|
|
29
|
+
|
|
30
|
+
## Repository Digest
|
|
31
|
+
|
|
32
|
+
{digest}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_system_prompt(digest: str) -> str:
|
|
37
|
+
"""Build the complete system prompt with the repo digest embedded.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
digest: The packed repository digest (from the packer module).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Complete system prompt string.
|
|
44
|
+
"""
|
|
45
|
+
return SYSTEM_PROMPT_TEMPLATE.format(digest=digest)
|