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 ADDED
@@ -0,0 +1,2 @@
1
+ """OpalaCoder – AI coding agent with session management and modular execution."""
2
+ __version__ = "0.1.0"
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