minima-cli 0.4.9__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 (161) hide show
  1. minima/__init__.py +5 -0
  2. minima/api/__init__.py +1 -0
  3. minima/api/auth.py +39 -0
  4. minima/api/errors.py +40 -0
  5. minima/api/routers/__init__.py +1 -0
  6. minima/api/routers/calibration.py +50 -0
  7. minima/api/routers/feedback.py +279 -0
  8. minima/api/routers/health.py +50 -0
  9. minima/api/routers/models.py +42 -0
  10. minima/api/routers/recommend.py +66 -0
  11. minima/api/routers/savings.py +55 -0
  12. minima/api/routers/strategies.py +33 -0
  13. minima/catalog/__init__.py +1 -0
  14. minima/catalog/data/capability_priors.json +210 -0
  15. minima/catalog/data/model_aliases.json +12 -0
  16. minima/catalog/merge.py +69 -0
  17. minima/catalog/refresh.py +54 -0
  18. minima/catalog/sources/__init__.py +1 -0
  19. minima/catalog/sources/litellm.py +19 -0
  20. minima/catalog/sources/openrouter.py +25 -0
  21. minima/catalog/store.py +86 -0
  22. minima/config.py +288 -0
  23. minima/deps.py +35 -0
  24. minima/llm/__init__.py +1 -0
  25. minima/llm/anthropic.py +106 -0
  26. minima/llm/base.py +196 -0
  27. minima/llm/gemini.py +124 -0
  28. minima/llm/registry.py +54 -0
  29. minima/logging.py +28 -0
  30. minima/main.py +109 -0
  31. minima/memory/__init__.py +1 -0
  32. minima/memory/adapter.py +572 -0
  33. minima/memory/keys.py +83 -0
  34. minima/memory/records.py +190 -0
  35. minima/memory/threadpool.py +41 -0
  36. minima/metrics/__init__.py +1 -0
  37. minima/metrics/calibration.py +415 -0
  38. minima/metrics/report.py +116 -0
  39. minima/metrics/savings.py +98 -0
  40. minima/recommender/__init__.py +1 -0
  41. minima/recommender/_pg_pool.py +38 -0
  42. minima/recommender/_redis_client.py +32 -0
  43. minima/recommender/aggregate.py +157 -0
  44. minima/recommender/classify.py +165 -0
  45. minima/recommender/decisionlog.py +505 -0
  46. minima/recommender/durablerefs.py +312 -0
  47. minima/recommender/engine.py +997 -0
  48. minima/recommender/escalation.py +83 -0
  49. minima/recommender/propensity.py +189 -0
  50. minima/recommender/recstore.py +368 -0
  51. minima/recommender/score.py +318 -0
  52. minima/recommender/types.py +166 -0
  53. minima/schemas/__init__.py +1 -0
  54. minima/schemas/common.py +73 -0
  55. minima/schemas/feedback.py +34 -0
  56. minima/schemas/models_catalog.py +36 -0
  57. minima/schemas/recommend.py +104 -0
  58. minima/schemas/savings.py +39 -0
  59. minima/schemas/strategies.py +57 -0
  60. minima/schemas/workflow.py +43 -0
  61. minima/seeding/__init__.py +1 -0
  62. minima/seeding/items.py +42 -0
  63. minima/seeding/llmrouterbench.py +232 -0
  64. minima/seeding/routerbench.py +141 -0
  65. minima/seeding/run_seed.py +56 -0
  66. minima/seeding/synthetic.py +70 -0
  67. minima/tenancy/__init__.py +8 -0
  68. minima/tenancy/context.py +37 -0
  69. minima/tenancy/passthrough.py +110 -0
  70. minima/version.py +3 -0
  71. minima_cli-0.4.9.dist-info/METADATA +275 -0
  72. minima_cli-0.4.9.dist-info/RECORD +161 -0
  73. minima_cli-0.4.9.dist-info/WHEEL +4 -0
  74. minima_cli-0.4.9.dist-info/entry_points.txt +5 -0
  75. minima_cli-0.4.9.dist-info/licenses/LICENSE +295 -0
  76. minima_client/__init__.py +19 -0
  77. minima_client/autocapture.py +101 -0
  78. minima_client/client.py +301 -0
  79. minima_client/errors.py +23 -0
  80. minima_harness/LICENSE_PI +32 -0
  81. minima_harness/__init__.py +16 -0
  82. minima_harness/agent/__init__.py +72 -0
  83. minima_harness/agent/agent.py +276 -0
  84. minima_harness/agent/events.py +124 -0
  85. minima_harness/agent/loop.py +311 -0
  86. minima_harness/agent/state.py +79 -0
  87. minima_harness/agent/tools.py +97 -0
  88. minima_harness/ai/__init__.py +66 -0
  89. minima_harness/ai/compat.py +71 -0
  90. minima_harness/ai/errors.py +96 -0
  91. minima_harness/ai/events.py +117 -0
  92. minima_harness/ai/openrouter_catalog.py +153 -0
  93. minima_harness/ai/provider_catalog.py +299 -0
  94. minima_harness/ai/provider_quirks.py +37 -0
  95. minima_harness/ai/providers/__init__.py +75 -0
  96. minima_harness/ai/providers/_common.py +48 -0
  97. minima_harness/ai/providers/anthropic.py +290 -0
  98. minima_harness/ai/providers/base.py +65 -0
  99. minima_harness/ai/providers/faux.py +173 -0
  100. minima_harness/ai/providers/google.py +221 -0
  101. minima_harness/ai/providers/openai_compat.py +278 -0
  102. minima_harness/ai/registry.py +184 -0
  103. minima_harness/ai/stream.py +82 -0
  104. minima_harness/ai/tools.py +51 -0
  105. minima_harness/ai/types.py +204 -0
  106. minima_harness/ai/usage.py +41 -0
  107. minima_harness/minima/__init__.py +40 -0
  108. minima_harness/minima/cache.py +102 -0
  109. minima_harness/minima/config.py +85 -0
  110. minima_harness/minima/goals.py +226 -0
  111. minima_harness/minima/judge.py +144 -0
  112. minima_harness/minima/mapping.py +147 -0
  113. minima_harness/minima/meter.py +143 -0
  114. minima_harness/minima/router.py +220 -0
  115. minima_harness/minima/runtime.py +544 -0
  116. minima_harness/minima/signals.py +195 -0
  117. minima_harness/session/__init__.py +14 -0
  118. minima_harness/session/format.py +35 -0
  119. minima_harness/session/store.py +236 -0
  120. minima_harness/tasks/__init__.py +17 -0
  121. minima_harness/tasks/task_set.py +78 -0
  122. minima_harness/tools/__init__.py +7 -0
  123. minima_harness/tools/_io.py +34 -0
  124. minima_harness/tools/bash.py +70 -0
  125. minima_harness/tools/builtin.py +23 -0
  126. minima_harness/tools/edit.py +50 -0
  127. minima_harness/tools/find.py +38 -0
  128. minima_harness/tools/grep.py +73 -0
  129. minima_harness/tools/ls.py +35 -0
  130. minima_harness/tools/read.py +38 -0
  131. minima_harness/tools/tasks.py +75 -0
  132. minima_harness/tools/write.py +36 -0
  133. minima_harness/tui/__init__.py +3 -0
  134. minima_harness/tui/analytics.py +111 -0
  135. minima_harness/tui/app.py +1927 -0
  136. minima_harness/tui/bridge.py +103 -0
  137. minima_harness/tui/cli.py +227 -0
  138. minima_harness/tui/clipboard.py +60 -0
  139. minima_harness/tui/commands.py +49 -0
  140. minima_harness/tui/compaction.py +17 -0
  141. minima_harness/tui/config_cli.py +141 -0
  142. minima_harness/tui/config_store.py +237 -0
  143. minima_harness/tui/context.py +93 -0
  144. minima_harness/tui/customize.py +95 -0
  145. minima_harness/tui/diff.py +53 -0
  146. minima_harness/tui/editor.py +43 -0
  147. minima_harness/tui/extensions.py +84 -0
  148. minima_harness/tui/extra_models.py +52 -0
  149. minima_harness/tui/history.py +71 -0
  150. minima_harness/tui/mubit.py +295 -0
  151. minima_harness/tui/overlays.py +593 -0
  152. minima_harness/tui/packages.py +59 -0
  153. minima_harness/tui/run_modes.py +66 -0
  154. minima_harness/tui/theme.py +77 -0
  155. minima_harness/tui/welcome.py +83 -0
  156. minima_harness/tui/widgets/__init__.py +3 -0
  157. minima_harness/tui/widgets/banner.py +38 -0
  158. minima_harness/tui/widgets/editor.py +83 -0
  159. minima_harness/tui/widgets/footer.py +73 -0
  160. minima_harness/tui/widgets/messages.py +151 -0
  161. minima_harness/tui/widgets/status.py +57 -0
minima/llm/gemini.py ADDED
@@ -0,0 +1,124 @@
1
+ """Gemini-backed reasoner (best-effort, secondary provider).
2
+
3
+ Uses google-genai structured output. Any error degrades gracefully to None.
4
+
5
+ Gemini's ``response_schema`` is a constrained dialect of JSON Schema (a single ``type``
6
+ per field plus a ``nullable`` flag; no ``type: [...]`` unions and no
7
+ ``additionalProperties``), so the Anthropic/strict-JSON schemas in ``llm.base`` can't be
8
+ reused verbatim — these Gemini-native equivalents below are what the API accepts.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from collections.abc import Sequence
15
+ from typing import Any
16
+
17
+ from minima.llm.base import (
18
+ CLASSIFY_SYSTEM,
19
+ RANK_SYSTEM,
20
+ CandidateView,
21
+ ReasonerResult,
22
+ build_rank_user,
23
+ parse_classification,
24
+ parse_ranking,
25
+ )
26
+ from minima.logging import get_logger
27
+ from minima.schemas.common import Difficulty, TaskType
28
+
29
+ log = get_logger("minima.llm.gemini")
30
+
31
+ DEFAULT_MODEL = "gemini-2.5-flash"
32
+
33
+ _GEMINI_RANKING_SCHEMA: dict[str, Any] = {
34
+ "type": "object",
35
+ "properties": {
36
+ "recommended": {"type": "string"},
37
+ "fallback": {"type": "string", "nullable": True},
38
+ "ranking": {
39
+ "type": "array",
40
+ "items": {
41
+ "type": "object",
42
+ "properties": {
43
+ "model_id": {"type": "string"},
44
+ "predicted_success": {"type": "number"},
45
+ "rationale": {"type": "string"},
46
+ },
47
+ "required": ["model_id", "predicted_success", "rationale"],
48
+ },
49
+ },
50
+ },
51
+ "required": ["recommended", "ranking"],
52
+ }
53
+
54
+ _GEMINI_CLASSIFY_SCHEMA: dict[str, Any] = {
55
+ "type": "object",
56
+ "properties": {
57
+ "task_type": {"type": "string", "enum": [t.value for t in TaskType]},
58
+ "difficulty": {"type": "string", "enum": [d.value for d in Difficulty]},
59
+ },
60
+ "required": ["task_type", "difficulty"],
61
+ }
62
+
63
+
64
+ class GeminiReasoner:
65
+ def __init__(self, *, model: str, api_key: str, timeout_ms: int, max_tokens: int):
66
+ import google.genai as genai # lazy; optional extra
67
+
68
+ self._genai = genai
69
+ self._model = model
70
+ self._max_tokens = max_tokens
71
+ # Bound the call so a hung provider can't stall a recommendation (genai timeout
72
+ # is in milliseconds). Without this the Gemini client would wait indefinitely.
73
+ self._client = genai.Client(api_key=api_key, http_options={"timeout": timeout_ms})
74
+
75
+ async def _json_call(self, *, system: str, user: str, schema: dict) -> Any | None:
76
+ try:
77
+ resp = await self._client.aio.models.generate_content(
78
+ model=self._model,
79
+ contents=user,
80
+ config={
81
+ "system_instruction": system,
82
+ "response_mime_type": "application/json",
83
+ "response_schema": schema,
84
+ "max_output_tokens": self._max_tokens,
85
+ },
86
+ )
87
+ text = getattr(resp, "text", None)
88
+ if not text:
89
+ return None
90
+ return json.loads(text)
91
+ except Exception as exc: # noqa: BLE001 — reasoner must never break a recommendation
92
+ log.warning("reasoner_call_failed", model=self._model, error=str(exc))
93
+ return None
94
+
95
+ async def rank(
96
+ self,
97
+ *,
98
+ task: str,
99
+ task_type: str,
100
+ difficulty: str,
101
+ candidates: Sequence[CandidateView],
102
+ memory_block: str,
103
+ cost_quality_tradeoff: float,
104
+ ) -> ReasonerResult | None:
105
+ user = build_rank_user(
106
+ task=task,
107
+ task_type=task_type,
108
+ difficulty=difficulty,
109
+ candidates=candidates,
110
+ memory_block=memory_block,
111
+ cost_quality_tradeoff=cost_quality_tradeoff,
112
+ )
113
+ data = await self._json_call(system=RANK_SYSTEM, user=user, schema=_GEMINI_RANKING_SCHEMA)
114
+ if data is None:
115
+ return None
116
+ return parse_ranking(data, {c.model_id for c in candidates})
117
+
118
+ async def classify(self, *, task: str) -> tuple[TaskType, Difficulty] | None:
119
+ data = await self._json_call(
120
+ system=CLASSIFY_SYSTEM, user=task[:2000], schema=_GEMINI_CLASSIFY_SCHEMA
121
+ )
122
+ if data is None:
123
+ return None
124
+ return parse_classification(data)
minima/llm/registry.py ADDED
@@ -0,0 +1,54 @@
1
+ """Build a reasoner from settings, or None when disabled/unconfigured."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from minima.config import Settings
6
+ from minima.llm.base import Reasoner
7
+ from minima.logging import get_logger
8
+
9
+ log = get_logger("minima.llm")
10
+
11
+
12
+ def build_reasoner(settings: Settings) -> Reasoner | None:
13
+ provider = settings.minima_reasoner_provider.lower().strip()
14
+ if provider in ("", "none"):
15
+ return None
16
+
17
+ if provider == "anthropic":
18
+ if not settings.anthropic_api_key:
19
+ log.warning("reasoner_disabled_no_key", provider="anthropic")
20
+ return None
21
+ # The provider SDK is imported lazily (here and inside the constructor), so
22
+ # catch ImportError across both: a missing extra must degrade, not crash startup.
23
+ try:
24
+ from minima.llm.anthropic import DEFAULT_MODEL, AnthropicReasoner
25
+
26
+ return AnthropicReasoner(
27
+ model=settings.minima_reasoner_model or DEFAULT_MODEL,
28
+ api_key=settings.anthropic_api_key,
29
+ timeout_ms=settings.minima_reasoner_timeout_ms,
30
+ max_tokens=settings.minima_reasoner_max_tokens,
31
+ )
32
+ except ImportError:
33
+ log.warning("reasoner_extra_missing", provider="anthropic", extra="reasoner-anthropic")
34
+ return None
35
+
36
+ if provider == "gemini":
37
+ if not settings.gemini_api_key:
38
+ log.warning("reasoner_disabled_no_key", provider="gemini")
39
+ return None
40
+ try:
41
+ from minima.llm.gemini import DEFAULT_MODEL, GeminiReasoner
42
+
43
+ return GeminiReasoner(
44
+ model=settings.minima_reasoner_model or DEFAULT_MODEL,
45
+ api_key=settings.gemini_api_key,
46
+ timeout_ms=settings.minima_reasoner_timeout_ms,
47
+ max_tokens=settings.minima_reasoner_max_tokens,
48
+ )
49
+ except ImportError:
50
+ log.warning("reasoner_extra_missing", provider="gemini", extra="reasoner-gemini")
51
+ return None
52
+
53
+ log.warning("reasoner_unknown_provider", provider=provider)
54
+ return None
minima/logging.py ADDED
@@ -0,0 +1,28 @@
1
+ """Structured logging setup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ import structlog
8
+
9
+
10
+ def configure_logging(level: str = "info") -> None:
11
+ log_level = getattr(logging, level.upper(), logging.INFO)
12
+ logging.basicConfig(format="%(message)s", level=log_level)
13
+ structlog.configure(
14
+ wrapper_class=structlog.make_filtering_bound_logger(log_level),
15
+ processors=[
16
+ structlog.contextvars.merge_contextvars,
17
+ structlog.processors.add_log_level,
18
+ structlog.processors.TimeStamper(fmt="iso"),
19
+ structlog.processors.StackInfoRenderer(),
20
+ structlog.processors.format_exc_info,
21
+ structlog.processors.JSONRenderer(),
22
+ ],
23
+ cache_logger_on_first_use=True,
24
+ )
25
+
26
+
27
+ def get_logger(name: str = "minima") -> structlog.stdlib.BoundLogger:
28
+ return structlog.get_logger(name)
minima/main.py ADDED
@@ -0,0 +1,109 @@
1
+ """FastAPI application factory and lifespan wiring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextlib
7
+ from collections.abc import AsyncIterator
8
+
9
+ from fastapi import FastAPI
10
+
11
+ from minima.api.errors import register_error_handlers
12
+ from minima.api.routers import (
13
+ calibration,
14
+ feedback,
15
+ health,
16
+ models,
17
+ recommend,
18
+ savings,
19
+ strategies,
20
+ )
21
+ from minima.catalog.refresh import refresh_loop
22
+ from minima.catalog.store import CatalogStore
23
+ from minima.config import Settings, get_settings
24
+ from minima.llm.registry import build_reasoner
25
+ from minima.logging import configure_logging
26
+ from minima.memory.adapter import Memory
27
+ from minima.recommender.decisionlog import build_decision_log
28
+ from minima.recommender.durablerefs import build_durable_refs
29
+ from minima.recommender.engine import Recommender
30
+ from minima.recommender.propensity import build_propensity
31
+ from minima.recommender.recstore import LaneCounter, RecStore, build_recstore
32
+ from minima.tenancy.passthrough import PassthroughRuntime
33
+ from minima.version import __version__
34
+
35
+
36
+ @contextlib.asynccontextmanager
37
+ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
38
+ settings: Settings = app.state.settings
39
+ configure_logging(settings.minima_log_level)
40
+
41
+ injected: dict = getattr(app.state, "_injected", {})
42
+ catalog_store: CatalogStore = injected.get("catalog_store") or CatalogStore(settings)
43
+ recstore_backend: RecStore = injected.get("recstore") or build_recstore(settings)
44
+ propensity_backend = build_propensity(settings)
45
+ decision_log_backend = build_decision_log(settings)
46
+ reasoner = build_reasoner(settings)
47
+ lane_counter = LaneCounter()
48
+
49
+ app.state.catalog_store = catalog_store
50
+ app.state.lane_counter = lane_counter
51
+ injected_memory: Memory | None = injected.get("memory")
52
+ app.state.passthrough_runtime = injected.get("passthrough_runtime") or PassthroughRuntime(
53
+ settings=settings,
54
+ catalog_store=catalog_store,
55
+ reasoner=reasoner,
56
+ recstore_backend=recstore_backend,
57
+ propensity_backend=propensity_backend,
58
+ lane_counter=lane_counter,
59
+ memory_factory=(lambda _key: injected_memory) if injected_memory is not None else None,
60
+ decision_log_backend=decision_log_backend,
61
+ durable_refs_backend=build_durable_refs(settings),
62
+ )
63
+
64
+ refresh_task: asyncio.Task | None = None
65
+ if getattr(app.state, "_start_refresh", True):
66
+ refresh_task = asyncio.create_task(refresh_loop(settings, catalog_store))
67
+
68
+ try:
69
+ yield
70
+ finally:
71
+ if refresh_task is not None:
72
+ refresh_task.cancel()
73
+ with contextlib.suppress(asyncio.CancelledError):
74
+ await refresh_task
75
+
76
+
77
+ def create_app(
78
+ *,
79
+ settings: Settings | None = None,
80
+ memory: Memory | None = None,
81
+ catalog_store: CatalogStore | None = None,
82
+ recstore: RecStore | None = None,
83
+ recommender: Recommender | None = None,
84
+ passthrough_runtime: PassthroughRuntime | None = None,
85
+ start_refresh: bool = True,
86
+ ) -> FastAPI:
87
+ app = FastAPI(title="Minima", version=__version__, lifespan=lifespan)
88
+ app.state.settings = settings or get_settings()
89
+ app.state._injected = {
90
+ "memory": memory,
91
+ "catalog_store": catalog_store,
92
+ "recstore": recstore,
93
+ "recommender": recommender,
94
+ "passthrough_runtime": passthrough_runtime,
95
+ }
96
+ app.state._start_refresh = start_refresh
97
+
98
+ register_error_handlers(app)
99
+ app.include_router(recommend.router)
100
+ app.include_router(feedback.router)
101
+ app.include_router(models.router)
102
+ app.include_router(strategies.router)
103
+ app.include_router(savings.router)
104
+ app.include_router(calibration.router)
105
+ app.include_router(health.router)
106
+ return app
107
+
108
+
109
+ app = create_app()
@@ -0,0 +1 @@
1
+ """Mubit memory integration — the only place the Mubit SDK is touched."""