sugarai 3.1.1.dev5__tar.gz → 3.1.1.dev6__tar.gz
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.
- {sugarai-3.1.1.dev5/sugarai.egg-info → sugarai-3.1.1.dev6}/PKG-INFO +11 -1
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/README.md +10 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/pyproject.toml +1 -1
- sugarai-3.1.1.dev6/sugar/config/__init__.py +6 -0
- sugarai-3.1.1.dev6/sugar/config/prompt_config.py +135 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/profiles/issue_responder.py +16 -1
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6/sugarai.egg-info}/PKG-INFO +11 -1
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugarai.egg-info/SOURCES.txt +2 -0
- sugarai-3.1.1.dev6/tests/test_prompt_config.py +359 -0
- sugarai-3.1.1.dev5/sugar/config/__init__.py +0 -5
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/LICENSE +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/setup.cfg +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/__version__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/agent/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/agent/base.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/agent/hooks.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/agent/subagent_manager.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/agent/tools.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/billing/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/billing/api_keys.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/billing/tiers.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/billing/usage.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/config/issue_responder_config.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/core/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/core/loop.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/discovery/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/discovery/code_quality.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/discovery/error_monitor.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/discovery/github_watcher.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/discovery/test_coverage.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/executor/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/executor/agent_sdk_executor.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/executor/base.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/executor/claude_wrapper.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/executor/structured_request.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/integrations/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/integrations/github.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/learning/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/learning/adaptive_scheduler.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/learning/feedback_processor.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/main.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/mcp/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/mcp/server.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/orchestration/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/orchestration/agent_router.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/orchestration/task_orchestrator.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/profiles/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/profiles/base.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/profiles/default.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/coordinator.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/diff_validator.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/evidence.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/failure_handler.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/functional_verifier.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/preflight_checks.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/success_criteria.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/test_validator.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/quality_gates/truth_enforcer.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/storage/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/storage/issue_response_manager.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/storage/task_type_manager.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/storage/work_queue.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/utils/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/utils/git_operations.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/utils/toon_encoder.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/workflow/__init__.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugar/workflow/orchestrator.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugarai.egg-info/dependency_links.txt +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugarai.egg-info/entry_points.txt +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugarai.egg-info/requires.txt +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/sugarai.egg-info/top_level.txt +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_agent.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_billing.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_cli.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_core_loop.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_goose_integration.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_hold_functionality.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_hooks.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_issue_responder_cli.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_issue_response_integration.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_orchestration.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_profiles.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_quality_gates.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_storage.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_subagent_manager.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_task_types.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_toon_encoder.py +0 -0
- {sugarai-3.1.1.dev5 → sugarai-3.1.1.dev6}/tests/test_v3_benchmarks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sugarai
|
|
3
|
-
Version: 3.1.1.
|
|
3
|
+
Version: 3.1.1.dev6
|
|
4
4
|
Summary: Sugar 🍰 - A dev team that never stops. Delegate full tasks to AI in the background.
|
|
5
5
|
Author-email: Steven Leggett <contact@roboticforce.io>
|
|
6
6
|
License: MIT
|
|
@@ -228,6 +228,16 @@ sugar issue respond 42 --post
|
|
|
228
228
|
|
|
229
229
|
The Issue Responder evaluates confidence before posting. Use `--force-post` to override the confidence check, or adjust the threshold with `--confidence-threshold`.
|
|
230
230
|
|
|
231
|
+
**Custom Prompts:** Customize Sugar's responses per-project by creating `.sugar/prompts/issue_responder.json`:
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"instructions": "You are a helpful assistant for MyProject. Be friendly and professional.",
|
|
236
|
+
"guidelines": ["Always search the codebase first", "Include file paths"],
|
|
237
|
+
"constraints": ["Never share API keys", "Don't promise release dates"]
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
231
241
|
**Full documentation:** [docs/issue-responder.md](docs/issue-responder.md)
|
|
232
242
|
|
|
233
243
|
## How It Works
|
|
@@ -174,6 +174,16 @@ sugar issue respond 42 --post
|
|
|
174
174
|
|
|
175
175
|
The Issue Responder evaluates confidence before posting. Use `--force-post` to override the confidence check, or adjust the threshold with `--confidence-threshold`.
|
|
176
176
|
|
|
177
|
+
**Custom Prompts:** Customize Sugar's responses per-project by creating `.sugar/prompts/issue_responder.json`:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"instructions": "You are a helpful assistant for MyProject. Be friendly and professional.",
|
|
182
|
+
"guidelines": ["Always search the codebase first", "Include file paths"],
|
|
183
|
+
"constraints": ["Never share API keys", "Don't promise release dates"]
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
177
187
|
**Full documentation:** [docs/issue-responder.md](docs/issue-responder.md)
|
|
178
188
|
|
|
179
189
|
## How It Works
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sugarai"
|
|
7
|
-
version = "3.1.1.
|
|
7
|
+
version = "3.1.1.dev6"
|
|
8
8
|
description = "Sugar 🍰 - A dev team that never stops. Delegate full tasks to AI in the background."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt configuration for Issue Responder
|
|
3
|
+
|
|
4
|
+
Loads custom prompt configuration from .sugar/prompts/issue_responder.json
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class PersonaConfig:
|
|
18
|
+
"""Agent persona configuration"""
|
|
19
|
+
|
|
20
|
+
role: Optional[str] = None
|
|
21
|
+
goal: Optional[str] = None
|
|
22
|
+
expertise: List[str] = field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_dict(cls, data: dict) -> "PersonaConfig":
|
|
26
|
+
"""Create PersonaConfig from a dictionary.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
data: Dictionary containing persona configuration
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
PersonaConfig instance
|
|
33
|
+
"""
|
|
34
|
+
return cls(
|
|
35
|
+
role=data.get("role"),
|
|
36
|
+
goal=data.get("goal"),
|
|
37
|
+
expertise=data.get("expertise", []),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class PromptConfig:
|
|
43
|
+
"""Custom prompt configuration for Issue Responder"""
|
|
44
|
+
|
|
45
|
+
instructions: str
|
|
46
|
+
name: Optional[str] = None
|
|
47
|
+
description: Optional[str] = None
|
|
48
|
+
persona: Optional[PersonaConfig] = None
|
|
49
|
+
guidelines: List[str] = field(default_factory=list)
|
|
50
|
+
constraints: List[str] = field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_dict(cls, data: dict) -> "PromptConfig":
|
|
54
|
+
"""Load from dictionary"""
|
|
55
|
+
persona_data = data.get("persona")
|
|
56
|
+
persona = PersonaConfig.from_dict(persona_data) if persona_data else None
|
|
57
|
+
|
|
58
|
+
return cls(
|
|
59
|
+
instructions=data.get("instructions", ""),
|
|
60
|
+
name=data.get("name"),
|
|
61
|
+
description=data.get("description"),
|
|
62
|
+
persona=persona,
|
|
63
|
+
guidelines=data.get("guidelines", []),
|
|
64
|
+
constraints=data.get("constraints", []),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def load_from_file(cls, config_path: Path = None) -> Optional["PromptConfig"]:
|
|
69
|
+
"""
|
|
70
|
+
Load from JSON file.
|
|
71
|
+
|
|
72
|
+
Default path: .sugar/prompts/issue_responder.json
|
|
73
|
+
Returns None if file doesn't exist (use defaults)
|
|
74
|
+
"""
|
|
75
|
+
if config_path is None:
|
|
76
|
+
config_path = Path(".sugar/prompts/issue_responder.json")
|
|
77
|
+
|
|
78
|
+
if not config_path.exists():
|
|
79
|
+
logger.debug(f"No custom prompt config at {config_path}")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
with open(config_path) as f:
|
|
84
|
+
data = json.load(f)
|
|
85
|
+
return cls.from_dict(data)
|
|
86
|
+
except json.JSONDecodeError as e:
|
|
87
|
+
logger.error(f"Invalid JSON in {config_path}: {e}")
|
|
88
|
+
return None
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Error loading prompt config: {e}")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def validate(self) -> List[str]:
|
|
94
|
+
"""Validate configuration, return list of errors"""
|
|
95
|
+
errors = []
|
|
96
|
+
if not self.instructions or not self.instructions.strip():
|
|
97
|
+
errors.append("instructions is required and cannot be empty")
|
|
98
|
+
return errors
|
|
99
|
+
|
|
100
|
+
def build_system_prompt_additions(self) -> str:
|
|
101
|
+
"""
|
|
102
|
+
Build the system prompt additions from this config.
|
|
103
|
+
Returns a string to be added to the base system prompt.
|
|
104
|
+
"""
|
|
105
|
+
parts = []
|
|
106
|
+
|
|
107
|
+
# Add persona section
|
|
108
|
+
if self.persona:
|
|
109
|
+
persona_parts = []
|
|
110
|
+
if self.persona.role:
|
|
111
|
+
persona_parts.append(f"**Role**: {self.persona.role}")
|
|
112
|
+
if self.persona.goal:
|
|
113
|
+
persona_parts.append(f"**Goal**: {self.persona.goal}")
|
|
114
|
+
if self.persona.expertise:
|
|
115
|
+
persona_parts.append(
|
|
116
|
+
f"**Expertise**: {', '.join(self.persona.expertise)}"
|
|
117
|
+
)
|
|
118
|
+
if persona_parts:
|
|
119
|
+
parts.append("## Your Persona\n" + "\n".join(persona_parts))
|
|
120
|
+
|
|
121
|
+
# Add instructions
|
|
122
|
+
if self.instructions:
|
|
123
|
+
parts.append(f"## Project-Specific Instructions\n{self.instructions}")
|
|
124
|
+
|
|
125
|
+
# Add guidelines
|
|
126
|
+
if self.guidelines:
|
|
127
|
+
guidelines_text = "\n".join(f"- {g}" for g in self.guidelines)
|
|
128
|
+
parts.append(f"## Guidelines\n{guidelines_text}")
|
|
129
|
+
|
|
130
|
+
# Add constraints
|
|
131
|
+
if self.constraints:
|
|
132
|
+
constraints_text = "\n".join(f"- {c}" for c in self.constraints)
|
|
133
|
+
parts.append(f"## Constraints\n{constraints_text}")
|
|
134
|
+
|
|
135
|
+
return "\n\n".join(parts)
|
|
@@ -15,6 +15,7 @@ import re
|
|
|
15
15
|
from dataclasses import dataclass, field
|
|
16
16
|
from typing import Any, Dict, List, Optional
|
|
17
17
|
|
|
18
|
+
from ..config import PromptConfig
|
|
18
19
|
from .base import BaseProfile, ProfileConfig
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
@@ -110,7 +111,7 @@ class IssueResponderProfile(BaseProfile):
|
|
|
110
111
|
if repo:
|
|
111
112
|
repo_info = f"\nRepository: {repo}"
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
base_prompt = f"""You are Sugar, an AI assistant that helps respond to GitHub issues.
|
|
114
115
|
|
|
115
116
|
{repo_info}
|
|
116
117
|
|
|
@@ -156,6 +157,20 @@ Rate your confidence in your response:
|
|
|
156
157
|
Only auto-post responses with confidence >= 0.8.
|
|
157
158
|
"""
|
|
158
159
|
|
|
160
|
+
# Load custom prompt config if available
|
|
161
|
+
custom_config = PromptConfig.load_from_file()
|
|
162
|
+
if custom_config:
|
|
163
|
+
errors = custom_config.validate()
|
|
164
|
+
if errors:
|
|
165
|
+
logger.warning(f"Invalid prompt config: {errors}")
|
|
166
|
+
else:
|
|
167
|
+
custom_additions = custom_config.build_system_prompt_additions()
|
|
168
|
+
if custom_additions:
|
|
169
|
+
base_prompt += f"\n\n# Custom Configuration\n\n{custom_additions}"
|
|
170
|
+
logger.info("Loaded custom prompt configuration")
|
|
171
|
+
|
|
172
|
+
return base_prompt
|
|
173
|
+
|
|
159
174
|
async def process_input(self, input_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
160
175
|
"""Process issue data for analysis"""
|
|
161
176
|
issue = input_data.get("issue", {})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sugarai
|
|
3
|
-
Version: 3.1.1.
|
|
3
|
+
Version: 3.1.1.dev6
|
|
4
4
|
Summary: Sugar 🍰 - A dev team that never stops. Delegate full tasks to AI in the background.
|
|
5
5
|
Author-email: Steven Leggett <contact@roboticforce.io>
|
|
6
6
|
License: MIT
|
|
@@ -228,6 +228,16 @@ sugar issue respond 42 --post
|
|
|
228
228
|
|
|
229
229
|
The Issue Responder evaluates confidence before posting. Use `--force-post` to override the confidence check, or adjust the threshold with `--confidence-threshold`.
|
|
230
230
|
|
|
231
|
+
**Custom Prompts:** Customize Sugar's responses per-project by creating `.sugar/prompts/issue_responder.json`:
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"instructions": "You are a helpful assistant for MyProject. Be friendly and professional.",
|
|
236
|
+
"guidelines": ["Always search the codebase first", "Include file paths"],
|
|
237
|
+
"constraints": ["Never share API keys", "Don't promise release dates"]
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
231
241
|
**Full documentation:** [docs/issue-responder.md](docs/issue-responder.md)
|
|
232
242
|
|
|
233
243
|
## How It Works
|
|
@@ -15,6 +15,7 @@ sugar/billing/tiers.py
|
|
|
15
15
|
sugar/billing/usage.py
|
|
16
16
|
sugar/config/__init__.py
|
|
17
17
|
sugar/config/issue_responder_config.py
|
|
18
|
+
sugar/config/prompt_config.py
|
|
18
19
|
sugar/core/__init__.py
|
|
19
20
|
sugar/core/loop.py
|
|
20
21
|
sugar/discovery/__init__.py
|
|
@@ -77,6 +78,7 @@ tests/test_issue_responder_cli.py
|
|
|
77
78
|
tests/test_issue_response_integration.py
|
|
78
79
|
tests/test_orchestration.py
|
|
79
80
|
tests/test_profiles.py
|
|
81
|
+
tests/test_prompt_config.py
|
|
80
82
|
tests/test_quality_gates.py
|
|
81
83
|
tests/test_storage.py
|
|
82
84
|
tests/test_subagent_manager.py
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for prompt configuration system
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import pytest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from sugar.config.prompt_config import PersonaConfig, PromptConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestPersonaConfig:
|
|
13
|
+
"""Test PersonaConfig dataclass"""
|
|
14
|
+
|
|
15
|
+
def test_default_values(self):
|
|
16
|
+
"""Test default values for PersonaConfig"""
|
|
17
|
+
persona = PersonaConfig()
|
|
18
|
+
|
|
19
|
+
assert persona.role is None
|
|
20
|
+
assert persona.goal is None
|
|
21
|
+
assert persona.expertise == []
|
|
22
|
+
|
|
23
|
+
def test_from_dict_with_full_data(self):
|
|
24
|
+
"""Test from_dict with all fields populated"""
|
|
25
|
+
data = {
|
|
26
|
+
"role": "Senior QA Engineer",
|
|
27
|
+
"goal": "Ensure high-quality test coverage",
|
|
28
|
+
"expertise": ["pytest", "playwright", "test automation"],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
persona = PersonaConfig.from_dict(data)
|
|
32
|
+
|
|
33
|
+
assert persona.role == "Senior QA Engineer"
|
|
34
|
+
assert persona.goal == "Ensure high-quality test coverage"
|
|
35
|
+
assert persona.expertise == ["pytest", "playwright", "test automation"]
|
|
36
|
+
|
|
37
|
+
def test_from_dict_with_partial_data(self):
|
|
38
|
+
"""Test from_dict with only some fields"""
|
|
39
|
+
data = {"role": "QA Engineer"}
|
|
40
|
+
|
|
41
|
+
persona = PersonaConfig.from_dict(data)
|
|
42
|
+
|
|
43
|
+
assert persona.role == "QA Engineer"
|
|
44
|
+
assert persona.goal is None
|
|
45
|
+
assert persona.expertise == []
|
|
46
|
+
|
|
47
|
+
def test_from_dict_with_empty_dict(self):
|
|
48
|
+
"""Test from_dict with empty dictionary"""
|
|
49
|
+
persona = PersonaConfig.from_dict({})
|
|
50
|
+
|
|
51
|
+
assert persona.role is None
|
|
52
|
+
assert persona.goal is None
|
|
53
|
+
assert persona.expertise == []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestPromptConfig:
|
|
57
|
+
"""Test PromptConfig dataclass"""
|
|
58
|
+
|
|
59
|
+
def test_default_values(self):
|
|
60
|
+
"""Test default values for PromptConfig with required instructions"""
|
|
61
|
+
config = PromptConfig(instructions="Always write tests first")
|
|
62
|
+
|
|
63
|
+
assert config.instructions == "Always write tests first"
|
|
64
|
+
assert config.name is None
|
|
65
|
+
assert config.description is None
|
|
66
|
+
assert config.persona is None
|
|
67
|
+
assert config.guidelines == []
|
|
68
|
+
assert config.constraints == []
|
|
69
|
+
|
|
70
|
+
def test_from_dict_with_full_data(self):
|
|
71
|
+
"""Test from_dict with all fields populated"""
|
|
72
|
+
data = {
|
|
73
|
+
"name": "Test-First Development",
|
|
74
|
+
"description": "TDD approach for all features",
|
|
75
|
+
"instructions": "Write tests before implementation",
|
|
76
|
+
"persona": {
|
|
77
|
+
"role": "Senior QA Engineer",
|
|
78
|
+
"goal": "Ensure code quality",
|
|
79
|
+
"expertise": ["pytest", "TDD"],
|
|
80
|
+
},
|
|
81
|
+
"guidelines": ["Use descriptive test names", "Follow AAA pattern"],
|
|
82
|
+
"constraints": ["Minimum 80% coverage", "No skipped tests"],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
config = PromptConfig.from_dict(data)
|
|
86
|
+
|
|
87
|
+
assert config.name == "Test-First Development"
|
|
88
|
+
assert config.description == "TDD approach for all features"
|
|
89
|
+
assert config.instructions == "Write tests before implementation"
|
|
90
|
+
assert config.persona is not None
|
|
91
|
+
assert config.persona.role == "Senior QA Engineer"
|
|
92
|
+
assert config.persona.goal == "Ensure code quality"
|
|
93
|
+
assert config.persona.expertise == ["pytest", "TDD"]
|
|
94
|
+
assert config.guidelines == ["Use descriptive test names", "Follow AAA pattern"]
|
|
95
|
+
assert config.constraints == ["Minimum 80% coverage", "No skipped tests"]
|
|
96
|
+
|
|
97
|
+
def test_from_dict_with_partial_data(self):
|
|
98
|
+
"""Test from_dict with only required fields"""
|
|
99
|
+
data = {"instructions": "Follow project coding standards"}
|
|
100
|
+
|
|
101
|
+
config = PromptConfig.from_dict(data)
|
|
102
|
+
|
|
103
|
+
assert config.instructions == "Follow project coding standards"
|
|
104
|
+
assert config.name is None
|
|
105
|
+
assert config.description is None
|
|
106
|
+
assert config.persona is None
|
|
107
|
+
assert config.guidelines == []
|
|
108
|
+
assert config.constraints == []
|
|
109
|
+
|
|
110
|
+
def test_from_dict_without_persona(self):
|
|
111
|
+
"""Test from_dict when persona is not provided"""
|
|
112
|
+
data = {
|
|
113
|
+
"instructions": "Use black for formatting",
|
|
114
|
+
"guidelines": ["Follow PEP 8"],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
config = PromptConfig.from_dict(data)
|
|
118
|
+
|
|
119
|
+
assert config.instructions == "Use black for formatting"
|
|
120
|
+
assert config.persona is None
|
|
121
|
+
assert config.guidelines == ["Follow PEP 8"]
|
|
122
|
+
|
|
123
|
+
def test_validation_missing_instructions(self):
|
|
124
|
+
"""Test validation fails when instructions is missing"""
|
|
125
|
+
config = PromptConfig(instructions="")
|
|
126
|
+
|
|
127
|
+
errors = config.validate()
|
|
128
|
+
|
|
129
|
+
assert len(errors) == 1
|
|
130
|
+
assert "instructions is required and cannot be empty" in errors[0]
|
|
131
|
+
|
|
132
|
+
def test_validation_empty_instructions(self):
|
|
133
|
+
"""Test validation fails when instructions is whitespace only"""
|
|
134
|
+
config = PromptConfig(instructions=" ")
|
|
135
|
+
|
|
136
|
+
errors = config.validate()
|
|
137
|
+
|
|
138
|
+
assert len(errors) == 1
|
|
139
|
+
assert "instructions is required and cannot be empty" in errors[0]
|
|
140
|
+
|
|
141
|
+
def test_validation_valid_config(self):
|
|
142
|
+
"""Test validation passes for valid config"""
|
|
143
|
+
config = PromptConfig(
|
|
144
|
+
instructions="Follow coding standards",
|
|
145
|
+
guidelines=["Use type hints", "Write docstrings"],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
errors = config.validate()
|
|
149
|
+
|
|
150
|
+
assert len(errors) == 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TestLoadFromFile:
|
|
154
|
+
"""Test file loading functionality"""
|
|
155
|
+
|
|
156
|
+
def test_load_from_file_missing_file(self, tmp_path):
|
|
157
|
+
"""Test load_from_file returns None for missing file"""
|
|
158
|
+
config_path = tmp_path / "nonexistent.json"
|
|
159
|
+
|
|
160
|
+
config = PromptConfig.load_from_file(config_path)
|
|
161
|
+
|
|
162
|
+
assert config is None
|
|
163
|
+
|
|
164
|
+
def test_load_from_file_invalid_json(self, tmp_path):
|
|
165
|
+
"""Test load_from_file returns None for invalid JSON"""
|
|
166
|
+
config_path = tmp_path / "invalid.json"
|
|
167
|
+
config_path.write_text("{ invalid json }")
|
|
168
|
+
|
|
169
|
+
config = PromptConfig.load_from_file(config_path)
|
|
170
|
+
|
|
171
|
+
assert config is None
|
|
172
|
+
|
|
173
|
+
def test_load_from_file_valid_json(self, tmp_path):
|
|
174
|
+
"""Test load_from_file returns config for valid JSON"""
|
|
175
|
+
config_path = tmp_path / "config.json"
|
|
176
|
+
data = {
|
|
177
|
+
"name": "Project Config",
|
|
178
|
+
"instructions": "Follow project guidelines",
|
|
179
|
+
"persona": {"role": "Developer", "goal": "Build quality software"},
|
|
180
|
+
"guidelines": ["Write clean code"],
|
|
181
|
+
"constraints": ["No magic numbers"],
|
|
182
|
+
}
|
|
183
|
+
config_path.write_text(json.dumps(data))
|
|
184
|
+
|
|
185
|
+
config = PromptConfig.load_from_file(config_path)
|
|
186
|
+
|
|
187
|
+
assert config is not None
|
|
188
|
+
assert config.name == "Project Config"
|
|
189
|
+
assert config.instructions == "Follow project guidelines"
|
|
190
|
+
assert config.persona is not None
|
|
191
|
+
assert config.persona.role == "Developer"
|
|
192
|
+
assert config.guidelines == ["Write clean code"]
|
|
193
|
+
assert config.constraints == ["No magic numbers"]
|
|
194
|
+
|
|
195
|
+
def test_load_from_file_minimal_valid_json(self, tmp_path):
|
|
196
|
+
"""Test load_from_file with minimal valid JSON"""
|
|
197
|
+
config_path = tmp_path / "minimal.json"
|
|
198
|
+
data = {"instructions": "Minimal config"}
|
|
199
|
+
config_path.write_text(json.dumps(data))
|
|
200
|
+
|
|
201
|
+
config = PromptConfig.load_from_file(config_path)
|
|
202
|
+
|
|
203
|
+
assert config is not None
|
|
204
|
+
assert config.instructions == "Minimal config"
|
|
205
|
+
assert config.persona is None
|
|
206
|
+
assert config.guidelines == []
|
|
207
|
+
assert config.constraints == []
|
|
208
|
+
|
|
209
|
+
def test_load_from_file_default_path(self, tmp_path, monkeypatch):
|
|
210
|
+
"""Test load_from_file uses default path when not specified"""
|
|
211
|
+
# Change to tmp_path directory
|
|
212
|
+
monkeypatch.chdir(tmp_path)
|
|
213
|
+
|
|
214
|
+
# Create default path
|
|
215
|
+
default_dir = tmp_path / ".sugar" / "prompts"
|
|
216
|
+
default_dir.mkdir(parents=True)
|
|
217
|
+
config_path = default_dir / "issue_responder.json"
|
|
218
|
+
data = {"instructions": "Default config"}
|
|
219
|
+
config_path.write_text(json.dumps(data))
|
|
220
|
+
|
|
221
|
+
config = PromptConfig.load_from_file()
|
|
222
|
+
|
|
223
|
+
assert config is not None
|
|
224
|
+
assert config.instructions == "Default config"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class TestBuildSystemPromptAdditions:
|
|
228
|
+
"""Test build_system_prompt_additions functionality"""
|
|
229
|
+
|
|
230
|
+
def test_minimal_config_with_only_instructions(self):
|
|
231
|
+
"""Test returns formatted instructions for minimal config"""
|
|
232
|
+
config = PromptConfig(instructions="Follow coding standards")
|
|
233
|
+
|
|
234
|
+
result = config.build_system_prompt_additions()
|
|
235
|
+
|
|
236
|
+
assert "## Project-Specific Instructions\nFollow coding standards" in result
|
|
237
|
+
assert "## Your Persona" not in result
|
|
238
|
+
assert "## Guidelines" not in result
|
|
239
|
+
assert "## Constraints" not in result
|
|
240
|
+
|
|
241
|
+
def test_includes_persona_section(self):
|
|
242
|
+
"""Test includes persona section when persona provided"""
|
|
243
|
+
config = PromptConfig(
|
|
244
|
+
instructions="Write tests",
|
|
245
|
+
persona=PersonaConfig(
|
|
246
|
+
role="QA Engineer",
|
|
247
|
+
goal="Ensure quality",
|
|
248
|
+
expertise=["pytest", "testing"],
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
result = config.build_system_prompt_additions()
|
|
253
|
+
|
|
254
|
+
assert "## Your Persona" in result
|
|
255
|
+
assert "**Role**: QA Engineer" in result
|
|
256
|
+
assert "**Goal**: Ensure quality" in result
|
|
257
|
+
assert "**Expertise**: pytest, testing" in result
|
|
258
|
+
|
|
259
|
+
def test_includes_guidelines_when_provided(self):
|
|
260
|
+
"""Test includes guidelines section when provided"""
|
|
261
|
+
config = PromptConfig(
|
|
262
|
+
instructions="Write clean code",
|
|
263
|
+
guidelines=["Use type hints", "Write docstrings", "Follow PEP 8"],
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
result = config.build_system_prompt_additions()
|
|
267
|
+
|
|
268
|
+
assert "## Guidelines" in result
|
|
269
|
+
assert "- Use type hints" in result
|
|
270
|
+
assert "- Write docstrings" in result
|
|
271
|
+
assert "- Follow PEP 8" in result
|
|
272
|
+
|
|
273
|
+
def test_includes_constraints_when_provided(self):
|
|
274
|
+
"""Test includes constraints section when provided"""
|
|
275
|
+
config = PromptConfig(
|
|
276
|
+
instructions="Follow rules",
|
|
277
|
+
constraints=[
|
|
278
|
+
"No TODO comments",
|
|
279
|
+
"Max line length 88",
|
|
280
|
+
"Type all functions",
|
|
281
|
+
],
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
result = config.build_system_prompt_additions()
|
|
285
|
+
|
|
286
|
+
assert "## Constraints" in result
|
|
287
|
+
assert "- No TODO comments" in result
|
|
288
|
+
assert "- Max line length 88" in result
|
|
289
|
+
assert "- Type all functions" in result
|
|
290
|
+
|
|
291
|
+
def test_combines_all_sections_properly(self):
|
|
292
|
+
"""Test combines all sections in correct order"""
|
|
293
|
+
config = PromptConfig(
|
|
294
|
+
instructions="Main instructions here",
|
|
295
|
+
persona=PersonaConfig(role="Developer", goal="Build features"),
|
|
296
|
+
guidelines=["Guideline 1", "Guideline 2"],
|
|
297
|
+
constraints=["Constraint 1"],
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
result = config.build_system_prompt_additions()
|
|
301
|
+
|
|
302
|
+
# Check all sections are present
|
|
303
|
+
assert "## Your Persona" in result
|
|
304
|
+
assert "## Project-Specific Instructions" in result
|
|
305
|
+
assert "## Guidelines" in result
|
|
306
|
+
assert "## Constraints" in result
|
|
307
|
+
|
|
308
|
+
# Check order (persona, instructions, guidelines, constraints)
|
|
309
|
+
persona_idx = result.index("## Your Persona")
|
|
310
|
+
instructions_idx = result.index("## Project-Specific Instructions")
|
|
311
|
+
guidelines_idx = result.index("## Guidelines")
|
|
312
|
+
constraints_idx = result.index("## Constraints")
|
|
313
|
+
|
|
314
|
+
assert persona_idx < instructions_idx < guidelines_idx < constraints_idx
|
|
315
|
+
|
|
316
|
+
# Check sections are separated by double newlines
|
|
317
|
+
assert "\n\n" in result
|
|
318
|
+
|
|
319
|
+
def test_persona_with_partial_fields(self):
|
|
320
|
+
"""Test persona section with only some fields populated"""
|
|
321
|
+
config = PromptConfig(
|
|
322
|
+
instructions="Instructions",
|
|
323
|
+
persona=PersonaConfig(role="Developer"), # Only role, no goal/expertise
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
result = config.build_system_prompt_additions()
|
|
327
|
+
|
|
328
|
+
assert "## Your Persona" in result
|
|
329
|
+
assert "**Role**: Developer" in result
|
|
330
|
+
assert "**Goal**:" not in result
|
|
331
|
+
assert "**Expertise**:" not in result
|
|
332
|
+
|
|
333
|
+
def test_empty_persona_not_included(self):
|
|
334
|
+
"""Test persona section not included if all fields are None/empty"""
|
|
335
|
+
config = PromptConfig(
|
|
336
|
+
instructions="Instructions",
|
|
337
|
+
persona=PersonaConfig(), # All fields None/empty
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
result = config.build_system_prompt_additions()
|
|
341
|
+
|
|
342
|
+
# Persona section should not be included if no fields are populated
|
|
343
|
+
assert "## Your Persona" not in result
|
|
344
|
+
|
|
345
|
+
def test_section_formatting(self):
|
|
346
|
+
"""Test proper markdown formatting of sections"""
|
|
347
|
+
config = PromptConfig(
|
|
348
|
+
instructions="These are the instructions.\nMultiline instructions work.",
|
|
349
|
+
guidelines=["First guideline", "Second guideline"],
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
result = config.build_system_prompt_additions()
|
|
353
|
+
|
|
354
|
+
# Check instructions preserve newlines
|
|
355
|
+
assert "These are the instructions.\nMultiline instructions work." in result
|
|
356
|
+
|
|
357
|
+
# Check guidelines are bulleted
|
|
358
|
+
assert "- First guideline" in result
|
|
359
|
+
assert "- Second guideline" in result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|