minder-cli 0.2.5__tar.gz → 0.2.7__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.
Files changed (134) hide show
  1. {minder_cli-0.2.5 → minder_cli-0.2.7}/PKG-INFO +10 -9
  2. {minder_cli-0.2.5 → minder_cli-0.2.7}/pyproject.toml +12 -11
  3. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/config.py +6 -6
  4. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/continuity.py +3 -3
  5. minder_cli-0.2.7/src/minder/embedding/local.py +76 -0
  6. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/graph.py +4 -3
  7. minder_cli-0.2.7/src/minder/llm/local.py +233 -0
  8. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/http/admin/memories.py +2 -1
  9. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/http/admin/search.py +4 -2
  10. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/prompts/formatter.py +3 -3
  11. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/server.py +10 -6
  12. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/milvus/client.py +1 -1
  13. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/milvus/collections.py +1 -1
  14. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/tools/memory.py +2 -1
  15. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/tools/query.py +4 -3
  16. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/tools/skills.py +2 -1
  17. minder_cli-0.2.5/src/minder/embedding/local.py +0 -65
  18. minder_cli-0.2.5/src/minder/llm/local.py +0 -381
  19. {minder_cli-0.2.5 → minder_cli-0.2.7}/.gitignore +0 -0
  20. {minder_cli-0.2.5 → minder_cli-0.2.7}/LICENSE +0 -0
  21. {minder_cli-0.2.5 → minder_cli-0.2.7}/README.md +0 -0
  22. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/__init__.py +0 -0
  23. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/api/routers/prompts.py +0 -0
  24. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/application/__init__.py +0 -0
  25. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/application/admin/__init__.py +0 -0
  26. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/application/admin/dto.py +0 -0
  27. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/application/admin/jobs.py +0 -0
  28. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/application/admin/use_cases.py +0 -0
  29. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/auth/__init__.py +0 -0
  30. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/auth/context.py +0 -0
  31. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/auth/middleware.py +0 -0
  32. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/auth/principal.py +0 -0
  33. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/auth/rate_limiter.py +0 -0
  34. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/auth/rbac.py +0 -0
  35. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/auth/service.py +0 -0
  36. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/bootstrap/__init__.py +0 -0
  37. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/bootstrap/providers.py +0 -0
  38. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/bootstrap/transport.py +0 -0
  39. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/cache/__init__.py +0 -0
  40. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/cache/providers.py +0 -0
  41. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/chunking/__init__.py +0 -0
  42. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/chunking/code_splitter.py +0 -0
  43. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/chunking/splitter.py +0 -0
  44. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/cli.py +0 -0
  45. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/dev.py +0 -0
  46. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/embedding/__init__.py +0 -0
  47. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/embedding/base.py +0 -0
  48. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/embedding/openai.py +0 -0
  49. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/__init__.py +0 -0
  50. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/edges.py +0 -0
  51. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/executor.py +0 -0
  52. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/nodes/__init__.py +0 -0
  53. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/nodes/evaluator.py +0 -0
  54. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/nodes/guard.py +0 -0
  55. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/nodes/llm.py +0 -0
  56. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/nodes/planning.py +0 -0
  57. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/nodes/reasoning.py +0 -0
  58. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/nodes/reranker.py +0 -0
  59. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/nodes/retriever.py +0 -0
  60. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/nodes/verification.py +0 -0
  61. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/nodes/workflow_planner.py +0 -0
  62. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/runtime.py +0 -0
  63. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/graph/state.py +0 -0
  64. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/llm/__init__.py +0 -0
  65. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/llm/base.py +0 -0
  66. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/llm/openai.py +0 -0
  67. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/__init__.py +0 -0
  68. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/base.py +0 -0
  69. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/client.py +0 -0
  70. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/document.py +0 -0
  71. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/error.py +0 -0
  72. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/graph.py +0 -0
  73. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/history.py +0 -0
  74. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/job.py +0 -0
  75. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/prompt.py +0 -0
  76. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/repository.py +0 -0
  77. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/rule.py +0 -0
  78. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/session.py +0 -0
  79. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/skill.py +0 -0
  80. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/user.py +0 -0
  81. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/models/workflow.py +0 -0
  82. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/observability/__init__.py +0 -0
  83. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/observability/audit.py +0 -0
  84. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/observability/logging.py +0 -0
  85. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/observability/metrics.py +0 -0
  86. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/observability/tracing.py +0 -0
  87. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/__init__.py +0 -0
  88. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/http/__init__.py +0 -0
  89. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/http/admin/__init__.py +0 -0
  90. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/http/admin/api.py +0 -0
  91. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/http/admin/context.py +0 -0
  92. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/http/admin/dashboard.py +0 -0
  93. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/http/admin/jobs.py +0 -0
  94. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/http/admin/prompts.py +0 -0
  95. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/http/admin/routes.py +0 -0
  96. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/http/admin/runtime.py +0 -0
  97. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/presentation/http/admin/skills.py +0 -0
  98. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/prompts/__init__.py +0 -0
  99. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/resources/__init__.py +0 -0
  100. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/retrieval/__init__.py +0 -0
  101. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/retrieval/hybrid.py +0 -0
  102. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/retrieval/mmr.py +0 -0
  103. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/retrieval/multi_hop.py +0 -0
  104. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/runtime.py +0 -0
  105. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/__init__.py +0 -0
  106. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/document.py +0 -0
  107. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/error.py +0 -0
  108. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/feedback.py +0 -0
  109. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/graph.py +0 -0
  110. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/history.py +0 -0
  111. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/interfaces.py +0 -0
  112. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/milvus/__init__.py +0 -0
  113. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/milvus/vector_store.py +0 -0
  114. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/mongodb/__init__.py +0 -0
  115. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/mongodb/client.py +0 -0
  116. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/mongodb/indexes.py +0 -0
  117. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/mongodb/operational_store.py +0 -0
  118. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/relational.py +0 -0
  119. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/repo_state.py +0 -0
  120. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/rule.py +0 -0
  121. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/store/vector.py +0 -0
  122. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/tools/__init__.py +0 -0
  123. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/tools/auth.py +0 -0
  124. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/tools/graph.py +0 -0
  125. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/tools/ingest.py +0 -0
  126. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/tools/registry.py +0 -0
  127. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/tools/repo_scanner.py +0 -0
  128. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/tools/search.py +0 -0
  129. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/tools/session.py +0 -0
  130. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/tools/workflow.py +0 -0
  131. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/transport/__init__.py +0 -0
  132. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/transport/base.py +0 -0
  133. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/transport/sse.py +0 -0
  134. {minder_cli-0.2.5 → minder_cli-0.2.7}/src/minder/transport/stdio.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: minder-cli
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: Minder CLI is the command-line interface for the Minder self-hosted MCP platform.
5
5
  Project-URL: Homepage, https://github.com/hiimtrung/minder
6
6
  Project-URL: Repository, https://github.com/hiimtrung/minder
@@ -12,16 +12,15 @@ Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: Apache Software License
14
14
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
- Requires-Python: >=3.13
17
+ Requires-Python: >=3.14
18
18
  Requires-Dist: fastapi>=0.136.0
19
- Requires-Dist: httpx>=0.27.0
19
+ Requires-Dist: httpx>=0.28.0
20
20
  Provides-Extra: server
21
21
  Requires-Dist: aiosqlite>=0.21.0; extra == 'server'
22
- Requires-Dist: langgraph>=1.1.4; extra == 'server'
22
+ Requires-Dist: langgraph>=1.1.8; extra == 'server'
23
23
  Requires-Dist: litellm>=1.83.1; extra == 'server'
24
- Requires-Dist: llama-cpp-python>=0.3.20; extra == 'server'
25
24
  Requires-Dist: mcp>=1.26.0; extra == 'server'
26
25
  Requires-Dist: motor>=3.7.0; extra == 'server'
27
26
  Requires-Dist: passlib[bcrypt]>=1.7.4; extra == 'server'
@@ -29,9 +28,11 @@ Requires-Dist: prometheus-client>=0.24.1; extra == 'server'
29
28
  Requires-Dist: pydantic-settings[toml]>=2.13.1; extra == 'server'
30
29
  Requires-Dist: pydantic>=2.12.5; extra == 'server'
31
30
  Requires-Dist: pyjwt>=2.12.1; extra == 'server'
32
- Requires-Dist: pymilvus>=2.5.0; extra == 'server'
33
- Requires-Dist: redis[hiredis]>=6.0.0; extra == 'server'
34
- Requires-Dist: sqlalchemy[asyncio]>=2.0.48; extra == 'server'
31
+ Requires-Dist: pymilvus>=2.6.12; extra == 'server'
32
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.49; extra == 'server'
33
+ Requires-Dist: yarl>=1.16.0; extra == 'server'
34
+ Requires-Dist: zipp>=3.21.0; extra == 'server'
35
+ Requires-Dist: zstandard>=0.25.0; extra == 'server'
35
36
  Description-Content-Type: text/markdown
36
37
 
37
38
  # Minder CLI
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "minder-cli"
7
- version = "0.2.5"
7
+ version = "0.2.7"
8
8
  description = "Minder CLI is the command-line interface for the Minder self-hosted MCP platform."
9
9
  readme = "README.md"
10
- requires-python = ">=3.13"
10
+ requires-python = ">=3.14"
11
11
  keywords = ["mcp", "cli", "ai", "code-search", "workflow"]
12
12
  classifiers = [
13
13
  "Development Status :: 3 - Alpha",
@@ -15,20 +15,19 @@ classifiers = [
15
15
  "Intended Audience :: Developers",
16
16
  "License :: OSI Approved :: Apache Software License",
17
17
  "Programming Language :: Python :: 3",
18
- "Programming Language :: Python :: 3.13",
18
+ "Programming Language :: Python :: 3.14",
19
19
  "Topic :: Software Development :: Libraries :: Python Modules",
20
20
  ]
21
21
  dependencies = [
22
22
  "fastapi>=0.136.0",
23
- "httpx>=0.27.0",
23
+ "httpx>=0.28.0",
24
24
  ]
25
25
 
26
26
  [project.optional-dependencies]
27
27
  server = [
28
28
  "aiosqlite>=0.21.0",
29
- "langgraph>=1.1.4",
29
+ "langgraph>=1.1.8",
30
30
  "litellm>=1.83.1",
31
- "llama-cpp-python>=0.3.20",
32
31
  "mcp>=1.26.0",
33
32
  "motor>=3.7.0",
34
33
  "passlib[bcrypt]>=1.7.4",
@@ -36,9 +35,11 @@ server = [
36
35
  "pydantic>=2.12.5",
37
36
  "pydantic-settings[toml]>=2.13.1",
38
37
  "pyjwt>=2.12.1",
39
- "pymilvus>=2.5.0",
40
- "redis[hiredis]>=6.0.0",
41
- "sqlalchemy[asyncio]>=2.0.48",
38
+ "pymilvus>=2.6.12",
39
+ "sqlalchemy[asyncio]>=2.0.49",
40
+ "yarl>=1.16.0",
41
+ "zipp>=3.21.0",
42
+ "zstandard>=0.25.0",
42
43
  ]
43
44
 
44
45
  [project.urls]
@@ -51,8 +52,8 @@ minder = "minder.cli:main"
51
52
 
52
53
  [dependency-groups]
53
54
  dev = [
54
- "fakeredis[lua]>=2.26.0",
55
- "mypy>=1.20.0",
55
+ "fakeredis[lua]>=2.27.0",
56
+ "mypy>=1.20.1",
56
57
  "pytest>=9.0.2",
57
58
  "pytest-asyncio>=1.3.0",
58
59
  "pytest-timeout>=2.3.1",
@@ -31,18 +31,18 @@ class AuthConfig(BaseModel):
31
31
 
32
32
 
33
33
  class EmbeddingConfig(BaseModel):
34
- provider: str = "llamacpp"
35
- model_name: str = "ggml-org/embeddinggemma-300M-GGUF"
36
- model_path: str = "~/.minder/models/embeddinggemma-300M-Q8_0.gguf"
34
+ provider: str = "ollama"
35
+ ollama_url: str = "http://localhost:11434"
36
+ ollama_model: str = "embeddinggemma"
37
37
  dimensions: int = 768
38
38
  openai_api_key: Optional[str] = None
39
39
  openai_model: str = "text-embedding-3-small"
40
40
 
41
41
 
42
42
  class LLMConfig(BaseModel):
43
- provider: str = "llamacpp"
44
- model_name: str = "ggml-org/gemma-4-E2B-it-GGUF"
45
- model_path: str = "~/.minder/models/gemma-4-e2b-it-Q8_0.gguf"
43
+ provider: str = "ollama"
44
+ ollama_url: str = "http://localhost:11434"
45
+ ollama_model: str = "gemma3:4b"
46
46
  context_length: int = 131072
47
47
  temperature: float = 0.1
48
48
  openai_api_key: Optional[str] = None
@@ -272,8 +272,8 @@ class ContinuitySynthesizer:
272
272
 
273
273
  self._config = config
274
274
  self._llm = LocalModelLLM(
275
- config.llm.model_path,
276
- runtime="auto",
275
+ ollama_url=config.llm.ollama_url,
276
+ ollama_model=config.llm.ollama_model,
277
277
  context_length=config.llm.context_length,
278
278
  )
279
279
 
@@ -312,7 +312,7 @@ class ContinuitySynthesizer:
312
312
  if not parsed:
313
313
  return fallback, {
314
314
  "provider": "heuristic",
315
- "model": self._config.llm.model_name,
315
+ "model": self._config.llm.ollama_model,
316
316
  "runtime": self._llm.runtime,
317
317
  }
318
318
  return {
@@ -0,0 +1,76 @@
1
+ """
2
+ Local Embedding provider — delegates to Ollama HTTP API.
3
+
4
+ Falls back to a deterministic hash-based stub when Ollama is unreachable.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import logging
12
+ from urllib.request import Request, urlopen
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class LocalEmbeddingProvider:
18
+ def __init__(
19
+ self,
20
+ ollama_url: str = "http://localhost:11434",
21
+ ollama_model: str = "embeddinggemma",
22
+ dimensions: int = 768,
23
+ runtime: str = "auto",
24
+ ) -> None:
25
+ self._ollama_url = ollama_url.rstrip("/")
26
+ self._ollama_model = ollama_model
27
+ self._dimensions = dimensions
28
+ self._runtime = runtime
29
+
30
+ @property
31
+ def runtime(self) -> str:
32
+ runtime = self._runtime
33
+ if runtime == "auto":
34
+ try:
35
+ req = Request(f"{self._ollama_url}/api/tags", method="GET")
36
+ with urlopen(req, timeout=3):
37
+ return "ollama"
38
+ except Exception:
39
+ return "mock"
40
+ return runtime
41
+
42
+ def embed(self, text: str) -> list[float]:
43
+ if self.runtime == "ollama":
44
+ embedded = self._embed_with_ollama(text)
45
+ if embedded is not None:
46
+ return embedded[: self._dimensions]
47
+ # Deterministic hash-based fallback
48
+ digest = hashlib.sha256(text.encode("utf-8")).digest()
49
+ values: list[float] = []
50
+ for index in range(self._dimensions):
51
+ byte = digest[index % len(digest)]
52
+ values.append(round(byte / 255.0, 6))
53
+ return values
54
+
55
+ def _embed_with_ollama(self, text: str) -> list[float] | None:
56
+ payload = {
57
+ "model": self._ollama_model,
58
+ "input": text,
59
+ }
60
+ data = json.dumps(payload).encode()
61
+ req = Request(
62
+ f"{self._ollama_url}/api/embed",
63
+ data=data,
64
+ headers={"Content-Type": "application/json"},
65
+ method="POST",
66
+ )
67
+ try:
68
+ with urlopen(req, timeout=30) as resp:
69
+ body = json.loads(resp.read())
70
+ embeddings = body.get("embeddings", [])
71
+ if embeddings and isinstance(embeddings[0], list):
72
+ return [float(v) for v in embeddings[0]]
73
+ return None
74
+ except Exception:
75
+ logger.warning("Ollama embedding failed, using hash fallback")
76
+ return None
@@ -58,7 +58,8 @@ class MinderGraph:
58
58
  self._planning = planning or PlanningNode()
59
59
  vector_store = VectorStore(store, store)
60
60
  embedder = LocalEmbeddingProvider(
61
- config.embedding.model_path,
61
+ ollama_url=config.embedding.ollama_url,
62
+ ollama_model=config.embedding.ollama_model,
62
63
  dimensions=config.embedding.dimensions,
63
64
  runtime="auto",
64
65
  )
@@ -72,8 +73,8 @@ class MinderGraph:
72
73
  self._reasoning = reasoning or ReasoningNode()
73
74
  self._llm = llm or LLMNode(
74
75
  primary=LocalModelLLM(
75
- config.llm.model_path,
76
- runtime="auto",
76
+ ollama_url=config.llm.ollama_url,
77
+ ollama_model=config.llm.ollama_model,
77
78
  context_length=config.llm.context_length,
78
79
  ),
79
80
  fallback=OpenAIFallbackLLM(
@@ -0,0 +1,233 @@
1
+ """
2
+ Local LLM provider — delegates inference to an Ollama HTTP server.
3
+
4
+ Ollama must be running and accessible at the configured URL (default
5
+ ``http://localhost:11434``). The provider gracefully falls back to a
6
+ deterministic text stub when Ollama is unreachable so that the rest of
7
+ the Minder pipeline can still operate.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ from collections.abc import Generator
15
+ from urllib.request import Request, urlopen
16
+
17
+ from minder.graph.state import GraphState
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class LocalModelLLM:
23
+
24
+ def __init__(
25
+ self,
26
+ ollama_url: str = "http://localhost:11434",
27
+ ollama_model: str = "gemma3:4b",
28
+ fail: bool = False,
29
+ context_length: int = 131072,
30
+ ) -> None:
31
+ self._ollama_url = ollama_url.rstrip("/")
32
+ self._ollama_model = ollama_model
33
+ self._fail = fail
34
+ self._context_length = max(512, context_length)
35
+
36
+ @property
37
+ def runtime(self) -> str:
38
+ if self._fail:
39
+ return "mock"
40
+ try:
41
+ req = Request(f"{self._ollama_url}/api/tags", method="GET")
42
+ with urlopen(req, timeout=3):
43
+ return "ollama"
44
+ except Exception:
45
+ return "mock"
46
+
47
+ # ------------------------------------------------------------------
48
+ # Public API
49
+ # ------------------------------------------------------------------
50
+
51
+ def generate(self, state: GraphState) -> dict[str, object]:
52
+ if self._fail:
53
+ raise RuntimeError("Local model unavailable")
54
+
55
+ runtime = self.runtime
56
+ source_paths = [doc["path"] for doc in state.reranked_docs[:3]]
57
+ guidance = state.workflow_context.get("guidance", "")
58
+ fallback = (
59
+ f"{guidance}\n"
60
+ f"Plan intent: {state.plan.get('intent', 'unknown')}.\n"
61
+ f"Answer: grounded response for '{state.query}'.\n"
62
+ f"Sources: {', '.join(source_paths) if source_paths else 'none'}."
63
+ )
64
+
65
+ text = fallback
66
+ if runtime == "ollama":
67
+ text = self._generate_with_ollama(state, fallback=fallback)
68
+
69
+ return {
70
+ "text": text,
71
+ "sources": source_paths,
72
+ "provider": "local_llm",
73
+ "model": self._ollama_model,
74
+ "runtime": runtime,
75
+ "stream": [line for line in text.splitlines() if line],
76
+ }
77
+
78
+ def stream_generate(
79
+ self, state: GraphState
80
+ ) -> Generator[dict[str, object], None, None]:
81
+ if self._fail:
82
+ raise RuntimeError("Local model unavailable")
83
+
84
+ runtime = self.runtime
85
+ source_paths = [doc["path"] for doc in state.reranked_docs[:3]]
86
+ guidance = state.workflow_context.get("guidance", "")
87
+ fallback = (
88
+ f"{guidance}\n"
89
+ f"Plan intent: {state.plan.get('intent', 'unknown')}.\n"
90
+ f"Answer: grounded response for '{state.query}'.\n"
91
+ f"Sources: {', '.join(source_paths) if source_paths else 'none'}."
92
+ )
93
+
94
+ if runtime != "ollama":
95
+ if fallback:
96
+ yield {"type": "chunk", "delta": fallback}
97
+ yield {
98
+ "type": "result",
99
+ "result": self._build_result(fallback, source_paths, runtime),
100
+ }
101
+ return
102
+
103
+ deltas: list[str] = []
104
+ try:
105
+ for delta in self._stream_ollama(str(state.query)):
106
+ deltas.append(delta)
107
+ yield {"type": "chunk", "delta": delta}
108
+ except Exception:
109
+ logger.warning("Ollama stream failed, using fallback")
110
+ if fallback:
111
+ yield {"type": "chunk", "delta": fallback}
112
+ deltas = [fallback] if fallback else []
113
+
114
+ text = "".join(deltas).strip() or fallback
115
+ yield {
116
+ "type": "result",
117
+ "result": self._build_result(text, source_paths, runtime, deltas),
118
+ }
119
+
120
+ def complete_text(
121
+ self,
122
+ prompt: str,
123
+ *,
124
+ max_tokens: int = 512,
125
+ temperature: float = 0.1,
126
+ fallback: str = "",
127
+ ) -> str:
128
+ if self._fail:
129
+ raise RuntimeError("Local model unavailable")
130
+
131
+ if self.runtime != "ollama":
132
+ return fallback
133
+
134
+ try:
135
+ return self._chat_ollama(prompt, max_tokens=max_tokens, temperature=temperature)
136
+ except Exception:
137
+ logger.warning("Ollama completion failed, using fallback")
138
+ return fallback
139
+
140
+ # ------------------------------------------------------------------
141
+ # Ollama HTTP helpers
142
+ # ------------------------------------------------------------------
143
+
144
+ def _chat_ollama(
145
+ self,
146
+ prompt: str,
147
+ *,
148
+ max_tokens: int = 512,
149
+ temperature: float = 0.1,
150
+ ) -> str:
151
+ payload = {
152
+ "model": self._ollama_model,
153
+ "messages": [{"role": "user", "content": prompt}],
154
+ "stream": False,
155
+ "options": {
156
+ "temperature": temperature,
157
+ "num_predict": max_tokens,
158
+ "num_ctx": self._context_length,
159
+ },
160
+ }
161
+ data = json.dumps(payload).encode()
162
+ req = Request(
163
+ f"{self._ollama_url}/api/chat",
164
+ data=data,
165
+ headers={"Content-Type": "application/json"},
166
+ method="POST",
167
+ )
168
+ with urlopen(req, timeout=120) as resp:
169
+ body = json.loads(resp.read())
170
+ return body.get("message", {}).get("content", "").strip()
171
+
172
+ def _stream_ollama(
173
+ self,
174
+ prompt: str,
175
+ *,
176
+ max_tokens: int = 256,
177
+ temperature: float = 0.1,
178
+ ) -> Generator[str, None, None]:
179
+ payload = {
180
+ "model": self._ollama_model,
181
+ "messages": [{"role": "user", "content": prompt}],
182
+ "stream": True,
183
+ "options": {
184
+ "temperature": temperature,
185
+ "num_predict": max_tokens,
186
+ "num_ctx": self._context_length,
187
+ },
188
+ }
189
+ data = json.dumps(payload).encode()
190
+ req = Request(
191
+ f"{self._ollama_url}/api/chat",
192
+ data=data,
193
+ headers={"Content-Type": "application/json"},
194
+ method="POST",
195
+ )
196
+ with urlopen(req, timeout=120) as resp:
197
+ for line in resp:
198
+ if not line:
199
+ continue
200
+ chunk = json.loads(line)
201
+ content = chunk.get("message", {}).get("content", "")
202
+ if content:
203
+ yield content
204
+
205
+ def _generate_with_ollama(self, state: GraphState, *, fallback: str) -> str:
206
+ reasoning_output = getattr(state, "reasoning_output", {}) or {}
207
+ prompt = reasoning_output.get("prompt") or state.query
208
+ return self.complete_text(
209
+ str(prompt),
210
+ max_tokens=256,
211
+ temperature=0.1,
212
+ fallback=fallback,
213
+ )
214
+
215
+ # ------------------------------------------------------------------
216
+ # Helpers
217
+ # ------------------------------------------------------------------
218
+
219
+ def _build_result(
220
+ self,
221
+ text: str,
222
+ source_paths: list[str],
223
+ runtime: str,
224
+ stream: list[str] | None = None,
225
+ ) -> dict[str, object]:
226
+ return {
227
+ "text": text,
228
+ "sources": source_paths,
229
+ "provider": "local_llm",
230
+ "model": self._ollama_model,
231
+ "runtime": runtime,
232
+ "stream": stream if stream else ([text] if text else []),
233
+ }
@@ -140,7 +140,8 @@ def build_memories_routes(context: AdminRouteContext) -> list[BaseRoute]:
140
140
  if "title" in update_data or "content" in update_data:
141
141
  config = _config_from_request(request)
142
142
  embedder = LocalEmbeddingProvider(
143
- config.embedding.model_path,
143
+ ollama_url=config.embedding.ollama_url,
144
+ ollama_model=config.embedding.ollama_model,
144
145
  dimensions=min(config.embedding.dimensions, 16),
145
146
  runtime="auto",
146
147
  )
@@ -73,7 +73,8 @@ def _rank_serialized_items(
73
73
  return items
74
74
 
75
75
  embedder = LocalEmbeddingProvider(
76
- config.embedding.model_path,
76
+ ollama_url=config.embedding.ollama_url,
77
+ ollama_model=config.embedding.ollama_model,
77
78
  dimensions=min(config.embedding.dimensions, 16),
78
79
  runtime="auto",
79
80
  )
@@ -108,7 +109,8 @@ def _rank_skill_items(
108
109
  return items
109
110
 
110
111
  embedder = LocalEmbeddingProvider(
111
- config.embedding.model_path,
112
+ ollama_url=config.embedding.ollama_url,
113
+ ollama_model=config.embedding.ollama_model,
112
114
  dimensions=min(config.embedding.dimensions, 16),
113
115
  runtime="auto",
114
116
  )
@@ -89,8 +89,8 @@ def polish_prompt_draft(
89
89
 
90
90
  polished = _heuristic_polish(draft)
91
91
  llm = LocalModelLLM(
92
- config.llm.model_path,
93
- runtime="auto",
92
+ ollama_url=config.llm.ollama_url,
93
+ ollama_model=config.llm.ollama_model,
94
94
  context_length=config.llm.context_length,
95
95
  )
96
96
  runtime = llm.runtime
@@ -118,7 +118,7 @@ Make the prompt direct, structured, and useful for a coding workflow.
118
118
  if not parsed:
119
119
  return polished, {
120
120
  "provider": "heuristic",
121
- "model": config.llm.model_name,
121
+ "model": config.llm.ollama_model,
122
122
  "runtime": runtime,
123
123
  }
124
124
 
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import sys
5
- from pathlib import Path
6
5
 
7
6
  from minder.bootstrap.providers import (
8
7
  build_cache,
@@ -33,12 +32,13 @@ __all__ = [
33
32
 
34
33
  def runtime_summary(config: Settings) -> dict[str, object]:
35
34
  llm = LocalModelLLM(
36
- config.llm.model_path,
37
- runtime="auto",
35
+ ollama_url=config.llm.ollama_url,
36
+ ollama_model=config.llm.ollama_model,
38
37
  context_length=config.llm.context_length,
39
38
  )
40
39
  embedder = LocalEmbeddingProvider(
41
- config.embedding.model_path,
40
+ ollama_url=config.embedding.ollama_url,
41
+ ollama_model=config.embedding.ollama_model,
42
42
  dimensions=config.embedding.dimensions,
43
43
  runtime="auto",
44
44
  )
@@ -53,10 +53,14 @@ def runtime_summary(config: Settings) -> dict[str, object]:
53
53
  "orchestration_runtime_effective": graph_runtime_name(
54
54
  config.workflow.orchestration_runtime
55
55
  ),
56
- "llm_model_path": str(Path(config.llm.model_path).expanduser()),
56
+ "llm_provider": config.llm.provider,
57
+ "llm_ollama_url": config.llm.ollama_url,
58
+ "llm_ollama_model": config.llm.ollama_model,
57
59
  "llm_runtime_effective": llm.runtime,
58
60
  "llm_context_length": config.llm.context_length,
59
- "embedding_model_path": str(Path(config.embedding.model_path).expanduser()),
61
+ "embedding_provider": config.embedding.provider,
62
+ "embedding_ollama_url": config.embedding.ollama_url,
63
+ "embedding_ollama_model": config.embedding.ollama_model,
60
64
  "embedding_runtime_effective": embedder.runtime,
61
65
  "openai_fallback_configured": fallback.available(),
62
66
  "openai_fallback_runtime_effective": fallback.runtime,
@@ -5,7 +5,7 @@ Milvus Client — connection wrapper.
5
5
  from __future__ import annotations
6
6
 
7
7
  import asyncio
8
- from pymilvus import MilvusClient as PyMilvusClient # type: ignore[import-untyped]
8
+ from pymilvus import MilvusClient as PyMilvusClient # type: ignore[import-not-found, import-untyped]
9
9
 
10
10
 
11
11
  class MilvusClient:
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Milvus Collections — schema definitions.
3
3
  """
4
- from pymilvus import CollectionSchema, DataType, FieldSchema # type: ignore[import-untyped]
4
+ from pymilvus import CollectionSchema, DataType, FieldSchema # type: ignore[import-not-found, import-untyped]
5
5
 
6
6
  def get_document_schema(dimensions: int = 768) -> CollectionSchema:
7
7
  fields = [
@@ -19,7 +19,8 @@ class MemoryTools:
19
19
  self._store = store
20
20
  self._config = config
21
21
  self._embedder = LocalEmbeddingProvider(
22
- config.embedding.model_path,
22
+ ollama_url=config.embedding.ollama_url,
23
+ ollama_model=config.embedding.ollama_model,
23
24
  dimensions=min(config.embedding.dimensions, 16),
24
25
  runtime="auto",
25
26
  )
@@ -35,7 +35,8 @@ class QueryTools:
35
35
  self._graph = graph or MinderGraph(store, config)
36
36
  self._vector_store = vector_store or VectorStore(store, store)
37
37
  self._embedding_provider = LocalEmbeddingProvider(
38
- config.embedding.model_path,
38
+ ollama_url=config.embedding.ollama_url,
39
+ ollama_model=config.embedding.ollama_model,
39
40
  dimensions=config.embedding.dimensions,
40
41
  runtime="auto",
41
42
  )
@@ -132,7 +133,7 @@ class QueryTools:
132
133
  "evaluation": result.evaluation,
133
134
  "provider": result.llm_output.get("provider"),
134
135
  "model": result.llm_output.get(
135
- "model", result.llm_output.get("model_path")
136
+ "model", result.llm_output.get("ollama_model")
136
137
  ),
137
138
  "runtime": result.llm_output.get("runtime"),
138
139
  "orchestration_runtime": result.metadata.get("orchestration_runtime"),
@@ -237,7 +238,7 @@ class QueryTools:
237
238
  "evaluation": result.evaluation,
238
239
  "provider": result.llm_output.get("provider"),
239
240
  "model": result.llm_output.get(
240
- "model", result.llm_output.get("model_path")
241
+ "model", result.llm_output.get("ollama_model")
241
242
  ),
242
243
  "runtime": result.llm_output.get("runtime"),
243
244
  "orchestration_runtime": result.metadata.get("orchestration_runtime"),
@@ -86,7 +86,8 @@ class SkillTools:
86
86
  def __init__(self, store: IOperationalStore, config: MinderConfig) -> None:
87
87
  self._store = store
88
88
  self._embedder = LocalEmbeddingProvider(
89
- config.embedding.model_path,
89
+ ollama_url=config.embedding.ollama_url,
90
+ ollama_model=config.embedding.ollama_model,
90
91
  dimensions=min(config.embedding.dimensions, 16),
91
92
  runtime="auto",
92
93
  )