codepilot-00 0.0.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Prit_kanani
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: codepilot-00
3
+ Version: 0.0.0
4
+ Summary: A command-line AI assistant for generating and testing Python code.
5
+ Keywords: ai,code-generation,command-line
6
+ Author: Prit_kanani
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Software Development :: Code Generators
16
+ Requires-Dist: openai>=3.31.0
17
+ Requires-Dist: prompt-toolkit>=3.0.53
18
+ Requires-Dist: python-dotenv>=1.2.4
19
+ Requires-Dist: rich>=15.0.0
20
+ Requires-Dist: typer>=0.27.3
21
+ Requires-Python: >=3.14
22
+ Project-URL: Repository, https://github.com/kevit-prit-kanani/Agents
23
+ Project-URL: Issues, https://github.com/kevit-prit-kanani/Agents/issues
24
+ Description-Content-Type: text/markdown
25
+
26
+ # CodePilot
27
+
28
+ CodePilot is a command-line Python assistant that can answer questions and
29
+ generate, test, and repair small Python programs.
30
+
31
+ ## Project structure
32
+
33
+ - `src/codepilot/agent.py` talks to the language model.
34
+ - `src/codepilot/workflow.py` coordinates code generation, testing, and retries.
35
+ - `src/codepilot/runner.py` runs generated code and tests in a temporary folder.
36
+ - `src/codepilot/cli.py` handles terminal input, output, and saving solutions.
37
+ - `tests/` contains the project's own automated checks.
38
+
39
+ Generated test code is used only during the temporary test run. CodePilot saves
40
+ the generated implementation under `generated/`; it does not save generated
41
+ test scripts.
42
+
43
+ During interactive work, CodePilot shows each active stage and marks completed
44
+ stages before moving on to the next one.
45
+
46
+ The chat prompt suggests matching slash commands as you type. Use `/help` to
47
+ list commands, and `/exit` or `/quit` to end the chat.
48
+
49
+ ## Run
50
+
51
+ Install the package, save your OpenAI API key, and start the interactive
52
+ session:
53
+
54
+ ```console
55
+ pipx install codepilot-00
56
+ codepilot setup
57
+ codepilot chat
58
+ ```
59
+
60
+ The PyPI distribution is named `codepilot-00`; the installed terminal command
61
+ is `codepilot`.
62
+
63
+ If you prefer not to use `pipx`, install the distribution in a virtual
64
+ environment and ensure its scripts directory is on your `PATH`:
65
+
66
+ ```console
67
+ python -m pip install codepilot-00
68
+ ```
69
+
70
+ Generate code directly without the interactive session:
71
+
72
+ ```console
73
+ uv run codepilot generate "Write a function that checks whether text is a palindrome"
74
+ ```
75
+
76
+ See the [installation and publishing guide](https://github.com/kevit-prit-kanani/Agents/blob/master/SETUP.md)
77
+ for detailed instructions.
@@ -0,0 +1,52 @@
1
+ # CodePilot
2
+
3
+ CodePilot is a command-line Python assistant that can answer questions and
4
+ generate, test, and repair small Python programs.
5
+
6
+ ## Project structure
7
+
8
+ - `src/codepilot/agent.py` talks to the language model.
9
+ - `src/codepilot/workflow.py` coordinates code generation, testing, and retries.
10
+ - `src/codepilot/runner.py` runs generated code and tests in a temporary folder.
11
+ - `src/codepilot/cli.py` handles terminal input, output, and saving solutions.
12
+ - `tests/` contains the project's own automated checks.
13
+
14
+ Generated test code is used only during the temporary test run. CodePilot saves
15
+ the generated implementation under `generated/`; it does not save generated
16
+ test scripts.
17
+
18
+ During interactive work, CodePilot shows each active stage and marks completed
19
+ stages before moving on to the next one.
20
+
21
+ The chat prompt suggests matching slash commands as you type. Use `/help` to
22
+ list commands, and `/exit` or `/quit` to end the chat.
23
+
24
+ ## Run
25
+
26
+ Install the package, save your OpenAI API key, and start the interactive
27
+ session:
28
+
29
+ ```console
30
+ pipx install codepilot-00
31
+ codepilot setup
32
+ codepilot chat
33
+ ```
34
+
35
+ The PyPI distribution is named `codepilot-00`; the installed terminal command
36
+ is `codepilot`.
37
+
38
+ If you prefer not to use `pipx`, install the distribution in a virtual
39
+ environment and ensure its scripts directory is on your `PATH`:
40
+
41
+ ```console
42
+ python -m pip install codepilot-00
43
+ ```
44
+
45
+ Generate code directly without the interactive session:
46
+
47
+ ```console
48
+ uv run codepilot generate "Write a function that checks whether text is a palindrome"
49
+ ```
50
+
51
+ See the [installation and publishing guide](https://github.com/kevit-prit-kanani/Agents/blob/master/SETUP.md)
52
+ for detailed instructions.
@@ -0,0 +1,52 @@
1
+ [project]
2
+ name = "codepilot-00"
3
+ version = "0.0.0"
4
+ description = "A command-line AI assistant for generating and testing Python code."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.14"
9
+ keywords = [
10
+ "ai",
11
+ "code-generation",
12
+ "command-line",
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Topic :: Software Development :: Code Generators",
22
+ ]
23
+ dependencies = [
24
+ "openai>=3.31.0",
25
+ "prompt-toolkit>=3.0.53",
26
+ "python-dotenv>=1.2.4",
27
+ "rich>=15.0.0",
28
+ "typer>=0.27.3",
29
+ ]
30
+
31
+ [[project.authors]]
32
+ name = "Prit_kanani"
33
+
34
+ [project.scripts]
35
+ codepilot = "codepilot.cli:app"
36
+
37
+ [project.urls]
38
+ Repository = "https://github.com/kevit-prit-kanani/Agents"
39
+ Issues = "https://github.com/kevit-prit-kanani/Agents/issues"
40
+
41
+ [build-system]
42
+ requires = ["uv_build>=0.12.9,<0.13.0"]
43
+ build-backend = "uv_build"
44
+
45
+ [tool.uv]
46
+ package = true
47
+
48
+ [tool.uv.build-backend]
49
+ module-name = "codepilot"
50
+
51
+ [dependency-groups]
52
+ dev = ["pytest>=9.1.1"]
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "codepilot-00"
3
+ version = "0.0.0"
4
+ description = "A command-line AI assistant for generating and testing Python code."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Prit_kanani" }
10
+ ]
11
+ requires-python = ">=3.14"
12
+ keywords = ["ai", "code-generation", "command-line"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Topic :: Software Development :: Code Generators",
21
+ ]
22
+ dependencies = [
23
+ "openai>=3.31.0",
24
+ "prompt-toolkit>=3.0.53",
25
+ "python-dotenv>=1.2.4",
26
+ "rich>=15.0.0",
27
+ "typer>=0.27.3",
28
+ ]
29
+
30
+ [build-system]
31
+ requires = ["uv_build>=0.12.9,<0.13.0"]
32
+ build-backend = "uv_build"
33
+
34
+ [tool.uv]
35
+ package = true
36
+
37
+ [tool.uv.build-backend]
38
+ module-name = "codepilot"
39
+
40
+ [dependency-groups]
41
+ dev = [
42
+ "pytest>=9.1.1",
43
+ ]
44
+
45
+ [project.scripts]
46
+ codepilot = "codepilot.cli:app"
47
+
48
+ [project.urls]
49
+ Repository = "https://github.com/kevit-prit-kanani/Agents"
50
+ Issues = "https://github.com/kevit-prit-kanani/Agents/issues"
File without changes
@@ -0,0 +1,151 @@
1
+ import json
2
+ import os
3
+ import re
4
+
5
+ from openai import OpenAI
6
+
7
+ from codepilot.settings import load_configuration
8
+
9
+
10
+ def clean_code(text: str) -> str:
11
+ """Remove optional Markdown fences from generated code."""
12
+ text = text.strip()
13
+ text = re.sub(
14
+ r"\A```(?:python|py)?[ \t]*\r?\n?",
15
+ "",
16
+ text,
17
+ flags=re.IGNORECASE,
18
+ )
19
+ text = re.sub(r"\r?\n?```[ \t]*\Z", "", text)
20
+ return text.strip()
21
+
22
+
23
+ class CodeAgent:
24
+ """Generate Python implementations and their tests."""
25
+
26
+ def __init__(self) -> None:
27
+ load_configuration()
28
+ api_key = os.getenv("OPENAI_API_KEY")
29
+ if not api_key:
30
+ raise ValueError(
31
+ "OPENAI_API_KEY is missing. Run `codepilot setup` to save it, "
32
+ "or set the OPENAI_API_KEY environment variable."
33
+ )
34
+
35
+ self.model = os.getenv("OPENAI_MODEL", "gpt-5.5")
36
+ self.client = OpenAI(api_key=api_key)
37
+
38
+ def _request(self, instructions: str, prompt: str) -> str:
39
+ response = self.client.responses.create(
40
+ model=self.model,
41
+ instructions=instructions,
42
+ input=prompt,
43
+ )
44
+ code = clean_code(response.output_text or "")
45
+ if not code:
46
+ raise RuntimeError("The model returned empty code.")
47
+ return code
48
+
49
+ def generate(
50
+ self,
51
+ task: str,
52
+ feedback: str | None = None,
53
+ ) -> str:
54
+ """Generate or repair a Python implementation."""
55
+ instructions = """
56
+ You are a Python code generation agent.
57
+
58
+ Requirements:
59
+ - Return Python implementation code only.
60
+ - Do not include Markdown fences or explanations.
61
+ - Prefer simple, readable, maintainable code.
62
+ - Do not include tests in the implementation.
63
+ - Correct the implementation using any provided test feedback.
64
+ - Do not claim that the code passed tests unless it was tested.
65
+ """
66
+
67
+ prompt = f"Programming task:\n{task}"
68
+
69
+ if feedback:
70
+ prompt += (
71
+ "\n\nThe previous implementation failed its tests."
72
+ "\nFix the implementation using this feedback."
73
+ f"\n\nTest feedback:\n{feedback}"
74
+ )
75
+
76
+ return self._request(instructions, prompt)
77
+
78
+ def generate_tests(self, task: str, code: str) -> str:
79
+ """Generate unittest tests for the candidate implementation."""
80
+ instructions = """
81
+ You are a Python test engineer.
82
+
83
+ Requirements:
84
+ - Return Python unittest test code only.
85
+ - Do not include Markdown fences or explanations.
86
+ - Do not repeat or redefine the candidate implementation.
87
+ - The candidate implementation will appear before your test code
88
+ in the same Python file.
89
+ - Use unittest.TestCase and test methods beginning with test_.
90
+ - Test normal behavior, boundary cases, and relevant invalid inputs.
91
+ - Derive expected results from the user's task, not from assumptions
92
+ that merely make the candidate implementation pass.
93
+ - Do not modify files, access the network, or run shell commands.
94
+ """
95
+ prompt = (
96
+ f"User's programming task:\n{task}\n\n"
97
+ f"Candidate implementation:\n{code}\n\n"
98
+ "Write a unittest test suite for this implementation."
99
+ )
100
+
101
+ return self._request(instructions, prompt)
102
+
103
+ def route_message(self, message: str) -> dict[str, str]:
104
+ """Decide whether to answer conversationally or start coding."""
105
+ instructions = """
106
+ You are CodePilot's message router and conversational assistant.
107
+
108
+ Return a JSON object with exactly these keys:
109
+ - "intent": either "chat" or "code"
110
+ - "response": a string
111
+
112
+ Choose "chat" for:
113
+ - Greetings and small talk.
114
+ - Questions about CodePilot or how you can help.
115
+ - General knowledge and conceptual explanations.
116
+ - Programming questions that ask for an explanation rather than implementation.
117
+ - Follow-up questions that do not request code creation, modification,
118
+ debugging, or execution.
119
+
120
+ For chat, answer the user directly and helpfully in the response field.
121
+ Do not generate a Python implementation for ordinary conversation.
122
+
123
+ Choose "code" when the user asks to create, write, implement, modify,
124
+ debug, repair, or test a program or code artifact.
125
+
126
+ For code, make the response field a concise, faithful description of the
127
+ requested coding task. Do not generate the implementation here.
128
+ Do not treat every mention of programming as a coding task.
129
+ """
130
+
131
+ result = self.client.responses.create(
132
+ model=self.model,
133
+ instructions=instructions + "\nReturn your decision as valid JSON.",
134
+ input=f"Classify this user message and respond in JSON:\n{message}",
135
+ text={"format": {"type": "json_object"}},
136
+ )
137
+
138
+ try:
139
+ decision = json.loads(result.output_text)
140
+ except (json.JSONDecodeError, TypeError) as exc:
141
+ raise RuntimeError(
142
+ "Could not interpret the message-routing response."
143
+ ) from exc
144
+
145
+ intent = decision.get("intent")
146
+ response = decision.get("response")
147
+
148
+ if intent not in {"chat", "code"} or not isinstance(response, str):
149
+ raise RuntimeError("The model returned an invalid routing decision.")
150
+
151
+ return {"intent": intent, "response": response}
@@ -0,0 +1,273 @@
1
+ import re
2
+ from collections.abc import Generator
3
+ from contextlib import contextmanager
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from prompt_toolkit import PromptSession
8
+ from prompt_toolkit.completion import CompleteEvent, Completer, Completion
9
+ from prompt_toolkit.document import Document
10
+ from rich.console import Console
11
+ from rich.markdown import Markdown
12
+ from rich.panel import Panel
13
+ from rich.syntax import Syntax
14
+
15
+ from codepilot.agent import CodeAgent
16
+ from codepilot.settings import save_api_key
17
+ from codepilot.workflow import CodeWorkflow
18
+
19
+ app = typer.Typer(
20
+ name="CodePilot",
21
+ help="Generate, test, and repair Python code.",
22
+ no_args_is_help=True,
23
+ )
24
+
25
+ console = Console()
26
+
27
+ SLASH_COMMANDS = {
28
+ "/help": "Show available commands",
29
+ "/exit": "Exit CodePilot",
30
+ "/quit": "Exit CodePilot",
31
+ }
32
+
33
+
34
+ class SlashCommandCompleter(Completer):
35
+ """Suggest available slash commands while the user types."""
36
+
37
+ def get_completions(
38
+ self,
39
+ document: Document,
40
+ complete_event: CompleteEvent,
41
+ ) -> Generator[Completion]:
42
+ prefix = document.text_before_cursor
43
+ if not prefix.startswith("/") or any(char.isspace() for char in prefix):
44
+ return
45
+
46
+ for command, description in SLASH_COMMANDS.items():
47
+ if command.startswith(prefix.lower()):
48
+ yield Completion(
49
+ command,
50
+ start_position=-len(prefix),
51
+ display_meta=description,
52
+ )
53
+
54
+
55
+ def save_code(task: str, code: str) -> Path:
56
+ """Save the generated implementation."""
57
+ output_dir = Path("generated")
58
+ output_dir.mkdir(parents=True, exist_ok=True)
59
+
60
+ slug = re.sub(r"[^a-zA-Z0-9_-]+", "_", task.lower()).strip("_")
61
+ slug = slug[:40] or "solution"
62
+
63
+ code_path = output_dir / f"{slug}.py"
64
+ code_path.write_text(code.rstrip() + "\n", encoding="utf-8")
65
+ return code_path
66
+
67
+
68
+ @contextmanager
69
+ def show_progress_step(label: str) -> Generator[None]:
70
+ """Show one active step, then keep its completion visible."""
71
+ with console.status(f"[cyan]{label}...", spinner="dots"):
72
+ yield
73
+ console.print(f"[bold green]✓ {label}[/bold green]")
74
+
75
+
76
+ def run_coding_task(
77
+ agent: CodeAgent,
78
+ task: str,
79
+ max_attempts: int,
80
+ ) -> None:
81
+ """Generate, test, and display one coding task."""
82
+ workflow = CodeWorkflow(
83
+ agent=agent,
84
+ max_attempts=max_attempts,
85
+ show_progress=show_progress_step,
86
+ )
87
+ result = workflow.run(task=task)
88
+
89
+ console.print(
90
+ Panel(
91
+ Syntax(
92
+ result.code,
93
+ "python",
94
+ theme="monokai",
95
+ line_numbers=True,
96
+ ),
97
+ title="Final Python Implementation",
98
+ border_style="green" if result.passed else "red",
99
+ )
100
+ )
101
+
102
+ code_path = save_code(task, result.code)
103
+
104
+ if result.passed:
105
+ console.print(
106
+ f"[bold green]Tests passed[/bold green] "
107
+ f"on attempt {result.attempts}/{max_attempts}."
108
+ )
109
+ else:
110
+ console.print(
111
+ f"[bold red]Tests did not pass[/bold red] "
112
+ f"after {result.attempts} attempt(s)."
113
+ )
114
+ console.print(
115
+ Panel(
116
+ result.test_result.feedback,
117
+ title="Final Test Feedback",
118
+ border_style="red",
119
+ )
120
+ )
121
+
122
+ console.print(f"Implementation saved to: [cyan]{code_path}[/cyan]")
123
+
124
+
125
+ def handle_message(
126
+ agent: CodeAgent,
127
+ message: str,
128
+ max_attempts: int,
129
+ ) -> None:
130
+ """Route a message to a direct answer or the coding workflow."""
131
+ with show_progress_step("Understanding your request"):
132
+ decision = agent.route_message(message)
133
+
134
+ if decision["intent"] == "chat":
135
+ console.print(
136
+ Panel(
137
+ Markdown(decision["response"]),
138
+ title="[bold cyan]CodePilot[/bold cyan]",
139
+ border_style="cyan",
140
+ )
141
+ )
142
+ return
143
+
144
+ run_coding_task(agent, decision["response"], max_attempts)
145
+
146
+
147
+ def handle_slash_command(message: str) -> bool | None:
148
+ """Run a slash command; return whether the chat should exit."""
149
+ command = message.strip().lower()
150
+
151
+ if command in {"/exit", "/quit"}:
152
+ return True
153
+ if command == "/help":
154
+ console.print(
155
+ Panel(
156
+ "\n".join(
157
+ f"[cyan]{name}[/cyan] — {description}"
158
+ for name, description in SLASH_COMMANDS.items()
159
+ ),
160
+ title="Available commands",
161
+ )
162
+ )
163
+ return False
164
+ if command.startswith("/"):
165
+ console.print(
166
+ f"[yellow]Unknown command:[/yellow] {command}. "
167
+ "Type /help to see available commands."
168
+ )
169
+ return False
170
+ return None
171
+
172
+
173
+ @app.command()
174
+ def setup() -> None:
175
+ """Save or replace the OpenAI API key for this user."""
176
+ api_key = typer.prompt(
177
+ "OpenAI API key",
178
+ hide_input=True,
179
+ confirmation_prompt=True,
180
+ )
181
+ try:
182
+ config_file = save_api_key(api_key)
183
+ except (OSError, ValueError) as exc:
184
+ console.print(f"[bold red]Could not save API key:[/bold red] {exc}")
185
+ raise typer.Exit(code=1) from exc
186
+
187
+ console.print(
188
+ "[bold green]API key saved.[/bold green] "
189
+ f"Configuration file: [cyan]{config_file}[/cyan]"
190
+ )
191
+
192
+
193
+ @app.command()
194
+ def generate(
195
+ task: str = typer.Argument(..., help="Describe the Python program."),
196
+ ) -> None:
197
+ """Generate code without testing it."""
198
+ try:
199
+ agent = CodeAgent()
200
+ with console.status("[cyan]Generating code...", spinner="dots"):
201
+ code = agent.generate(task=task)
202
+
203
+ console.print(
204
+ Panel(
205
+ Syntax(code, "python", theme="monokai", line_numbers=True),
206
+ title="Generated Python Code",
207
+ border_style="green",
208
+ )
209
+ )
210
+ except Exception as exc:
211
+ console.print(f"[bold red]Error:[/bold red] {exc}")
212
+ raise typer.Exit(code=1) from exc
213
+
214
+
215
+ @app.command()
216
+ def chat(
217
+ max_attempts: int = typer.Option(
218
+ 3,
219
+ "--max-attempts",
220
+ min=1,
221
+ help="Maximum implementation attempts.",
222
+ ),
223
+ ) -> None:
224
+ """Start an interactive conversational coding session."""
225
+ console.print(
226
+ Panel(
227
+ "Ask questions or give me a coding task.\n"
228
+ "Coding tasks are generated, tested, and repaired automatically.\n"
229
+ "Type /help for commands. Use /exit or /quit to stop.",
230
+ title="[bold cyan]CodePilot[/bold cyan]",
231
+ )
232
+ )
233
+
234
+ agent = None
235
+ prompt_session = PromptSession(
236
+ completer=SlashCommandCompleter(),
237
+ complete_while_typing=True,
238
+ )
239
+
240
+ while True:
241
+ try:
242
+ message = prompt_session.prompt("you > ").strip()
243
+ except KeyboardInterrupt:
244
+ console.print(
245
+ "\n[yellow]Input cancelled. Use /exit or /quit to end the chat.[/yellow]"
246
+ )
247
+ continue
248
+ except EOFError:
249
+ console.print("\n[yellow]Input stream closed.[/yellow]")
250
+ break
251
+
252
+ if not message:
253
+ continue
254
+
255
+ should_exit = handle_slash_command(message)
256
+ if should_exit is True:
257
+ console.print("[yellow]Goodbye.[/yellow]")
258
+ break
259
+ if should_exit is False:
260
+ continue
261
+
262
+ try:
263
+ if agent is None:
264
+ agent = CodeAgent()
265
+
266
+ handle_message(agent, message, max_attempts)
267
+
268
+ except Exception as exc: # noqa: BLE001
269
+ console.print(f"[bold red]Error:[/bold red] {exc}")
270
+
271
+
272
+ if __name__ == "__main__":
273
+ app()
@@ -0,0 +1,97 @@
1
+ import subprocess
2
+ import sys
3
+ import tempfile
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+
8
+ @dataclass
9
+ class TestResult:
10
+ """Structured result returned by the test runner."""
11
+
12
+ __test__ = False
13
+
14
+ passed: bool
15
+ return_code: int | None
16
+ stdout: str
17
+ stderr: str
18
+ timed_out: bool = False
19
+
20
+ @property
21
+ def feedback(self) -> str:
22
+ """Format test output for display or LLM repair feedback."""
23
+ if self.passed:
24
+ return "All tests passed!"
25
+
26
+ if self.timed_out:
27
+ return "Execution timed out"
28
+
29
+ return (
30
+ f"Exit code: {self.return_code}\n"
31
+ f"STDOUT:\n{self.stdout}\n"
32
+ f"STDERR:\n{self.stderr}"
33
+ )
34
+
35
+
36
+ class TestRunner:
37
+ """Execute candidate code and its generated tests in a child process."""
38
+
39
+ __test__ = False
40
+
41
+ def __init__(self, timeout: int = 5) -> None:
42
+ if timeout <= 0:
43
+ raise ValueError("timeout must be greater than zero")
44
+
45
+ self.timeout = timeout
46
+
47
+ def run(self, code: str, test_code: str) -> TestResult:
48
+ """
49
+ Run candidate code and its generated tests in a temporary child process.
50
+
51
+ The temporary test script is removed automatically after execution.
52
+ """
53
+
54
+ with tempfile.TemporaryDirectory(prefix="codepilot-") as tmp:
55
+ script_path = Path(tmp) / "candidate_test.py"
56
+ script_path.write_text(
57
+ code
58
+ + "\n\n"
59
+ + "import unittest\n"
60
+ + test_code
61
+ + "\n\n"
62
+ + 'if __name__ == "__main__":\n'
63
+ + "\tunittest.main(verbosity=2)\n",
64
+ encoding="utf-8",
65
+ )
66
+ try:
67
+ completed = subprocess.run(
68
+ [sys.executable, "-I", str(script_path)],
69
+ capture_output=True,
70
+ text=True,
71
+ timeout=self.timeout,
72
+ check=False,
73
+ cwd=tmp,
74
+ )
75
+ return TestResult(
76
+ passed=completed.returncode == 0,
77
+ return_code=completed.returncode,
78
+ stdout=completed.stdout,
79
+ stderr=completed.stderr,
80
+ )
81
+ except subprocess.TimeoutExpired as exc:
82
+ stdout = exc.stdout or ""
83
+ stderr = exc.stderr or ""
84
+
85
+ if isinstance(stdout, bytes):
86
+ stdout = stdout.decode("utf-8", errors="replace")
87
+
88
+ if isinstance(stderr, bytes):
89
+ stderr = stderr.decode("utf-8", errors="replace")
90
+
91
+ return TestResult(
92
+ passed=False,
93
+ return_code=None,
94
+ stdout=stdout,
95
+ stderr=stderr,
96
+ timed_out=True,
97
+ )
@@ -0,0 +1,38 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from dotenv import load_dotenv, set_key
5
+
6
+
7
+ def user_config_file() -> Path:
8
+ """Return the per-user .env file used by installed CodePilot commands."""
9
+ config_root = (
10
+ os.getenv("XDG_CONFIG_HOME")
11
+ or os.getenv("APPDATA")
12
+ or str(Path.home() / ".config")
13
+ )
14
+ return Path(config_root) / "codepilot" / ".env"
15
+
16
+
17
+ def load_configuration() -> None:
18
+ """Load user settings first, then fall back to a project-local .env file."""
19
+ load_dotenv(user_config_file())
20
+ load_dotenv(Path.cwd() / ".env")
21
+
22
+
23
+ def save_api_key(api_key: str) -> Path:
24
+ """Save the API key to the private per-user configuration file."""
25
+ api_key = api_key.strip()
26
+ if not api_key:
27
+ raise ValueError("The API key cannot be empty.")
28
+
29
+ config_file = user_config_file()
30
+ config_file.parent.mkdir(parents=True, exist_ok=True)
31
+ set_key(
32
+ str(config_file),
33
+ "OPENAI_API_KEY",
34
+ api_key,
35
+ quote_mode="always",
36
+ )
37
+ config_file.chmod(0o600)
38
+ return config_file
@@ -0,0 +1,84 @@
1
+
2
+ from collections.abc import Callable
3
+ from contextlib import AbstractContextManager, nullcontext
4
+ from dataclasses import dataclass
5
+
6
+ from codepilot.agent import CodeAgent
7
+ from codepilot.runner import TestResult, TestRunner
8
+
9
+
10
+ @dataclass
11
+ class WorkflowResult:
12
+ """Outcome of a generate-test-repair run."""
13
+
14
+ code: str
15
+ test_code: str
16
+ attempts: int
17
+ test_result: TestResult
18
+
19
+ @property
20
+ def passed(self) -> bool:
21
+ return self.test_result.passed
22
+
23
+
24
+ class CodeWorkflow:
25
+ """Generate code and tests, execute them, and retry on failure."""
26
+
27
+ def __init__(
28
+ self,
29
+ agent: CodeAgent | None = None,
30
+ runner: TestRunner | None = None,
31
+ max_attempts: int = 3,
32
+ show_progress: Callable[[str], AbstractContextManager[None]] | None = None,
33
+ ) -> None:
34
+ if max_attempts < 1:
35
+ raise ValueError("max_attempts must be at least 1")
36
+
37
+ self.agent = agent if agent is not None else CodeAgent()
38
+ self.runner = runner if runner is not None else TestRunner()
39
+ self.max_attempts = max_attempts
40
+ self.show_progress = show_progress
41
+
42
+ def run(self, task: str) -> WorkflowResult:
43
+ """Generate implementation and tests, then retry on failures."""
44
+ with self._progress_step("Writing the first implementation"):
45
+ code = self.agent.generate(task=task)
46
+
47
+ with self._progress_step("Preparing checks"):
48
+ test_code = self.agent.generate_tests(task=task, code=code)
49
+
50
+ for attempt in range(1, self.max_attempts + 1):
51
+ with self._progress_step(
52
+ f"Running checks (attempt {attempt}/{self.max_attempts})"
53
+ ):
54
+ result = self.runner.run(code=code, test_code=test_code)
55
+
56
+ if result.passed:
57
+ return WorkflowResult(
58
+ code=code,
59
+ test_code=test_code,
60
+ attempts=attempt,
61
+ test_result=result,
62
+ )
63
+
64
+ if attempt < self.max_attempts:
65
+ with self._progress_step(
66
+ f"Improving the implementation (attempt "
67
+ f"{attempt + 1}/{self.max_attempts})"
68
+ ):
69
+ code = self.agent.generate(
70
+ task=task,
71
+ feedback=result.feedback,
72
+ )
73
+
74
+ return WorkflowResult(
75
+ code=code,
76
+ test_code=test_code,
77
+ attempts=self.max_attempts,
78
+ test_result=result,
79
+ )
80
+
81
+ def _progress_step(self, label: str) -> AbstractContextManager[None]:
82
+ if self.show_progress is None:
83
+ return nullcontext()
84
+ return self.show_progress(label)