mita-code 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.
Files changed (67) hide show
  1. mita/__init__.py +5 -0
  2. mita/__main__.py +5 -0
  3. mita/agent/__init__.py +1 -0
  4. mita/agent/context.py +43 -0
  5. mita/agent/conversation.py +101 -0
  6. mita/agent/loop.py +594 -0
  7. mita/agent/system_prompt.py +75 -0
  8. mita/cli.py +940 -0
  9. mita/config/__init__.py +6 -0
  10. mita/config/defaults.py +41 -0
  11. mita/config/loader.py +53 -0
  12. mita/config/schema.py +131 -0
  13. mita/hooks/__init__.py +1 -0
  14. mita/hooks/manager.py +94 -0
  15. mita/hooks/runner.py +145 -0
  16. mita/index/__init__.py +1 -0
  17. mita/index/embeddings.py +49 -0
  18. mita/index/manager.py +170 -0
  19. mita/index/parser.py +331 -0
  20. mita/index/retriever.py +53 -0
  21. mita/index/store.py +143 -0
  22. mita/llm/__init__.py +1 -0
  23. mita/llm/client.py +86 -0
  24. mita/llm/instructor.py +80 -0
  25. mita/llm/streaming.py +58 -0
  26. mita/memory/__init__.py +6 -0
  27. mita/memory/discovery.py +61 -0
  28. mita/memory/loader.py +76 -0
  29. mita/memory/manager.py +117 -0
  30. mita/models/__init__.py +13 -0
  31. mita/models/hardware.py +289 -0
  32. mita/models/manager.py +268 -0
  33. mita/models/ollama_client.py +104 -0
  34. mita/models/recommender.py +88 -0
  35. mita/models/registry.py +167 -0
  36. mita/models/server.py +262 -0
  37. mita/plugins/__init__.py +1 -0
  38. mita/plugins/client.py +152 -0
  39. mita/plugins/manager.py +210 -0
  40. mita/py.typed +0 -0
  41. mita/skills/__init__.py +1 -0
  42. mita/skills/executor.py +84 -0
  43. mita/skills/loader.py +117 -0
  44. mita/skills/manager.py +129 -0
  45. mita/tools/__init__.py +1 -0
  46. mita/tools/builtins/__init__.py +28 -0
  47. mita/tools/builtins/file_edit.py +71 -0
  48. mita/tools/builtins/file_read.py +74 -0
  49. mita/tools/builtins/file_write.py +42 -0
  50. mita/tools/builtins/git.py +112 -0
  51. mita/tools/builtins/glob_tool.py +67 -0
  52. mita/tools/builtins/grep_tool.py +93 -0
  53. mita/tools/builtins/shell.py +83 -0
  54. mita/tools/executor.py +80 -0
  55. mita/tools/registry.py +69 -0
  56. mita/tools/safety.py +91 -0
  57. mita/tools/schema.py +87 -0
  58. mita/ui/__init__.py +1 -0
  59. mita/ui/display.py +139 -0
  60. mita/ui/repl.py +88 -0
  61. mita/ui/spinner.py +48 -0
  62. mita/ui/theme.py +23 -0
  63. mita_code-0.1.0.dist-info/METADATA +227 -0
  64. mita_code-0.1.0.dist-info/RECORD +67 -0
  65. mita_code-0.1.0.dist-info/WHEEL +4 -0
  66. mita_code-0.1.0.dist-info/entry_points.txt +3 -0
  67. mita_code-0.1.0.dist-info/licenses/LICENSE +201 -0
mita/llm/client.py ADDED
@@ -0,0 +1,86 @@
1
+ """LiteLLM client pointing at Ollama for chat completions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator
6
+ from typing import Any
7
+
8
+ import litellm
9
+
10
+ from mita.config.schema import MitaConfig
11
+
12
+
13
+ class LLMClient:
14
+ """Thin wrapper around LiteLLM configured for Ollama."""
15
+
16
+ def __init__(self, config: MitaConfig) -> None:
17
+ self._model = f"ollama/{config.model.default}"
18
+ self._api_base = config.ollama.host
19
+ self._temperature = config.model.temperature
20
+ self._max_tokens = config.model.max_tokens
21
+ self._stream = config.ui.stream
22
+ self._ollama_options = config.model.ollama_options.to_api_dict()
23
+
24
+ # Suppress LiteLLM's verbose logging
25
+ litellm.suppress_debug_info = True
26
+
27
+ @property
28
+ def model(self) -> str:
29
+ """The model identifier being used."""
30
+ return self._model
31
+
32
+ async def chat(
33
+ self,
34
+ messages: list[dict[str, Any]],
35
+ tools: list[dict[str, Any]] | None = None,
36
+ ) -> dict[str, Any]:
37
+ """Send a chat completion request (non-streaming).
38
+
39
+ Returns the full response dict from LiteLLM.
40
+ """
41
+ kwargs: dict[str, Any] = {
42
+ "model": self._model,
43
+ "messages": messages,
44
+ "temperature": self._temperature,
45
+ "max_tokens": self._max_tokens,
46
+ "api_base": self._api_base,
47
+ }
48
+ if tools:
49
+ kwargs["tools"] = tools
50
+ if self._ollama_options:
51
+ kwargs["extra_body"] = {"options": self._ollama_options}
52
+
53
+ response = await litellm.acompletion(**kwargs)
54
+ return response # type: ignore[no-any-return]
55
+
56
+ async def stream_chat(
57
+ self,
58
+ messages: list[dict[str, Any]],
59
+ tools: list[dict[str, Any]] | None = None,
60
+ ) -> AsyncIterator[dict[str, Any]]:
61
+ """Send a streaming chat completion request.
62
+
63
+ Yields delta dicts with content tokens as they arrive.
64
+ """
65
+ kwargs: dict[str, Any] = {
66
+ "model": self._model,
67
+ "messages": messages,
68
+ "temperature": self._temperature,
69
+ "max_tokens": self._max_tokens,
70
+ "api_base": self._api_base,
71
+ "stream": True,
72
+ "stream_options": {"include_usage": True},
73
+ }
74
+ if tools:
75
+ kwargs["tools"] = tools
76
+ if self._ollama_options:
77
+ kwargs["extra_body"] = {"options": self._ollama_options}
78
+
79
+ response = await litellm.acompletion(**kwargs)
80
+ async for chunk in response:
81
+ yield chunk
82
+
83
+
84
+ def get_client(config: MitaConfig) -> LLMClient:
85
+ """Create an LLM client from configuration."""
86
+ return LLMClient(config)
mita/llm/instructor.py ADDED
@@ -0,0 +1,80 @@
1
+ """Instructor wrapper for structured tool call parsing via JSON mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, TypeVar
6
+
7
+ import instructor
8
+ import litellm
9
+ from pydantic import BaseModel
10
+
11
+ from mita.config.schema import MitaConfig
12
+
13
+ T = TypeVar("T", bound=BaseModel)
14
+
15
+
16
+ class ParsedToolCall(BaseModel):
17
+ """A tool call parsed from LLM output (llm-local model, no tools dependency)."""
18
+
19
+ id: str
20
+ name: str
21
+ arguments: dict[str, Any]
22
+
23
+
24
+ class ToolCallResponse(BaseModel):
25
+ """Structured response containing tool calls parsed from LLM output."""
26
+
27
+ reasoning: str = ""
28
+ tool_calls: list[ParsedToolCall] = []
29
+ text_response: str = ""
30
+
31
+
32
+ class InstructorClient:
33
+ """Instructor-wrapped LiteLLM client for structured output parsing."""
34
+
35
+ def __init__(self, config: MitaConfig) -> None:
36
+ self._model = f"ollama/{config.model.default}"
37
+ self._api_base = config.ollama.host
38
+ self._temperature = config.model.temperature
39
+ self._max_tokens = config.model.max_tokens
40
+
41
+ self._client = instructor.from_litellm(
42
+ litellm.acompletion,
43
+ mode=instructor.Mode.JSON,
44
+ )
45
+
46
+ async def parse_tool_calls(
47
+ self,
48
+ messages: list[dict[str, Any]],
49
+ ) -> ToolCallResponse:
50
+ """Parse structured tool calls from LLM output using Instructor JSON mode."""
51
+ response: ToolCallResponse = await self._client.create(
52
+ model=self._model,
53
+ messages=messages,
54
+ response_model=ToolCallResponse,
55
+ temperature=self._temperature,
56
+ max_tokens=self._max_tokens,
57
+ api_base=self._api_base,
58
+ )
59
+ return response
60
+
61
+ async def parse_structured(
62
+ self,
63
+ messages: list[dict[str, Any]],
64
+ response_model: type[T],
65
+ ) -> T:
66
+ """Parse any structured Pydantic model from LLM output."""
67
+ response: T = await self._client.create(
68
+ model=self._model,
69
+ messages=messages,
70
+ response_model=response_model,
71
+ temperature=self._temperature,
72
+ max_tokens=self._max_tokens,
73
+ api_base=self._api_base,
74
+ )
75
+ return response
76
+
77
+
78
+ def get_instructor_client(config: MitaConfig) -> InstructorClient:
79
+ """Create an Instructor client from configuration."""
80
+ return InstructorClient(config)
mita/llm/streaming.py ADDED
@@ -0,0 +1,58 @@
1
+ """Token-by-token streaming handler for Rich terminal display."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ from mita.llm.client import LLMClient
9
+
10
+
11
+ async def stream_to_terminal(
12
+ client: LLMClient,
13
+ messages: list[dict[str, Any]],
14
+ on_token: Callable[[str], None] | None = None,
15
+ on_complete: Callable[[str], None] | None = None,
16
+ tools: list[dict[str, Any]] | None = None,
17
+ ) -> str:
18
+ """Stream a chat completion, calling on_token for each chunk.
19
+
20
+ Args:
21
+ client: The LLM client to use.
22
+ messages: Chat messages.
23
+ on_token: Callback called with each text token as it arrives.
24
+ on_complete: Callback called with the full text when streaming is done.
25
+ tools: Optional tool schemas for the LLM.
26
+
27
+ Returns:
28
+ The complete response text.
29
+ """
30
+ full_text = ""
31
+
32
+ async for chunk in client.stream_chat(messages, tools=tools):
33
+ delta = _extract_delta_content(chunk)
34
+ if delta:
35
+ full_text += delta
36
+ if on_token:
37
+ on_token(delta)
38
+
39
+ if on_complete:
40
+ on_complete(full_text)
41
+
42
+ return full_text
43
+
44
+
45
+ def _extract_delta_content(chunk: Any) -> str:
46
+ """Extract text content from a streaming chunk."""
47
+ try:
48
+ choices = chunk.choices if hasattr(chunk, "choices") else chunk.get("choices", [])
49
+ if not choices:
50
+ return ""
51
+ delta = choices[0].delta if hasattr(choices[0], "delta") else choices[0].get("delta", {})
52
+ if hasattr(delta, "content"):
53
+ return delta.content or ""
54
+ if isinstance(delta, dict):
55
+ return delta.get("content", "") or ""
56
+ except (IndexError, AttributeError, KeyError):
57
+ pass
58
+ return ""
@@ -0,0 +1,6 @@
1
+ """MITA.md memory discovery, loading, and management."""
2
+
3
+ from mita.memory.discovery import discover_memory_files
4
+ from mita.memory.loader import load_memory
5
+
6
+ __all__ = ["discover_memory_files", "load_memory"]
@@ -0,0 +1,61 @@
1
+ """Walk-up-tree discovery of MITA.md memory files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from mita.config.defaults import PROJECT_CONFIG_DIR
8
+
9
+ MEMORY_FILENAME = "MITA.md"
10
+
11
+
12
+ def get_global_memory_path() -> Path:
13
+ """Return the path to the global MITA.md file."""
14
+ return Path.home() / ".config" / "mita" / MEMORY_FILENAME
15
+
16
+
17
+ def discover_memory_files(cwd: Path | None = None) -> list[Path]:
18
+ """Discover MITA.md files by walking up from cwd to the project root.
19
+
20
+ Lookup order (returned list, lowest priority first):
21
+ 1. Global: ~/.config/mita/MITA.md
22
+ 2. Project root: <project_root>/MITA.md or <project_root>/.mita/MITA.md
23
+ 3. Directory-level: <cwd>/MITA.md (if different from project root)
24
+
25
+ Returns paths ordered from lowest to highest priority (global first).
26
+ """
27
+ if cwd is None:
28
+ cwd = Path.cwd()
29
+ cwd = cwd.resolve()
30
+
31
+ found: list[Path] = []
32
+
33
+ # Walk up from cwd, collecting MITA.md files
34
+ current = cwd
35
+ while True:
36
+ candidate = current / MEMORY_FILENAME
37
+ if candidate.is_file():
38
+ found.append(candidate)
39
+
40
+ # Check .mita/ subdirectory
41
+ dotmita_candidate = current / PROJECT_CONFIG_DIR / MEMORY_FILENAME
42
+ if dotmita_candidate.is_file() and dotmita_candidate not in found:
43
+ found.append(dotmita_candidate)
44
+
45
+ # Stop at project root (contains .git or .mita)
46
+ if (current / ".git").exists() or (current / PROJECT_CONFIG_DIR).exists():
47
+ break
48
+
49
+ parent = current.parent
50
+ if parent == current:
51
+ break
52
+ current = parent
53
+
54
+ # Add global memory if it exists and isn't already found
55
+ global_memory_path = get_global_memory_path()
56
+ if global_memory_path.is_file() and global_memory_path not in found:
57
+ found.append(global_memory_path)
58
+
59
+ # Reverse so global (lowest priority) is first
60
+ found.reverse()
61
+ return found
mita/memory/loader.py ADDED
@@ -0,0 +1,76 @@
1
+ """Read, merge, and truncate MITA.md memory files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from mita.config.schema import MemorySettings
8
+ from mita.memory.discovery import discover_memory_files
9
+
10
+
11
+ def _classify_scope(path: Path) -> str:
12
+ """Classify a memory file as global, project, or directory scope."""
13
+ home = Path.home()
14
+ if str(path).startswith(str(home / ".config" / "mita")):
15
+ return "global"
16
+ # If the file is at a project root (parent has .git or .mita), it's project-level
17
+ parent = path.parent
18
+ if parent.name == ".mita":
19
+ parent = parent.parent
20
+ if (parent / ".git").exists() or (parent / ".mita").exists():
21
+ return "project"
22
+ return "directory"
23
+
24
+
25
+ def _read_and_truncate(path: Path, max_lines: int) -> tuple[str, bool]:
26
+ """Read a file and truncate to max_lines. Returns (content, was_truncated)."""
27
+ lines = path.read_text(encoding="utf-8").splitlines()
28
+ if len(lines) <= max_lines:
29
+ return "\n".join(lines), False
30
+ truncated = lines[:max_lines]
31
+ truncated.append(f"[... truncated at {max_lines} lines. Keep MITA.md concise.]")
32
+ return "\n".join(truncated), True
33
+
34
+
35
+ def load_memory(
36
+ cwd: Path | None = None,
37
+ settings: MemorySettings | None = None,
38
+ ) -> str:
39
+ """Load and merge all discovered MITA.md files into a single string.
40
+
41
+ Returns formatted memory content with source annotations, suitable
42
+ for injection into the system prompt.
43
+ """
44
+ if settings is None:
45
+ settings = MemorySettings()
46
+
47
+ files = discover_memory_files(cwd)
48
+ if not files:
49
+ return ""
50
+
51
+ sections: list[str] = []
52
+ for path in files:
53
+ scope = _classify_scope(path)
54
+ content, _ = _read_and_truncate(path, settings.max_lines_per_file)
55
+ if content.strip():
56
+ sections.append(f"<!-- Source: {path} ({scope}) -->\n{content}")
57
+
58
+ if not sections:
59
+ return ""
60
+
61
+ return "<memory>\n" + "\n\n".join(sections) + "\n</memory>"
62
+
63
+
64
+ def load_memory_raw(cwd: Path | None = None) -> list[tuple[Path, str, str]]:
65
+ """Load memory files and return as list of (path, scope, content) tuples.
66
+
67
+ Useful for the `mita memory show` CLI command.
68
+ """
69
+ settings = MemorySettings()
70
+ files = discover_memory_files(cwd)
71
+ result: list[tuple[Path, str, str]] = []
72
+ for path in files:
73
+ scope = _classify_scope(path)
74
+ content, _ = _read_and_truncate(path, settings.max_lines_per_file)
75
+ result.append((path, scope, content))
76
+ return result
mita/memory/manager.py ADDED
@@ -0,0 +1,117 @@
1
+ """CLI commands for MITA.md memory management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ from rich.console import Console
10
+ from rich.markdown import Markdown
11
+ from rich.panel import Panel
12
+
13
+ from mita.config.defaults import _find_project_root
14
+ from mita.memory.discovery import MEMORY_FILENAME, discover_memory_files, get_global_memory_path
15
+ from mita.memory.loader import load_memory_raw
16
+
17
+ console = Console()
18
+
19
+
20
+ def show_memory() -> None:
21
+ """Display all discovered memory content with source annotations."""
22
+ entries = load_memory_raw()
23
+ if not entries:
24
+ console.print("[dim]No MITA.md files found.[/dim]")
25
+ return
26
+
27
+ for path, scope, content in entries:
28
+ console.print(
29
+ Panel(
30
+ Markdown(content) if content.strip() else "[dim]Empty[/dim]",
31
+ title=f"[bold]{path}[/bold]",
32
+ subtitle=f"[dim]{scope}[/dim]",
33
+ border_style="blue",
34
+ )
35
+ )
36
+
37
+
38
+ def show_memory_paths() -> None:
39
+ """Display the paths of all discovered MITA.md files."""
40
+ files = discover_memory_files()
41
+ if not files:
42
+ console.print("[dim]No MITA.md files found.[/dim]")
43
+ return
44
+
45
+ for path in files:
46
+ console.print(f" {path}")
47
+
48
+
49
+ def edit_memory(scope: str | None = None) -> None:
50
+ """Open a MITA.md file in $EDITOR.
51
+
52
+ Args:
53
+ scope: "global", "project", or None (nearest/project default).
54
+ """
55
+ editor = os.environ.get("EDITOR", "vi")
56
+
57
+ if scope == "global":
58
+ path = get_global_memory_path()
59
+ path.parent.mkdir(parents=True, exist_ok=True)
60
+ if not path.exists():
61
+ path.write_text("# Global Mita Memory\n\n", encoding="utf-8")
62
+ elif scope == "project":
63
+ project_root = _find_project_root(Path.cwd())
64
+ if project_root is None:
65
+ console.print("[red]Not in a project (no .git or .mita directory found).[/red]")
66
+ return
67
+ path = project_root / MEMORY_FILENAME
68
+ if not path.exists():
69
+ path.write_text(f"# {project_root.name} — Mita Memory\n\n", encoding="utf-8")
70
+ else:
71
+ # Find nearest existing MITA.md, or create at project root
72
+ files = discover_memory_files()
73
+ # Filter out global — prefer project/directory level
74
+ local_files = [f for f in files if "/.config/mita/" not in str(f)]
75
+ if local_files:
76
+ path = local_files[-1] # highest priority (most specific)
77
+ else:
78
+ project_root = _find_project_root(Path.cwd())
79
+ if project_root is None:
80
+ console.print(
81
+ "[red]Not in a project (no .git or .mita directory found). "
82
+ "Use --global to edit global memory.[/red]"
83
+ )
84
+ return
85
+ path = project_root / MEMORY_FILENAME
86
+ if not path.exists():
87
+ path.write_text(f"# {project_root.name} — Mita Memory\n\n", encoding="utf-8")
88
+
89
+ console.print(f"[dim]Opening {path} in {editor}...[/dim]")
90
+ subprocess.run([editor, str(path)])
91
+
92
+
93
+ def add_memory(text: str, scope: str | None = None) -> None:
94
+ """Append a line of text to a MITA.md file.
95
+
96
+ Args:
97
+ text: Text to append.
98
+ scope: "global", "project", or None (defaults to project).
99
+ """
100
+ if scope == "global":
101
+ path = get_global_memory_path()
102
+ path.parent.mkdir(parents=True, exist_ok=True)
103
+ if not path.exists():
104
+ path.write_text("# Global Mita Memory\n\n", encoding="utf-8")
105
+ else:
106
+ project_root = _find_project_root(Path.cwd())
107
+ if project_root is None:
108
+ console.print("[red]Not in a project (no .git or .mita directory found).[/red]")
109
+ return
110
+ path = project_root / MEMORY_FILENAME
111
+ if not path.exists():
112
+ path.write_text(f"# {project_root.name} — Mita Memory\n\n", encoding="utf-8")
113
+
114
+ with open(path, "a", encoding="utf-8") as f:
115
+ f.write(f"- {text}\n")
116
+
117
+ console.print(f"[green]Added to {path}[/green]")
@@ -0,0 +1,13 @@
1
+ """Model management: hardware detection, registry, recommendations, Ollama client."""
2
+
3
+ from mita.models.hardware import HardwareInfo, detect_hardware
4
+ from mita.models.recommender import ModelRecommendation, recommend_models
5
+ from mita.models.registry import ModelCard
6
+
7
+ __all__ = [
8
+ "HardwareInfo",
9
+ "ModelCard",
10
+ "ModelRecommendation",
11
+ "detect_hardware",
12
+ "recommend_models",
13
+ ]