mycode-aiagent 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.
my_code/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from .analyzer import StyleAnalyzer
4
+ from .generator import generate_code
5
+ from .backends import AIBackend, ClaudeBackend, OpenAIBackend, RickyBackend, MCPBackend, make_backend
6
+
7
+ __all__ = [
8
+ "StyleAnalyzer",
9
+ "generate_code",
10
+ "AIBackend",
11
+ "ClaudeBackend",
12
+ "OpenAIBackend",
13
+ "RickyBackend",
14
+ "MCPBackend",
15
+ "make_backend",
16
+ ]
my_code/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
my_code/analyzer.py ADDED
@@ -0,0 +1,84 @@
1
+ import json
2
+ import re
3
+ from pathlib import Path
4
+
5
+ from .backends import AIBackend
6
+ from .utils.prompts import STYLE_EXTRACTION_PROMPT, STYLE_SUMMARY_PROMPT
7
+
8
+ _SKIP_DIRS = {"__pycache__", ".git", ".venv", "venv", "node_modules", "dist", "build"}
9
+
10
+
11
+ def _collect_python_files(root: Path) -> list[Path]:
12
+ files = []
13
+ for path in root.rglob("*.py"):
14
+ if not any(part in _SKIP_DIRS for part in path.parts):
15
+ files.append(path)
16
+ return sorted(files)
17
+
18
+
19
+ def _extract_json(text: str) -> dict:
20
+ # Claude/OpenAI return clean JSON; llama wraps it in prose
21
+ try:
22
+ return json.loads(text.strip())
23
+ except json.JSONDecodeError:
24
+ pass
25
+ match = re.search(r"\{.*\}", text, re.DOTALL)
26
+ if not match:
27
+ raise ValueError(f"No JSON found in response:\n{text[:300]}")
28
+ return json.loads(match.group())
29
+
30
+
31
+ class StyleAnalyzer:
32
+ def __init__(self, backend: AIBackend):
33
+ self.backend = backend
34
+
35
+ def analyze_file(self, path: Path) -> dict:
36
+ source = path.read_text(encoding="utf-8", errors="ignore")[:self.backend.max_file_chars]
37
+ if not source.strip():
38
+ return {}
39
+ prompt = STYLE_EXTRACTION_PROMPT.format(filename=path.name, source=source)
40
+ raw = self.backend.ask_to_analyze(prompt)
41
+ try:
42
+ return _extract_json(raw)
43
+ except (ValueError, json.JSONDecodeError) as e:
44
+ print(f" [warn] Could not parse style from {path.name}: {e}")
45
+ return {}
46
+
47
+ def analyze_codebase(self, root: Path, verbose: bool = False) -> dict:
48
+ files = _collect_python_files(root)
49
+ if not files:
50
+ raise FileNotFoundError(f"No Python files found under {root}")
51
+
52
+ observations = []
53
+ for path in files:
54
+ rel = path.relative_to(root)
55
+ if verbose:
56
+ print(f" Analyzing {rel} ...")
57
+ obs = self.analyze_file(path)
58
+ if obs:
59
+ observations.append(obs)
60
+
61
+ if not observations:
62
+ raise RuntimeError("No style data could be extracted from any file.")
63
+
64
+ if len(observations) == 1:
65
+ return observations[0]
66
+
67
+ prompt = STYLE_SUMMARY_PROMPT.format(
68
+ observations=json.dumps(observations, indent=2)
69
+ )
70
+ raw = self.backend.ask_to_analyze(prompt)
71
+ try:
72
+ return _extract_json(raw)
73
+ except (ValueError, json.JSONDecodeError) as e:
74
+ print(f"[warn] Could not synthesize profile, using first observation: {e}")
75
+ return observations[0]
76
+
77
+ @staticmethod
78
+ def save_profile(profile: dict, path: Path):
79
+ path.write_text(json.dumps(profile, indent=2), encoding="utf-8")
80
+ print(f"Style profile saved to {path}")
81
+
82
+ @staticmethod
83
+ def load_profile(path: Path) -> dict:
84
+ return json.loads(path.read_text(encoding="utf-8"))
@@ -0,0 +1,33 @@
1
+ import os
2
+
3
+ from .base import AIBackend
4
+ from .claude_backend import ClaudeBackend
5
+ from .openai_backend import OpenAIBackend
6
+ from .ricky_backend import RickyBackend
7
+ from .mcp_backend import MCPBackend
8
+
9
+ __all__ = ["AIBackend", "ClaudeBackend", "OpenAIBackend", "RickyBackend", "MCPBackend", "make_backend"]
10
+
11
+
12
+ def make_backend(
13
+ backend: str = "llama",
14
+ api_key: str | None = None,
15
+ ricky_url: str = "http://localhost:8000/mcp",
16
+ mcp_url: str = "http://localhost:8001/mcp",
17
+ model: str | None = None,
18
+ ) -> AIBackend:
19
+ if backend == "claude":
20
+ key = api_key or os.environ.get("ANTHROPIC_API_KEY")
21
+ if not key:
22
+ raise ValueError("Claude backend requires ANTHROPIC_API_KEY or --api-key")
23
+ return ClaudeBackend(api_key=key, **{"model": model} if model else {})
24
+ if backend == "openai":
25
+ key = api_key or os.environ.get("OPENAI_API_KEY")
26
+ if not key:
27
+ raise ValueError("OpenAI backend requires OPENAI_API_KEY or --api-key")
28
+ return OpenAIBackend(api_key=key, **{"model": model} if model else {})
29
+ if backend == "mcp":
30
+ return MCPBackend(url=mcp_url)
31
+ if backend == "llama":
32
+ return RickyBackend(url=ricky_url)
33
+ raise ValueError(f"Unknown backend {backend!r}. Choose: llama, claude, openai, mcp")
@@ -0,0 +1,13 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class AIBackend(ABC):
5
+ max_file_chars: int = 6000
6
+
7
+ @abstractmethod
8
+ def ask_for_code(self, prompt: str) -> str:
9
+ pass
10
+
11
+ @abstractmethod
12
+ def ask_to_analyze(self, prompt: str) -> str:
13
+ pass
@@ -0,0 +1,32 @@
1
+ from .base import AIBackend
2
+
3
+
4
+ class ClaudeBackend(AIBackend):
5
+ def __init__(self, api_key: str, model: str = "claude-opus-4-7"):
6
+ try:
7
+ import anthropic
8
+ except ImportError:
9
+ raise ImportError("Claude backend requires 'anthropic': pip install 'my-code[claude]'")
10
+ self._client = anthropic.Anthropic(api_key=api_key)
11
+ self._model = model
12
+
13
+ def _call(self, system: str, prompt: str) -> str:
14
+ response = self._client.messages.create(
15
+ model=self._model,
16
+ max_tokens=2048,
17
+ system=system,
18
+ messages=[{"role": "user", "content": prompt}],
19
+ )
20
+ return response.content[0].text
21
+
22
+ def ask_for_code(self, prompt: str) -> str:
23
+ return self._call(
24
+ "You are an expert Python developer. Return only Python code, no explanation.",
25
+ prompt,
26
+ )
27
+
28
+ def ask_to_analyze(self, prompt: str) -> str:
29
+ return self._call(
30
+ "You are a code style analyst. Return only valid JSON, no explanation.",
31
+ prompt,
32
+ )
@@ -0,0 +1,14 @@
1
+ from .base import AIBackend
2
+
3
+ _MSG = "MCPBackend is not yet configured. Set the MCP server URL and implement this backend."
4
+
5
+
6
+ class MCPBackend(AIBackend):
7
+ def __init__(self, url: str = "http://localhost:8001/mcp"):
8
+ self.url = url
9
+
10
+ def ask_for_code(self, prompt: str) -> str:
11
+ raise NotImplementedError(_MSG)
12
+
13
+ def ask_to_analyze(self, prompt: str) -> str:
14
+ raise NotImplementedError(_MSG)
@@ -0,0 +1,33 @@
1
+ from .base import AIBackend
2
+
3
+
4
+ class OpenAIBackend(AIBackend):
5
+ def __init__(self, api_key: str, model: str = "gpt-4o"):
6
+ try:
7
+ import openai
8
+ except ImportError:
9
+ raise ImportError("OpenAI backend requires 'openai': pip install 'my-code[openai]'")
10
+ self._client = openai.OpenAI(api_key=api_key)
11
+ self._model = model
12
+
13
+ def _call(self, system: str, prompt: str) -> str:
14
+ response = self._client.chat.completions.create(
15
+ model=self._model,
16
+ messages=[
17
+ {"role": "system", "content": system},
18
+ {"role": "user", "content": prompt},
19
+ ],
20
+ )
21
+ return response.choices[0].message.content
22
+
23
+ def ask_for_code(self, prompt: str) -> str:
24
+ return self._call(
25
+ "You are an expert Python developer. Return only Python code, no explanation.",
26
+ prompt,
27
+ )
28
+
29
+ def ask_to_analyze(self, prompt: str) -> str:
30
+ return self._call(
31
+ "You are a code style analyst. Return only valid JSON, no explanation.",
32
+ prompt,
33
+ )
@@ -0,0 +1,15 @@
1
+ from .base import AIBackend
2
+ from ..ricky_client import RickyClient
3
+
4
+
5
+ class RickyBackend(AIBackend):
6
+ max_file_chars: int = 1500
7
+
8
+ def __init__(self, url: str = "http://localhost:8000/mcp"):
9
+ self._client = RickyClient(url=url)
10
+
11
+ def ask_for_code(self, prompt: str) -> str:
12
+ return self._client.ask_for_code(prompt)
13
+
14
+ def ask_to_analyze(self, prompt: str) -> str:
15
+ return self._client.ask_to_analyze(prompt)
my_code/cli.py ADDED
@@ -0,0 +1,93 @@
1
+ """
2
+ Style-aware code generation agent.
3
+
4
+ Usage:
5
+ my-code [--backend llama|claude|openai] analyze <dir>
6
+ my-code [--backend llama|claude|openai] --api-key <key> generate "<task>"
7
+ """
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ from .analyzer import StyleAnalyzer
15
+ from .backends import make_backend
16
+ from .generator import generate_code
17
+
18
+ DEFAULT_PROFILE = Path("style_profile.json")
19
+
20
+
21
+ def cmd_analyze(args):
22
+ root = Path(args.codebase).resolve()
23
+ if not root.exists():
24
+ sys.exit(f"Error: directory not found: {root}")
25
+
26
+ backend = make_backend(
27
+ backend=args.backend,
28
+ api_key=args.api_key,
29
+ ricky_url=args.ricky_url,
30
+ mcp_url=args.mcp_url,
31
+ model=args.model,
32
+ )
33
+
34
+ print(f"Analyzing codebase at {root} ...")
35
+ analyzer = StyleAnalyzer(backend)
36
+ profile = analyzer.analyze_codebase(root, verbose=args.verbose)
37
+
38
+ out = Path(args.profile)
39
+ StyleAnalyzer.save_profile(profile, out)
40
+
41
+
42
+ def cmd_generate(args):
43
+ profile_path = Path(args.profile)
44
+ if not profile_path.exists():
45
+ sys.exit(
46
+ f"Error: style profile not found at {profile_path}. "
47
+ "Run 'analyze' first."
48
+ )
49
+
50
+ backend = make_backend(
51
+ backend=args.backend,
52
+ api_key=args.api_key,
53
+ ricky_url=args.ricky_url,
54
+ mcp_url=args.mcp_url,
55
+ model=args.model,
56
+ )
57
+
58
+ profile = json.loads(profile_path.read_text(encoding="utf-8"))
59
+
60
+ print("Generating code ...\n")
61
+ code = generate_code(task=args.task, backend=backend, profile=profile)
62
+ print(code)
63
+
64
+
65
+ def main():
66
+ parser = argparse.ArgumentParser(description="Style-aware code agent")
67
+ parser.add_argument(
68
+ "--backend", default="llama", choices=["llama", "claude", "openai", "mcp"],
69
+ help="AI backend to use (default: llama)",
70
+ )
71
+ parser.add_argument("--api-key", default=None, help="API key (claude/openai); falls back to env var")
72
+ parser.add_argument("--model", default=None, help="Override default model for claude/openai backends")
73
+ parser.add_argument("--ricky-url", default="http://localhost:8000/mcp", help="Ricky MCP server URL (llama backend)")
74
+ parser.add_argument("--mcp-url", default="http://localhost:8001/mcp", help="MCP server URL (mcp backend)")
75
+ parser.add_argument("--profile", default=str(DEFAULT_PROFILE))
76
+
77
+ sub = parser.add_subparsers(dest="command", required=True)
78
+
79
+ p_analyze = sub.add_parser("analyze", help="Analyze a codebase and save a style profile")
80
+ p_analyze.add_argument("codebase", help="Path to the codebase directory")
81
+ p_analyze.add_argument("-v", "--verbose", action="store_true")
82
+ p_analyze.set_defaults(func=cmd_analyze)
83
+
84
+ p_gen = sub.add_parser("generate", help="Generate code matching the saved style profile")
85
+ p_gen.add_argument("task", help="Description of what to write")
86
+ p_gen.set_defaults(func=cmd_generate)
87
+
88
+ args = parser.parse_args()
89
+ args.func(args)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
my_code/generator.py ADDED
@@ -0,0 +1,13 @@
1
+ import json
2
+
3
+ from .backends.base import AIBackend
4
+ from .utils.prompts import CODE_GENERATION_PROMPT
5
+
6
+
7
+ def generate_code(task: str, backend: AIBackend, profile: dict) -> str:
8
+ """Generate Python code matching the given style profile."""
9
+ prompt = CODE_GENERATION_PROMPT.format(
10
+ style_profile=json.dumps(profile, indent=2),
11
+ task=task,
12
+ )
13
+ return backend.ask_for_code(prompt)
@@ -0,0 +1,112 @@
1
+ import json
2
+ import uuid
3
+
4
+ import requests
5
+
6
+ # Ricky's own instructions: give it explicit, complete prompts.
7
+ _PROTOCOL = "2024-11-05"
8
+
9
+
10
+ def _parse_sse_result(text: str) -> dict:
11
+ """Extract the first JSON-RPC result/error from an SSE response body."""
12
+ for line in text.splitlines():
13
+ if line.startswith("data:"):
14
+ payload = line[5:].strip()
15
+ if payload and payload != "[DONE]":
16
+ return json.loads(payload)
17
+ raise ValueError(f"No data event in SSE body:\n{text[:400]}")
18
+
19
+
20
+ class RickyClient:
21
+ """MCP client for ricky using the Streamable HTTP transport.
22
+
23
+ Flow:
24
+ 1. POST /mcp initialize → SSE body + Mcp-Session-Id header
25
+ 2. POST /mcp <any call> → SSE body (Mcp-Session-Id in every request)
26
+ """
27
+
28
+ def __init__(self, url: str = "http://localhost:8000/mcp"):
29
+ self.url = url
30
+ self._session_id: str | None = None
31
+ self._tool_name: str | None = None
32
+ self._tool_name_analyze: str | None = None
33
+ self._initialize()
34
+ self._discover_tools()
35
+ print(f"Connected to ricky — tools: '{self._tool_name}', '{self._tool_name_analyze}'")
36
+
37
+ def _post(self, method: str, params: dict, timeout: int = 120) -> dict:
38
+ headers = {
39
+ "Content-Type": "application/json",
40
+ "Accept": "application/json, text/event-stream",
41
+ }
42
+ if self._session_id:
43
+ headers["Mcp-Session-Id"] = self._session_id
44
+
45
+ payload = {
46
+ "jsonrpc": "2.0",
47
+ "id": str(uuid.uuid4()),
48
+ "method": method,
49
+ "params": params,
50
+ }
51
+ resp = requests.post(self.url, json=payload, headers=headers, timeout=timeout)
52
+ resp.raise_for_status()
53
+
54
+ if "Mcp-Session-Id" in resp.headers:
55
+ self._session_id = resp.headers["Mcp-Session-Id"]
56
+
57
+ msg = _parse_sse_result(resp.text)
58
+ if "error" in msg:
59
+ raise RuntimeError(f"MCP error ({method}): {msg['error']}")
60
+ return msg.get("result", {})
61
+
62
+ def _initialize(self):
63
+ self._post("initialize", {
64
+ "protocolVersion": _PROTOCOL,
65
+ "capabilities": {},
66
+ "clientInfo": {"name": "style-agent", "version": "1.0"},
67
+ })
68
+
69
+ def _discover_tools(self):
70
+ tools = self._post("tools/list", {}).get("tools", [])
71
+ if not tools:
72
+ raise RuntimeError("Ricky exposes no tools")
73
+ for t in tools:
74
+ name = t["name"]
75
+ if "analyze" in name and self._tool_name_analyze is None:
76
+ self._tool_name_analyze = name
77
+ elif self._tool_name is None:
78
+ self._tool_name = name
79
+ if not self._tool_name:
80
+ raise RuntimeError("Ricky exposes no coding tool")
81
+ if not self._tool_name_analyze:
82
+ raise RuntimeError("Ricky exposes no analyze tool")
83
+
84
+ def _parse_content(self, result: dict) -> str:
85
+ content = result.get("content", [])
86
+ if isinstance(content, list):
87
+ for block in content:
88
+ if block.get("type") == "text":
89
+ text = block.get("text", "")
90
+ # Ricky wraps llama.cpp output: {"result": "<completion JSON>"}
91
+ try:
92
+ outer = json.loads(text)
93
+ inner_str = outer.get("result", text)
94
+ inner = json.loads(inner_str)
95
+ return inner["choices"][0]["text"]
96
+ except (json.JSONDecodeError, KeyError, TypeError):
97
+ return text
98
+ return str(content)
99
+
100
+ def ask_for_code(self, prompt: str) -> str:
101
+ result = self._post("tools/call", {
102
+ "name": self._tool_name,
103
+ "arguments": {"query": prompt},
104
+ })
105
+ return self._parse_content(result)
106
+
107
+ def ask_to_analyze(self, prompt: str) -> str:
108
+ result = self._post("tools/call", {
109
+ "name": self._tool_name_analyze,
110
+ "arguments": {"query": prompt},
111
+ }, timeout=600)
112
+ return self._parse_content(result)
File without changes
@@ -0,0 +1,58 @@
1
+ STYLE_EXTRACTION_PROMPT = """\
2
+ You are a code style analyst. Analyze the following Python source file and return a JSON object describing its style. Be precise and concise.
3
+
4
+ Return ONLY valid JSON with this exact schema:
5
+ {{
6
+ "naming": {{
7
+ "functions": "<snake_case|camelCase|PascalCase|other>",
8
+ "classes": "<snake_case|camelCase|PascalCase|other>",
9
+ "variables": "<snake_case|camelCase|PascalCase|other>",
10
+ "constants": "<UPPER_SNAKE_CASE|other>",
11
+ "notes": "<any notable patterns, e.g. verb_noun for functions, single-letter loop vars, etc.>"
12
+ }},
13
+ "structure": {{
14
+ "import_style": "<grouped|flat|alphabetical|none>",
15
+ "class_method_order": "<init_first|alphabetical|public_then_private|none>",
16
+ "preferred_length": "<short|medium|long>",
17
+ "module_layout": "<description of top-level ordering>"
18
+ }},
19
+ "comments": {{
20
+ "docstring_style": "<Google|NumPy|reStructuredText|plain|none>",
21
+ "inline_density": "<sparse|moderate|heavy|none>",
22
+ "docstring_sections": [<list of sections used, e.g. "Args", "Returns", "Raises", "Example">]
23
+ }},
24
+ "representative_snippets": [<1-3 short verbatim code snippets that best show the style>]
25
+ }}
26
+
27
+ Source file ({filename}):
28
+ ```python
29
+ {source}
30
+ ```
31
+ """
32
+
33
+ STYLE_SUMMARY_PROMPT = """\
34
+ You are a code style analyst. Below are JSON style observations extracted from multiple files in a codebase. Synthesize them into a single authoritative style profile JSON.
35
+
36
+ Use the same schema. For fields where files disagree, pick the majority or most consistent value. Add a "confidence" field (low/medium/high) per section. Keep representative_snippets to the 3 best examples across all files.
37
+
38
+ Return ONLY valid JSON.
39
+
40
+ Observations:
41
+ {observations}
42
+ """
43
+
44
+ CODE_GENERATION_PROMPT = """\
45
+ You are an expert Python developer. Write code that matches the style profile below exactly.
46
+
47
+ Style profile:
48
+ {style_profile}
49
+
50
+ Task:
51
+ {task}
52
+
53
+ Rules:
54
+ - Match naming conventions, docstring style, and comment density from the profile exactly.
55
+ - Follow the module and class structure patterns.
56
+ - Use the representative snippets as a style reference.
57
+ - Return ONLY the Python code, no explanation.
58
+ """
@@ -0,0 +1,341 @@
1
+ Metadata-Version: 2.4
2
+ Name: mycode-aiagent
3
+ Version: 0.1.0
4
+ Summary: Style-aware code generation — analyze any codebase and generate new code that matches its style
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/RyanAbbottData/MyCode
7
+ Project-URL: Repository, https://github.com/RyanAbbottData/MyCode
8
+ Project-URL: Issues, https://github.com/RyanAbbottData/MyCode/issues
9
+ Keywords: code generation,style analysis,llm,ai,developer tools
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Code Generators
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: requests
22
+ Provides-Extra: claude
23
+ Requires-Dist: anthropic>=0.40; extra == "claude"
24
+ Provides-Extra: openai
25
+ Requires-Dist: openai>=1.0; extra == "openai"
26
+ Provides-Extra: all
27
+ Requires-Dist: anthropic>=0.40; extra == "all"
28
+ Requires-Dist: openai>=1.0; extra == "all"
29
+
30
+ ```
31
+ __ __ ____ _
32
+ | \/ |_ _ / ___|___ __| | ___
33
+ | |\/| | | | | | / _ \ / _` |/ _ \
34
+ | | | | |_| | |__| (_) | (_| | __/
35
+ |_| |_|\__, |\____\___/ \__,_|\___|
36
+ |___/
37
+ ```
38
+
39
+ > **Style-aware code generation.** Analyze any codebase to extract its coding style, then generate new code that matches it exactly.
40
+
41
+ ---
42
+
43
+ ## What it does
44
+
45
+ MyCode learns how a developer or team writes code — naming conventions, type annotation style, import grouping, docstring format, error handling patterns — and uses that style profile to generate new code that feels like it was written by the same hand.
46
+
47
+ It is built to slot into larger agentic systems: the analyzer and generator are clean library functions, and the backend is swappable (local LLM, Claude, or OpenAI).
48
+
49
+ ---
50
+
51
+ ## Requirements
52
+
53
+ - Python 3.10+
54
+ - A running AI backend (see [Backends](#backends))
55
+
56
+ ---
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ # Clone and install in editable mode
62
+ git clone <repo-url>
63
+ cd MyCode
64
+ pip install -e .
65
+
66
+ # For Claude backend support
67
+ pip install -e ".[claude]"
68
+
69
+ # For OpenAI backend support
70
+ pip install -e ".[openai]"
71
+
72
+ # For all backends
73
+ pip install -e ".[all]"
74
+ ```
75
+
76
+ The `my-code` CLI command is registered automatically on install.
77
+
78
+ ---
79
+
80
+ ## Backends
81
+
82
+ MyCode delegates inference to a pluggable backend. Choose one based on what you have available.
83
+
84
+ | Backend | Flag | Requirement |
85
+ |---|---|---|
86
+ | Local LLM | `--backend llama` (default) | MCP server running at `localhost:8000` |
87
+ | Anthropic Claude | `--backend claude` | `ANTHROPIC_API_KEY` env var or `--api-key` |
88
+ | OpenAI | `--backend openai` | `OPENAI_API_KEY` env var or `--api-key` |
89
+ | Custom MCP server | `--backend mcp` | Any MCP server at `--mcp-url` |
90
+
91
+ ### Setting up a local LLM
92
+
93
+ The `llama` backend expects an MCP server at `http://localhost:8000/mcp` that exposes two tools: one for code generation and one for analysis. Any MCP-compatible wrapper around a local model will work. Here is a recommended setup using [llama.cpp](https://github.com/ggerganov/llama.cpp):
94
+
95
+ **1. Download a model**
96
+
97
+ A code-focused model works best. Good options:
98
+ - [CodeLlama-7B-Instruct](https://huggingface.co/TheBloke/CodeLlama-7B-Instruct-GGUF) — fast, runs on most hardware
99
+ - [CodeLlama-13B-Instruct](https://huggingface.co/TheBloke/CodeLlama-13B-Instruct-GGUF) — better quality, needs ~10 GB VRAM
100
+ - [DeepSeek-Coder-6.7B-Instruct](https://huggingface.co/TheBloke/deepseek-coder-6.7B-instruct-GGUF) — strong alternative
101
+
102
+ Download a `.gguf` quantized file (Q4_K_M is a good balance of size and quality).
103
+
104
+ **2. Start the llama.cpp server**
105
+
106
+ ```bash
107
+ # Install llama.cpp (or use a pre-built binary)
108
+ pip install llama-cpp-python[server]
109
+
110
+ # Start the OpenAI-compatible server
111
+ python -m llama_cpp.server \
112
+ --model ./models/codellama-7b-instruct.Q4_K_M.gguf \
113
+ --host 0.0.0.0 \
114
+ --port 8000 \
115
+ --n_ctx 4096
116
+ ```
117
+
118
+ **3. Wrap it with an MCP server**
119
+
120
+ The `llama` backend communicates over MCP, not directly with the llama.cpp HTTP API. You need a thin MCP wrapper that exposes two tools:
121
+ - A **code generation tool** (name must not contain `"analyze"`)
122
+ - An **analysis tool** (name must contain `"analyze"`)
123
+
124
+ Both tools accept a `query` string and return the model's completion. A minimal FastMCP wrapper example:
125
+
126
+ ```python
127
+ # llm_mcp_server.py
128
+ from fastmcp import FastMCP
129
+ import requests
130
+
131
+ mcp = FastMCP("local-llm")
132
+ LLM_URL = "http://localhost:8000/v1/completions"
133
+
134
+ def _complete(prompt: str) -> str:
135
+ resp = requests.post(LLM_URL, json={
136
+ "prompt": prompt,
137
+ "max_tokens": 1024,
138
+ "temperature": 0.1,
139
+ })
140
+ return resp.json()["choices"][0]["text"]
141
+
142
+ @mcp.tool()
143
+ def generate_code(query: str) -> str:
144
+ return _complete(query)
145
+
146
+ @mcp.tool()
147
+ def analyze_code(query: str) -> str:
148
+ return _complete(query)
149
+
150
+ if __name__ == "__main__":
151
+ mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
152
+ ```
153
+
154
+ ```bash
155
+ pip install fastmcp
156
+ python llm_mcp_server.py
157
+ ```
158
+
159
+ **4. Point MyCode at it**
160
+
161
+ ```bash
162
+ my-code --backend llama --ricky-url http://localhost:8000/mcp analyze .
163
+ ```
164
+
165
+ Or set a custom URL if your server runs on a different port:
166
+
167
+ ```bash
168
+ my-code --backend llama --ricky-url http://localhost:9000/mcp analyze .
169
+ ```
170
+
171
+ ---
172
+
173
+ ## CLI Usage
174
+
175
+ ### Step 1 — Analyze a codebase
176
+
177
+ Point MyCode at any directory. It reads every `.py` file and builds a style profile.
178
+
179
+ ```bash
180
+ my-code analyze ./path/to/codebase
181
+ ```
182
+
183
+ With verbose output:
184
+ ```bash
185
+ my-code analyze ./path/to/codebase --verbose
186
+ ```
187
+
188
+ Using a different backend:
189
+ ```bash
190
+ my-code --backend claude analyze ./path/to/codebase
191
+ my-code --backend openai --api-key sk-... analyze ./path/to/codebase
192
+ ```
193
+
194
+ The profile is saved to `style_profile.json` by default. Specify a different path with `--profile`:
195
+ ```bash
196
+ my-code analyze ./path/to/codebase --profile ./profiles/my_team.json
197
+ ```
198
+
199
+ ### Step 2 — Generate code
200
+
201
+ ```bash
202
+ my-code generate "write a function that parses a CSV file and returns a list of dicts"
203
+ ```
204
+
205
+ MyCode loads `style_profile.json` and instructs the backend to produce code that matches the analyzed style — naming, annotations, docstrings, structure and all.
206
+
207
+ ```bash
208
+ # Use a specific profile
209
+ my-code generate "write a retry decorator" --profile ./profiles/my_team.json
210
+
211
+ # Use Claude to generate
212
+ my-code --backend claude generate "write a binary search function"
213
+
214
+ # Override the model
215
+ my-code --backend claude --model claude-sonnet-4-6 generate "write a rate limiter"
216
+ ```
217
+
218
+ ---
219
+
220
+ ## CLI Reference
221
+
222
+ ```
223
+ my-code [OPTIONS] COMMAND
224
+
225
+ Options:
226
+ --backend {llama,claude,openai,mcp} AI backend to use (default: llama)
227
+ --api-key TEXT API key for claude/openai backends
228
+ --model TEXT Override the default model
229
+ --ricky-url TEXT Local LLM MCP server URL (default: http://localhost:8000/mcp)
230
+ --mcp-url TEXT Custom MCP server URL (default: http://localhost:8001/mcp)
231
+ --profile TEXT Path to style profile JSON (default: style_profile.json)
232
+
233
+ Commands:
234
+ analyze PATH Analyze a codebase and write a style profile
235
+ --verbose Print each file as it is analyzed
236
+
237
+ generate TASK Generate code matching the saved style profile
238
+ ```
239
+
240
+ ---
241
+
242
+ ## Python API
243
+
244
+ MyCode is a first-class library. All CLI functionality is available programmatically.
245
+
246
+ ```python
247
+ from my_code import StyleAnalyzer, generate_code, make_backend
248
+ from pathlib import Path
249
+
250
+ # Create a backend
251
+ backend = make_backend() # local LLM (default)
252
+ backend = make_backend("claude") # Claude (reads ANTHROPIC_API_KEY)
253
+ backend = make_backend("openai", api_key="sk-...") # OpenAI
254
+
255
+ # Analyze a codebase
256
+ analyzer = StyleAnalyzer(backend)
257
+ profile = analyzer.analyze_codebase(Path("./my_project"), verbose=True)
258
+
259
+ # Save and reload the profile
260
+ StyleAnalyzer.save_profile(profile, Path("style.json"))
261
+ profile = StyleAnalyzer.load_profile(Path("style.json"))
262
+
263
+ # Generate code
264
+ code = generate_code(
265
+ task="write a function that validates an email address",
266
+ backend=backend,
267
+ profile=profile,
268
+ )
269
+ print(code)
270
+ ```
271
+
272
+ ### Bring your own backend
273
+
274
+ Implement `AIBackend` to connect any model:
275
+
276
+ ```python
277
+ from my_code import AIBackend, StyleAnalyzer, generate_code
278
+
279
+ class MyBackend(AIBackend):
280
+ max_file_chars = 4000 # how much of each file to send for analysis
281
+
282
+ def ask_for_code(self, prompt: str) -> str:
283
+ # call your model, return the generated code as a string
284
+ ...
285
+
286
+ def ask_to_analyze(self, prompt: str) -> str:
287
+ # call your model, return a JSON string describing the style
288
+ ...
289
+
290
+ backend = MyBackend()
291
+ analyzer = StyleAnalyzer(backend)
292
+ profile = analyzer.analyze_codebase(Path("."))
293
+ code = generate_code("write a logging helper", backend, profile)
294
+ ```
295
+
296
+ ---
297
+
298
+ ## Deep Analysis
299
+
300
+ For a richer style profile, `scripts/deep_analyze.py` runs six focused queries (naming, error handling, string formatting, module structure, docstrings, and representative snippets) and synthesizes them into a single detailed profile.
301
+
302
+ ```bash
303
+ # Run from the project root; writes style_profile.json
304
+ python scripts/deep_analyze.py
305
+ ```
306
+
307
+ This is slower than the standard `analyze` command but produces a more detailed profile, which leads to better code generation.
308
+
309
+ ---
310
+
311
+ ## Running Tests
312
+
313
+ ```bash
314
+ python tests/test_library.py
315
+ ```
316
+
317
+ The test suite uses a `MockBackend` so no live AI backend is required. It exercises the full analyze → generate pipeline.
318
+
319
+ ---
320
+
321
+ ## Project Structure
322
+
323
+ ```
324
+ my_code/
325
+ ├── analyzer.py # StyleAnalyzer — scans files, builds style profile
326
+ ├── generator.py # generate_code() — formats prompt and calls backend
327
+ ├── ricky_client.py # Low-level MCP client for the local LLM server
328
+ ├── cli.py # CLI entry point (my-code command)
329
+ ├── backends/
330
+ │ ├── base.py # AIBackend abstract base class
331
+ │ ├── ricky_backend.py # Local LLM backend (connects via MCP)
332
+ │ ├── claude_backend.py
333
+ │ ├── openai_backend.py
334
+ │ └── mcp_backend.py # Placeholder for custom MCP backends
335
+ └── utils/
336
+ └── prompts.py # Prompt templates for extraction, summary, generation
337
+ scripts/
338
+ └── deep_analyze.py # Multi-query deep style analysis
339
+ tests/
340
+ └── test_library.py # Smoke tests (no live backend required)
341
+ ```
@@ -0,0 +1,19 @@
1
+ my_code/__init__.py,sha256=YCTHYVmOKP6TmIFDxnQDcc84K4N8AwqB6wGjn-H5vOU,372
2
+ my_code/__main__.py,sha256=-vt2T7-_AnWJ8BTBSmJKPXmOZtpe6TkNXghJJ6dIP_o,63
3
+ my_code/analyzer.py,sha256=-6auv6bbxe10ep_Y2T9uiq2qM7CLTU1M2nwRE7kN_rU,2850
4
+ my_code/cli.py,sha256=H1QXPLLbsrqEkdxcjASrr4xwHbs8uGVOU81E7Rzr4U4,2985
5
+ my_code/generator.py,sha256=R3FPVDNy-8YAMjyZyuLygLH913M2WAGE7q4BqvFdDXU,401
6
+ my_code/ricky_client.py,sha256=irve8roLPQBtsEZMDBXKHL4JWt2Mv65ZMNYGlYGbLWw,4187
7
+ my_code/backends/__init__.py,sha256=FjtrGB7y_3O_s1O81G5gICUUVcMHtnarY1BXP-uGmuU,1328
8
+ my_code/backends/base.py,sha256=9hc6HaNcJSX8twRZM897teGONQWe3CcoltwzIjhdrkA,268
9
+ my_code/backends/claude_backend.py,sha256=O37BnJXnOTzvRPIsWl07WtYElVH-Ov2cJKKt2T3jS_0,1082
10
+ my_code/backends/mcp_backend.py,sha256=g2SAqMmL-_A5NEEouNE1l7XdiVyw3iMku-Ko4F3ZHp8,421
11
+ my_code/backends/openai_backend.py,sha256=SneHFNQQKhq4hKZgN7WniyVgP9lJd6wK1vZaOIokCWw,1110
12
+ my_code/backends/ricky_backend.py,sha256=EVXgWyjBEr5b9tY6wvTxQLUgXu70agPSuByo30TVKYM,440
13
+ my_code/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ my_code/utils/prompts.py,sha256=azaC61hc9eaTSnGiFD0botw7-HoTMzQKawJ9X1YEa0w,2194
15
+ mycode_aiagent-0.1.0.dist-info/METADATA,sha256=5apqZyJtbt_4iSDs89O6pu3fxUI1r2-eyoY6NKLmoS0,10916
16
+ mycode_aiagent-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
17
+ mycode_aiagent-0.1.0.dist-info/entry_points.txt,sha256=-bGlti5BHHrdHW2XhcQW-W-gufCQek9PsczygeFZkXs,45
18
+ mycode_aiagent-0.1.0.dist-info/top_level.txt,sha256=xAInreiHP8EP1o4MxvRhovZW9pZuinc3miY3ZNAnlJc,8
19
+ mycode_aiagent-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ my-code = my_code.cli:main
@@ -0,0 +1 @@
1
+ my_code