opalacoder 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.
- opalacoder/__init__.py +2 -0
- opalacoder/agents.py +233 -0
- opalacoder/agents.yaml +78 -0
- opalacoder/api_keys.py +75 -0
- opalacoder/cli.py +339 -0
- opalacoder/cli_commands.py +277 -0
- opalacoder/config.py +215 -0
- opalacoder/embeddings.py +85 -0
- opalacoder/i18n.py +249 -0
- opalacoder/orchestrator.py +381 -0
- opalacoder/planner.py +206 -0
- opalacoder/project.py +196 -0
- opalacoder/session.py +4 -0
- opalacoder/skills/generaldeveloper.md +52 -0
- opalacoder/skills/html_css_js.md +51 -0
- opalacoder/skills/opalacoder.md +37 -0
- opalacoder/skills/python_subprocess.md +11 -0
- opalacoder/skills/react_vite.md +6 -0
- opalacoder/skills.py +184 -0
- opalacoder/structured.py +113 -0
- opalacoder/terminal.py +186 -0
- opalacoder/tools.py +351 -0
- opalacoder/vcs.py +254 -0
- opalacoder-0.1.0.dist-info/METADATA +230 -0
- opalacoder-0.1.0.dist-info/RECORD +27 -0
- opalacoder-0.1.0.dist-info/WHEEL +4 -0
- opalacoder-0.1.0.dist-info/entry_points.txt +2 -0
opalacoder/__init__.py
ADDED
opalacoder/agents.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Agent factory functions for OpalaCoder."""
|
|
2
|
+
|
|
3
|
+
from agenticblocks.blocks.llm.agent import LLMAgentBlock, AgentInput
|
|
4
|
+
from agenticblocks.blocks.llm.memgpt_agent import MemGPTAgentBlock
|
|
5
|
+
|
|
6
|
+
from .config import DEFAULT_MODEL, LITELLM_DEFAULTS, get_agent_llm_kwargs, get_agent_model, get_agent_max_heartbeats, get_agent_debug
|
|
7
|
+
from . import i18n
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _make_llm(name: str, system_prompt: str, model: str | None, **kwargs) -> LLMAgentBlock:
|
|
11
|
+
lang_name = "English" if i18n._LANG == "en" else "Portuguese"
|
|
12
|
+
lang_rule = f"\n\nCRITICAL RULE: The user interface language is set to {lang_name}. You MUST translate your final responses, explanations, and output to {lang_name}. However, keep your internal reasoning, code variables, and logic in English."
|
|
13
|
+
|
|
14
|
+
resolved_model = get_agent_model(name, model or DEFAULT_MODEL)
|
|
15
|
+
|
|
16
|
+
# Start from per-agent config, then apply any explicit caller overrides (caller wins)
|
|
17
|
+
merged_kwargs = {**get_agent_llm_kwargs(name), **kwargs.get("litellm_kwargs", {})}
|
|
18
|
+
kwargs["litellm_kwargs"] = merged_kwargs
|
|
19
|
+
|
|
20
|
+
return LLMAgentBlock(
|
|
21
|
+
name=name,
|
|
22
|
+
description=name,
|
|
23
|
+
model=resolved_model,
|
|
24
|
+
system_prompt=system_prompt + lang_rule,
|
|
25
|
+
**kwargs,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def make_landscape_planner(model: str | None = None) -> LLMAgentBlock:
|
|
30
|
+
return _make_llm(
|
|
31
|
+
"landscape_planner",
|
|
32
|
+
"""You are a high-level strategic planner working within a specific software project.
|
|
33
|
+
You receive a user request (which includes the project name and path) and produce a GENERAL PANORAMA
|
|
34
|
+
that describes how to accomplish the request inside that project.
|
|
35
|
+
|
|
36
|
+
Your output must:
|
|
37
|
+
- List 3 to 7 main phases in logical order
|
|
38
|
+
- Name each phase with a short title
|
|
39
|
+
- Describe each phase in at most 2 lines (WHAT to do, not HOW)
|
|
40
|
+
- Refer to the project's existing structure when relevant (e.g. "extend the existing auth module")
|
|
41
|
+
|
|
42
|
+
CRITICAL RULES:
|
|
43
|
+
1. NEVER call any function or tool. Output PLAIN TEXT only.
|
|
44
|
+
2. NEVER create phases to "ask the user", "get preferences", or "wait for feedback". If details are missing, DO NOT ask questions. Instead, ASSUME a reasonable default approach and include it in the plan.
|
|
45
|
+
3. If the request is vague, fill in the blanks using industry best practices and proceed autonomously.
|
|
46
|
+
4. Phases are executed autonomously inside the project directory. Create only TECHNICAL IMPLEMENTATION
|
|
47
|
+
phases (e.g. 'Add route handler', 'Update styles'). Include validation inside the same phase.
|
|
48
|
+
5. NEVER suggest creating a new project folder — the active project directory is always the workspace. Work inside it.
|
|
49
|
+
|
|
50
|
+
Output format:
|
|
51
|
+
1. [Phase Name]: [Brief description]
|
|
52
|
+
2. ...
|
|
53
|
+
|
|
54
|
+
Do not implement, do not detail, do not suggest code.
|
|
55
|
+
""",
|
|
56
|
+
model=model,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def make_intent_classifier(model: str | None = None) -> LLMAgentBlock:
|
|
61
|
+
return _make_llm(
|
|
62
|
+
"intent_classifier",
|
|
63
|
+
"""You are an intent classification engine.
|
|
64
|
+
Classify the user's message into EXACTLY ONE of the five categories below.
|
|
65
|
+
Read each category carefully — they have clear, non-overlapping definitions.
|
|
66
|
+
|
|
67
|
+
- "command_hint": The user's ENTIRE message is exactly one of these CLI command words,
|
|
68
|
+
optionally followed by arguments: clear, help, exit, quit, rename, list, load, delete,
|
|
69
|
+
skills, lsskills, addskill, rmskill.
|
|
70
|
+
The message contains NOTHING else — no question, no programming request, no context.
|
|
71
|
+
Examples: "clear", "exit", "rename myproject", "addskill python".
|
|
72
|
+
|
|
73
|
+
- "greetings": The user is saying hello, goodbye, or exchanging casual pleasantries.
|
|
74
|
+
No task is being requested.
|
|
75
|
+
Examples: "hi", "thanks", "bye", "good morning".
|
|
76
|
+
|
|
77
|
+
- "question": The user is asking for an explanation, concept clarification, or information
|
|
78
|
+
about code — WITHOUT requesting that anything be written, changed, or executed.
|
|
79
|
+
Examples: "what does async mean?", "how does this function work?".
|
|
80
|
+
|
|
81
|
+
- "plan": The user wants something to be built, changed, fixed, or deleted on disk.
|
|
82
|
+
This includes: creating files, adding features, modifying code, fixing bugs, refactoring,
|
|
83
|
+
approving a pending plan ("yes", "sim", "ok", "proceed"), or describing technical
|
|
84
|
+
requirements for a project to be built.
|
|
85
|
+
Examples: "create a calculator", "fix the login bug", "add a dark mode", "sim".
|
|
86
|
+
|
|
87
|
+
- "chat": A conversational message with no programming task implied — opinions, jokes,
|
|
88
|
+
philosophical discussion, follow-up small talk.
|
|
89
|
+
Examples: "that's interesting", "I don't like Python", "what do you think about AI?".
|
|
90
|
+
|
|
91
|
+
Respond with ONLY ONE WORD from the list: command_hint, greetings, question, plan, chat.
|
|
92
|
+
No punctuation, no explanation.""",
|
|
93
|
+
model=model,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def make_complexity_evaluator(model: str | None = None) -> LLMAgentBlock:
|
|
97
|
+
return _make_llm(
|
|
98
|
+
"complexity_evaluator",
|
|
99
|
+
"""You are a complexity evaluation engine.
|
|
100
|
+
Analyze the user's request. Does it require simple file edits, quick answers, or routine commands? Or does it require complex architectural changes, deep reasoning, extensive multi-file refactoring, or advanced coding logic?
|
|
101
|
+
|
|
102
|
+
Classify the user's request complexity into EXACTLY ONE of the following categories:
|
|
103
|
+
- "default": The task is simple, straightforward, and can be handled by a standard fast model.
|
|
104
|
+
- "alternative": The task is highly complex, involves heavy refactoring, or requires an advanced reasoning model.
|
|
105
|
+
|
|
106
|
+
Respond with ONLY ONE WORD from the list above. No punctuation, no explanation.""",
|
|
107
|
+
model=model,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def make_post_plan_evaluator(model: str | None = None) -> LLMAgentBlock:
|
|
112
|
+
return _make_llm(
|
|
113
|
+
"post_plan_evaluator",
|
|
114
|
+
"""You evaluate a finalized implementation plan to calculate the expected execution effort.
|
|
115
|
+
You will receive the full text of an APPROVED PLAN.
|
|
116
|
+
|
|
117
|
+
Your task is to analyze the steps, the scope of file modifications, and the logic required.
|
|
118
|
+
Then output a JSON object with exactly two keys:
|
|
119
|
+
1. "model": Choose "default" if the steps are standard, straightforward coding tasks. Choose "alternative" if the plan involves deep refactoring, new complex algorithms, heavy debugging, or high risk.
|
|
120
|
+
2. "estimated_steps": An integer representing how many distinct actions/tools the agent might realistically need to run to finish this entire plan. E.g., reading a file is 1 step, writing is 1 step, running tests is 1 step. Be slightly pessimistic (overestimate).
|
|
121
|
+
|
|
122
|
+
Output ONLY valid JSON. No markdown formatting, no comments, no extra text.
|
|
123
|
+
Example: {"model": "default", "estimated_steps": 12}
|
|
124
|
+
""",
|
|
125
|
+
model=model,
|
|
126
|
+
response_format={"type": "json_object"}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def make_chat_memgpt_agent(model: str | None = None) -> MemGPTAgentBlock:
|
|
131
|
+
"""Create a MemGPT chat agent that maintains conversation history internally.
|
|
132
|
+
|
|
133
|
+
The returned instance should be kept alive for the duration of a project REPL loop
|
|
134
|
+
so that its internal memory persists across turns.
|
|
135
|
+
"""
|
|
136
|
+
lang_name = "English" if i18n._LANG == "en" else "Portuguese"
|
|
137
|
+
lang_rule = (
|
|
138
|
+
f"\n\nCRITICAL RULE: Respond in {lang_name}. "
|
|
139
|
+
"Keep code identifiers in English."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
system_prompt = (
|
|
143
|
+
"You are OpalaCoder's conversational assistant, embedded inside a software project.\n"
|
|
144
|
+
"You have access to the conversation history and know which project the user is working on.\n"
|
|
145
|
+
"Answer programming questions, explain concepts, and discuss code in the context of that project.\n"
|
|
146
|
+
"When relevant, refer to the project's known structure and technology stack.\n"
|
|
147
|
+
"You do NOT build or execute projects; that is handled by the autonomous orchestrator "
|
|
148
|
+
"when the user explicitly requests it.\n"
|
|
149
|
+
"Be concise, friendly, and precise."
|
|
150
|
+
+ lang_rule
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return MemGPTAgentBlock(
|
|
154
|
+
name="chat_agent",
|
|
155
|
+
system_prompt=system_prompt,
|
|
156
|
+
model=get_agent_model("chat_agent", model or DEFAULT_MODEL),
|
|
157
|
+
tools=[], # chat agent has no filesystem tools
|
|
158
|
+
litellm_kwargs=get_agent_llm_kwargs("chat_agent"),
|
|
159
|
+
max_heartbeats=get_agent_max_heartbeats("chat_agent", 10),
|
|
160
|
+
debug=get_agent_debug("chat_agent", False),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def make_confirmation_agent(model: str | None = None) -> LLMAgentBlock:
|
|
165
|
+
return _make_llm(
|
|
166
|
+
"confirmation_agent",
|
|
167
|
+
"""You will receive:
|
|
168
|
+
AGENT: <CURRENT PLAN>
|
|
169
|
+
USER_RESPONSE: <USER RESPONSE>
|
|
170
|
+
|
|
171
|
+
Your task: determine if the user APPROVED the plan or wants to MODIFY it.
|
|
172
|
+
|
|
173
|
+
Answer ONLY with a single word: "yes" or "no".
|
|
174
|
+
|
|
175
|
+
Strict rules:
|
|
176
|
+
- Answer "yes" ONLY if the user expressed clear and unconditional approval.
|
|
177
|
+
Examples of approval: "yes", "ok", "approved", "proceed", "all good", "perfect".
|
|
178
|
+
- Answer "no" if the user requested ANY change, addition, removal or fix,
|
|
179
|
+
even if politely or partially.
|
|
180
|
+
Examples of NO approval: "i want...", "add...", "remove...", "change...",
|
|
181
|
+
"only show...", "no need to...", "the app must...".
|
|
182
|
+
|
|
183
|
+
Do not explain, do not add anything else. Just: yes or no.
|
|
184
|
+
""",
|
|
185
|
+
model=model,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def make_refinement_agent(model: str | None = None) -> LLMAgentBlock:
|
|
190
|
+
return _make_llm(
|
|
191
|
+
"refinement_agent",
|
|
192
|
+
"""You refine an implementation plan based on user feedback, staying within the scope of the project.
|
|
193
|
+
|
|
194
|
+
Input:
|
|
195
|
+
ORIGINAL REQUEST: <request>
|
|
196
|
+
ORIGINAL PLAN: <plan>
|
|
197
|
+
USER FEEDBACK: <feedback>
|
|
198
|
+
|
|
199
|
+
Rules:
|
|
200
|
+
- Apply only the changes the user asked for. Do not restructure phases unrelated to the feedback.
|
|
201
|
+
- Keep the plan scoped to the active project directory. Never propose creating or moving to a new folder.
|
|
202
|
+
- Maintain the same output format as the original plan.
|
|
203
|
+
|
|
204
|
+
Output: the refined plan only. No preamble, no explanation.
|
|
205
|
+
""",
|
|
206
|
+
model=model,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def make_skill_selector(model: str | None = None) -> LLMAgentBlock:
|
|
211
|
+
return _make_llm(
|
|
212
|
+
"skill_selector",
|
|
213
|
+
"""You are a semantic router. Your role is to analyze a user request and decide which Skills (abilities/rules) are needed.
|
|
214
|
+
|
|
215
|
+
You will receive:
|
|
216
|
+
USER REQUEST: <text>
|
|
217
|
+
AVAILABLE SKILLS:
|
|
218
|
+
- skill_name: skill description
|
|
219
|
+
...
|
|
220
|
+
|
|
221
|
+
Step 1: Internally translate the user demand to English to understand exactly what is being asked.
|
|
222
|
+
Step 2: Based on the translated demand, list the exact names of the skills you deem relevant for the success of the task.
|
|
223
|
+
|
|
224
|
+
Answer ONLY with the names of the skills, separated by comma. If none are relevant, answer 'none'.
|
|
225
|
+
|
|
226
|
+
CRITICAL RULES:
|
|
227
|
+
1. If the user demand is a single word without context or meaningless (e.g. "list", "help", "hello"), you MUST include the 'opalacoder' skill.
|
|
228
|
+
2. NEVER select a framework skill (like `react_vite`) if the user requests "plain", "vanilla", or "manually" created files, or explicitly says "without react".
|
|
229
|
+
3. ONLY select skills whose description perfectly matches the requested technologies.
|
|
230
|
+
""",
|
|
231
|
+
model=model,
|
|
232
|
+
)
|
|
233
|
+
|
opalacoder/agents.yaml
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
default: ollama/ministral-3:14b
|
|
2
|
+
alternative: gemini/gemini-3-flash-preview
|
|
3
|
+
|
|
4
|
+
complexity_inference_mode: double
|
|
5
|
+
git_strategy: auto
|
|
6
|
+
|
|
7
|
+
# Global LLM defaults applied to all agents unless overridden below.
|
|
8
|
+
llm_defaults:
|
|
9
|
+
temperature: 0.7
|
|
10
|
+
max_tokens: 4096
|
|
11
|
+
num_ctx: 8192
|
|
12
|
+
|
|
13
|
+
# Per-agent overrides. Only fields listed here are overridden;
|
|
14
|
+
# everything else falls back to llm_defaults.
|
|
15
|
+
#
|
|
16
|
+
# Roles:
|
|
17
|
+
# intent_classifier — classifies user intent (greetings/question/plan/chat)
|
|
18
|
+
# complexity_evaluator — decides default vs alternative model
|
|
19
|
+
# chat_agent — conversational assistant with memory
|
|
20
|
+
# landscape_planner — high-level strategic planner
|
|
21
|
+
# refinement_agent — refines plan based on user feedback
|
|
22
|
+
# skill_selector — routes request to the appropriate skills
|
|
23
|
+
# confirmation_agent — checks if user approved the plan
|
|
24
|
+
# orchestrator — autonomous MemGPT agent that executes the approved plan
|
|
25
|
+
|
|
26
|
+
agents:
|
|
27
|
+
# think: false disables Gemma4's thinking mode on ollama so that content is
|
|
28
|
+
# returned in message.content instead of the reasoning field (ollama issue #15288).
|
|
29
|
+
# Set think: false for single-shot classifiers/selectors that gain nothing from
|
|
30
|
+
# extended reasoning. Leave it unset (defaults to true) for planning/execution
|
|
31
|
+
# agents where deeper reasoning improves output quality.
|
|
32
|
+
|
|
33
|
+
intent_classifier:
|
|
34
|
+
temperature: 0
|
|
35
|
+
max_tokens: 64
|
|
36
|
+
num_ctx: 2048
|
|
37
|
+
think: false
|
|
38
|
+
|
|
39
|
+
complexity_evaluator:
|
|
40
|
+
temperature: 0.3
|
|
41
|
+
max_tokens: 64
|
|
42
|
+
num_ctx: 2048
|
|
43
|
+
think: false
|
|
44
|
+
|
|
45
|
+
chat_agent:
|
|
46
|
+
temperature: 1.0
|
|
47
|
+
max_tokens: 2048
|
|
48
|
+
num_ctx: 8192
|
|
49
|
+
max_heartbeats: 10
|
|
50
|
+
debug: false
|
|
51
|
+
|
|
52
|
+
landscape_planner:
|
|
53
|
+
temperature: 1.0
|
|
54
|
+
num_ctx: 8192
|
|
55
|
+
|
|
56
|
+
refinement_agent:
|
|
57
|
+
temperature: 0.3
|
|
58
|
+
num_ctx: 8192
|
|
59
|
+
|
|
60
|
+
skill_selector:
|
|
61
|
+
temperature: 0.0
|
|
62
|
+
max_tokens: 64
|
|
63
|
+
num_ctx: 4096
|
|
64
|
+
think: false
|
|
65
|
+
|
|
66
|
+
confirmation_agent:
|
|
67
|
+
temperature: 0.0
|
|
68
|
+
max_tokens: 64
|
|
69
|
+
num_ctx: 2048
|
|
70
|
+
think: false
|
|
71
|
+
|
|
72
|
+
orchestrator:
|
|
73
|
+
temperature: 0.2
|
|
74
|
+
num_ctx: 16384
|
|
75
|
+
max_heartbeats: auto
|
|
76
|
+
debug: false
|
|
77
|
+
strategy: autonomous
|
|
78
|
+
|
opalacoder/api_keys.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pathlib
|
|
3
|
+
from rich.prompt import Prompt, Confirm
|
|
4
|
+
from . import terminal as T
|
|
5
|
+
from .i18n import _
|
|
6
|
+
|
|
7
|
+
PROVIDER_KEYS = {
|
|
8
|
+
"gemini": "GEMINI_API_KEY",
|
|
9
|
+
"openai": "OPENAI_API_KEY",
|
|
10
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
11
|
+
"mistral": "MISTRAL_API_KEY",
|
|
12
|
+
"groq": "GROQ_API_KEY",
|
|
13
|
+
"together_ai": "TOGETHERAI_API_KEY",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
def get_env_var_for_model(model: str) -> str:
|
|
17
|
+
if "/" in model:
|
|
18
|
+
provider = model.split("/")[0].lower()
|
|
19
|
+
return PROVIDER_KEYS.get(provider, f"{provider.upper()}_API_KEY")
|
|
20
|
+
return ""
|
|
21
|
+
|
|
22
|
+
def ensure_api_key(model: str) -> bool:
|
|
23
|
+
"""
|
|
24
|
+
Checks if the model requires an API key and if it's present.
|
|
25
|
+
If not, prompts the user to enter it and offers to save it globally.
|
|
26
|
+
Returns True if the key is available, False if the user skipped.
|
|
27
|
+
"""
|
|
28
|
+
env_var = get_env_var_for_model(model)
|
|
29
|
+
# If no specific env var is determined, assume it doesn't need one (like local ollama models)
|
|
30
|
+
if not env_var or "ollama" in model.lower() or "local" in model.lower():
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
if os.getenv(env_var):
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
T.warning(f"The model '{model}' requires the environment variable {env_var}, which was not found.")
|
|
37
|
+
|
|
38
|
+
key = Prompt.ask(f"[bold cyan]Please enter your {env_var} (leave blank to skip)[/bold cyan]", password=True)
|
|
39
|
+
|
|
40
|
+
if not key.strip():
|
|
41
|
+
T.info("No key provided. The system will fallback to the default model.")
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
key = key.strip()
|
|
45
|
+
# Set in current process
|
|
46
|
+
os.environ[env_var] = key
|
|
47
|
+
|
|
48
|
+
save = Confirm.ask("Do you want to save this key to ~/.opalacoder/.env for future sessions?", default=True)
|
|
49
|
+
if save:
|
|
50
|
+
env_path = pathlib.Path.home() / ".opalacoder" / ".env"
|
|
51
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
lines = []
|
|
54
|
+
if env_path.exists():
|
|
55
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
56
|
+
lines = f.readlines()
|
|
57
|
+
|
|
58
|
+
updated = False
|
|
59
|
+
for i, line in enumerate(lines):
|
|
60
|
+
if line.startswith(f"{env_var}="):
|
|
61
|
+
lines[i] = f"{env_var}={key}\n"
|
|
62
|
+
updated = True
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
if not updated:
|
|
66
|
+
if lines and not lines[-1].endswith("\n"):
|
|
67
|
+
lines.append("\n")
|
|
68
|
+
lines.append(f"{env_var}={key}\n")
|
|
69
|
+
|
|
70
|
+
with open(env_path, "w", encoding="utf-8") as f:
|
|
71
|
+
f.writelines(lines)
|
|
72
|
+
|
|
73
|
+
T.success(f"Key successfully saved to {env_path}")
|
|
74
|
+
|
|
75
|
+
return True
|