minder-cli 0.2.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 (132) hide show
  1. minder/__init__.py +12 -0
  2. minder/api/routers/prompts.py +177 -0
  3. minder/application/__init__.py +1 -0
  4. minder/application/admin/__init__.py +11 -0
  5. minder/application/admin/dto.py +453 -0
  6. minder/application/admin/jobs.py +327 -0
  7. minder/application/admin/use_cases.py +1895 -0
  8. minder/auth/__init__.py +12 -0
  9. minder/auth/context.py +26 -0
  10. minder/auth/middleware.py +70 -0
  11. minder/auth/principal.py +59 -0
  12. minder/auth/rate_limiter.py +89 -0
  13. minder/auth/rbac.py +60 -0
  14. minder/auth/service.py +541 -0
  15. minder/bootstrap/__init__.py +9 -0
  16. minder/bootstrap/providers.py +109 -0
  17. minder/bootstrap/transport.py +807 -0
  18. minder/cache/__init__.py +10 -0
  19. minder/cache/providers.py +140 -0
  20. minder/chunking/__init__.py +4 -0
  21. minder/chunking/code_splitter.py +184 -0
  22. minder/chunking/splitter.py +136 -0
  23. minder/cli.py +1542 -0
  24. minder/config.py +179 -0
  25. minder/continuity.py +363 -0
  26. minder/dev.py +160 -0
  27. minder/embedding/__init__.py +9 -0
  28. minder/embedding/base.py +7 -0
  29. minder/embedding/local.py +65 -0
  30. minder/embedding/openai.py +7 -0
  31. minder/graph/__init__.py +11 -0
  32. minder/graph/edges.py +13 -0
  33. minder/graph/executor.py +127 -0
  34. minder/graph/graph.py +263 -0
  35. minder/graph/nodes/__init__.py +27 -0
  36. minder/graph/nodes/evaluator.py +21 -0
  37. minder/graph/nodes/guard.py +64 -0
  38. minder/graph/nodes/llm.py +59 -0
  39. minder/graph/nodes/planning.py +30 -0
  40. minder/graph/nodes/reasoning.py +87 -0
  41. minder/graph/nodes/reranker.py +141 -0
  42. minder/graph/nodes/retriever.py +86 -0
  43. minder/graph/nodes/verification.py +230 -0
  44. minder/graph/nodes/workflow_planner.py +250 -0
  45. minder/graph/runtime.py +15 -0
  46. minder/graph/state.py +26 -0
  47. minder/llm/__init__.py +5 -0
  48. minder/llm/base.py +14 -0
  49. minder/llm/local.py +381 -0
  50. minder/llm/openai.py +89 -0
  51. minder/models/__init__.py +109 -0
  52. minder/models/base.py +10 -0
  53. minder/models/client.py +137 -0
  54. minder/models/document.py +34 -0
  55. minder/models/error.py +32 -0
  56. minder/models/graph.py +114 -0
  57. minder/models/history.py +32 -0
  58. minder/models/job.py +62 -0
  59. minder/models/prompt.py +41 -0
  60. minder/models/repository.py +62 -0
  61. minder/models/rule.py +68 -0
  62. minder/models/session.py +51 -0
  63. minder/models/skill.py +52 -0
  64. minder/models/user.py +41 -0
  65. minder/models/workflow.py +35 -0
  66. minder/observability/__init__.py +57 -0
  67. minder/observability/audit.py +243 -0
  68. minder/observability/logging.py +253 -0
  69. minder/observability/metrics.py +448 -0
  70. minder/observability/tracing.py +215 -0
  71. minder/presentation/__init__.py +1 -0
  72. minder/presentation/http/__init__.py +1 -0
  73. minder/presentation/http/admin/__init__.py +3 -0
  74. minder/presentation/http/admin/api.py +1309 -0
  75. minder/presentation/http/admin/context.py +94 -0
  76. minder/presentation/http/admin/dashboard.py +111 -0
  77. minder/presentation/http/admin/jobs.py +208 -0
  78. minder/presentation/http/admin/memories.py +185 -0
  79. minder/presentation/http/admin/prompts.py +219 -0
  80. minder/presentation/http/admin/routes.py +127 -0
  81. minder/presentation/http/admin/runtime.py +650 -0
  82. minder/presentation/http/admin/search.py +368 -0
  83. minder/presentation/http/admin/skills.py +230 -0
  84. minder/prompts/__init__.py +646 -0
  85. minder/prompts/formatter.py +142 -0
  86. minder/resources/__init__.py +318 -0
  87. minder/retrieval/__init__.py +5 -0
  88. minder/retrieval/hybrid.py +178 -0
  89. minder/retrieval/mmr.py +116 -0
  90. minder/retrieval/multi_hop.py +115 -0
  91. minder/runtime.py +15 -0
  92. minder/server.py +145 -0
  93. minder/store/__init__.py +64 -0
  94. minder/store/document.py +115 -0
  95. minder/store/error.py +82 -0
  96. minder/store/feedback.py +114 -0
  97. minder/store/graph.py +588 -0
  98. minder/store/history.py +57 -0
  99. minder/store/interfaces.py +512 -0
  100. minder/store/milvus/__init__.py +11 -0
  101. minder/store/milvus/client.py +26 -0
  102. minder/store/milvus/collections.py +15 -0
  103. minder/store/milvus/vector_store.py +232 -0
  104. minder/store/mongodb/__init__.py +11 -0
  105. minder/store/mongodb/client.py +49 -0
  106. minder/store/mongodb/indexes.py +90 -0
  107. minder/store/mongodb/operational_store.py +993 -0
  108. minder/store/relational.py +1087 -0
  109. minder/store/repo_state.py +58 -0
  110. minder/store/rule.py +93 -0
  111. minder/store/vector.py +79 -0
  112. minder/tools/__init__.py +47 -0
  113. minder/tools/auth.py +94 -0
  114. minder/tools/graph.py +839 -0
  115. minder/tools/ingest.py +353 -0
  116. minder/tools/memory.py +381 -0
  117. minder/tools/query.py +307 -0
  118. minder/tools/registry.py +269 -0
  119. minder/tools/repo_scanner.py +1266 -0
  120. minder/tools/search.py +15 -0
  121. minder/tools/session.py +316 -0
  122. minder/tools/skills.py +899 -0
  123. minder/tools/workflow.py +215 -0
  124. minder/transport/__init__.py +4 -0
  125. minder/transport/base.py +286 -0
  126. minder/transport/sse.py +252 -0
  127. minder/transport/stdio.py +29 -0
  128. minder_cli-0.2.0.dist-info/METADATA +318 -0
  129. minder_cli-0.2.0.dist-info/RECORD +132 -0
  130. minder_cli-0.2.0.dist-info/WHEEL +4 -0
  131. minder_cli-0.2.0.dist-info/entry_points.txt +2 -0
  132. minder_cli-0.2.0.dist-info/licenses/LICENSE +201 -0
minder/llm/openai.py ADDED
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Generator
4
+ from typing import Any
5
+
6
+ from minder.graph.state import GraphState
7
+ from minder.runtime import load_attr, module_available
8
+
9
+
10
+ class OpenAIFallbackLLM:
11
+ def __init__(self, api_key: str | None, model: str, runtime: str = "mock") -> None:
12
+ self._api_key = api_key
13
+ self._model = model
14
+ self._runtime = runtime
15
+ self._completion_fn: Any | None = None
16
+
17
+ def available(self) -> bool:
18
+ return bool(self._api_key)
19
+
20
+ @property
21
+ def runtime(self) -> str:
22
+ if self._runtime == "auto":
23
+ return "litellm" if module_available("litellm") else "mock"
24
+ return self._runtime
25
+
26
+ def generate(self, state: GraphState) -> dict[str, object]:
27
+ if not self.available():
28
+ raise RuntimeError("OpenAI fallback is not configured")
29
+ text = (
30
+ f"{state.workflow_context.get('guidance', '')}\n"
31
+ f"Fallback answer for '{state.query}' using {self._model}."
32
+ )
33
+ if self.runtime == "litellm":
34
+ text = self._generate_with_litellm(state, fallback=text)
35
+ return {
36
+ "text": text,
37
+ "sources": [doc["path"] for doc in state.reranked_docs[:3]],
38
+ "provider": "openai_fallback",
39
+ "model": self._model,
40
+ "runtime": self.runtime,
41
+ "stream": [text],
42
+ }
43
+
44
+ def stream_generate(
45
+ self, state: GraphState
46
+ ) -> Generator[dict[str, object], None, None]:
47
+ result = self.generate(state)
48
+ text = str(result.get("text", ""))
49
+ if text:
50
+ yield {"type": "chunk", "delta": text}
51
+ yield {"type": "result", "result": result}
52
+
53
+ def _generate_with_litellm(self, state: GraphState, *, fallback: str) -> str:
54
+ completion = self._litellm_completion()
55
+ if completion is None:
56
+ return fallback
57
+ reasoning_output = getattr(state, "reasoning_output", {}) or {}
58
+ try:
59
+ response = completion(
60
+ model=self._model,
61
+ api_key=self._api_key,
62
+ messages=[
63
+ {
64
+ "role": "user",
65
+ "content": str(reasoning_output.get("prompt") or state.query),
66
+ }
67
+ ],
68
+ )
69
+ except Exception:
70
+ return fallback
71
+ choices = getattr(response, "choices", None)
72
+ if choices is None and isinstance(response, dict):
73
+ choices = response.get("choices", [])
74
+ if not choices:
75
+ return fallback
76
+ first = choices[0]
77
+ message = getattr(first, "message", None)
78
+ if message is None and isinstance(first, dict):
79
+ message = first.get("message", {})
80
+ content = getattr(message, "content", None)
81
+ if content is None and isinstance(message, dict):
82
+ content = message.get("content")
83
+ return str(content or fallback).strip() or fallback
84
+
85
+ def _litellm_completion(self) -> Any | None:
86
+ if self._completion_fn is not None:
87
+ return self._completion_fn
88
+ self._completion_fn = load_attr("litellm", "completion")
89
+ return self._completion_fn
@@ -0,0 +1,109 @@
1
+ # SQLAlchemy Base
2
+ from .base import Base as Base
3
+
4
+ # User
5
+ from .user import User as User, UserSchema as UserSchema
6
+
7
+ # Client/Auth Gateway
8
+ from .client import (
9
+ AuditLog as AuditLog,
10
+ AuditLogSchema as AuditLogSchema,
11
+ Client as Client,
12
+ ClientApiKey as ClientApiKey,
13
+ ClientApiKeySchema as ClientApiKeySchema,
14
+ ClientSchema as ClientSchema,
15
+ ClientSession as ClientSession,
16
+ ClientSessionSchema as ClientSessionSchema,
17
+ )
18
+
19
+ # Skill
20
+ from .skill import Skill as Skill, SkillSchema as SkillSchema
21
+
22
+ # Admin Jobs
23
+ from .job import AdminJob as AdminJob, AdminJobSchema as AdminJobSchema
24
+
25
+ # Session
26
+ from .session import Session as Session, SessionSchema as SessionSchema
27
+
28
+ # Workflow
29
+ from .workflow import Workflow as Workflow, WorkflowSchema as WorkflowSchema
30
+
31
+ # Repository
32
+ from .repository import (
33
+ Repository as Repository,
34
+ RepositorySchema as RepositorySchema,
35
+ RepositoryWorkflowState as RepositoryWorkflowState,
36
+ RepositoryWorkflowStateSchema as RepositoryWorkflowStateSchema,
37
+ )
38
+
39
+ # History
40
+ from .history import History as History, HistorySchema as HistorySchema
41
+
42
+ # Error
43
+ from .error import Error as Error, ErrorSchema as ErrorSchema
44
+
45
+ # Document
46
+ from .document import Document as Document, DocumentSchema as DocumentSchema
47
+
48
+ # Rules, Feedback & Misc
49
+ from .rule import (
50
+ Feedback as Feedback,
51
+ FeedbackSchema as FeedbackSchema,
52
+ MetadataSchema as MetadataSchema,
53
+ Rule as Rule,
54
+ RuleSchema as RuleSchema,
55
+ )
56
+
57
+ # Knowledge Graph
58
+ from .graph import (
59
+ GraphEdge as GraphEdge,
60
+ GraphEdgeSchema as GraphEdgeSchema,
61
+ GraphNode as GraphNode,
62
+ GraphNodeSchema as GraphNodeSchema,
63
+ )
64
+
65
+ __all__ = [
66
+ "Base",
67
+ "AuditLog",
68
+ "AuditLogSchema",
69
+ "AdminJob",
70
+ "AdminJobSchema",
71
+ "Client",
72
+ "ClientApiKey",
73
+ "ClientApiKeySchema",
74
+ "ClientSchema",
75
+ "ClientSession",
76
+ "ClientSessionSchema",
77
+ "Document",
78
+ "DocumentSchema",
79
+ "Error",
80
+ "ErrorSchema",
81
+ "Feedback",
82
+ "FeedbackSchema",
83
+ "GraphEdge",
84
+ "GraphEdgeSchema",
85
+ "GraphNode",
86
+ "GraphNodeSchema",
87
+ "History",
88
+ "HistorySchema",
89
+ "MetadataSchema",
90
+ "Repository",
91
+ "RepositorySchema",
92
+ "RepositoryWorkflowState",
93
+ "RepositoryWorkflowStateSchema",
94
+ "Rule",
95
+ "RuleSchema",
96
+ "Prompt",
97
+ "PromptSchema",
98
+ "Session",
99
+ "SessionSchema",
100
+ "Skill",
101
+ "SkillSchema",
102
+ "User",
103
+ "UserSchema",
104
+ "Workflow",
105
+ "WorkflowSchema",
106
+ ]
107
+
108
+ # Prompts
109
+ from .prompt import Prompt as Prompt, PromptSchema as PromptSchema
minder/models/base.py ADDED
@@ -0,0 +1,10 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+ from sqlalchemy.orm import DeclarativeBase
3
+
4
+ class Base(DeclarativeBase):
5
+ pass
6
+
7
+
8
+ class BaseModelMeta(BaseModel):
9
+ model_config = ConfigDict(from_attributes=True)
10
+ company_id: str = "default"
@@ -0,0 +1,137 @@
1
+ import uuid
2
+ from datetime import UTC, datetime
3
+ from typing import Any
4
+
5
+ from pydantic import Field
6
+ from sqlalchemy import DateTime, JSON, String, UUID, func
7
+ from sqlalchemy.orm import Mapped, mapped_column
8
+
9
+ from .base import Base, BaseModelMeta
10
+
11
+
12
+ class ClientSchema(BaseModelMeta):
13
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
14
+ name: str
15
+ slug: str
16
+ description: str = ""
17
+ status: str = "active"
18
+ created_by_user_id: uuid.UUID
19
+ owner_team: str | None = None
20
+ transport_modes: list[str] = Field(default_factory=lambda: ["sse", "stdio"])
21
+ tool_scopes: list[str] = Field(default_factory=list)
22
+ repo_scopes: list[str] = Field(default_factory=list)
23
+ workflow_scopes: list[str] = Field(default_factory=list)
24
+ rate_limit_policy: dict[str, Any] = Field(default_factory=dict)
25
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
26
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
27
+
28
+
29
+ class ClientApiKeySchema(BaseModelMeta):
30
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
31
+ client_id: uuid.UUID
32
+ key_prefix: str
33
+ secret_hash: str
34
+ status: str = "active"
35
+ last_used_at: datetime | None = None
36
+ created_by_user_id: uuid.UUID
37
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
38
+ expires_at: datetime | None = None
39
+ revoked_at: datetime | None = None
40
+
41
+
42
+ class ClientSessionSchema(BaseModelMeta):
43
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
44
+ client_id: uuid.UUID
45
+ access_token_id: str
46
+ status: str = "active"
47
+ scopes: list[str] = Field(default_factory=list)
48
+ issued_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
49
+ expires_at: datetime
50
+ last_seen_at: datetime | None = None
51
+ session_metadata: dict[str, Any] = Field(default_factory=dict)
52
+
53
+
54
+ class AuditLogSchema(BaseModelMeta):
55
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
56
+ actor_type: str
57
+ actor_id: str
58
+ event_type: str
59
+ resource_type: str
60
+ resource_id: str | None = None
61
+ request_id: str | None = None
62
+ tool_name: str | None = None
63
+ outcome: str = "success"
64
+ ip: str | None = None
65
+ user_agent: str | None = None
66
+ audit_metadata: dict[str, Any] = Field(default_factory=dict)
67
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
68
+
69
+
70
+ class Client(Base):
71
+ __tablename__ = "clients"
72
+
73
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
74
+ name: Mapped[str] = mapped_column(String, nullable=False)
75
+ slug: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
76
+ description: Mapped[str] = mapped_column(String, default="")
77
+ status: Mapped[str] = mapped_column(String, default="active")
78
+ created_by_user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
79
+ owner_team: Mapped[str | None] = mapped_column(String, nullable=True)
80
+ transport_modes: Mapped[list[str]] = mapped_column(JSON, default=list)
81
+ tool_scopes: Mapped[list[str]] = mapped_column(JSON, default=list)
82
+ repo_scopes: Mapped[list[str]] = mapped_column(JSON, default=list)
83
+ workflow_scopes: Mapped[list[str]] = mapped_column(JSON, default=list)
84
+ rate_limit_policy: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
85
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
86
+ updated_at: Mapped[datetime] = mapped_column(
87
+ DateTime(timezone=True),
88
+ server_default=func.now(),
89
+ onupdate=func.now(),
90
+ )
91
+
92
+
93
+ class ClientApiKey(Base):
94
+ __tablename__ = "client_api_keys"
95
+
96
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
97
+ client_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
98
+ key_prefix: Mapped[str] = mapped_column(String, index=True)
99
+ secret_hash: Mapped[str] = mapped_column(String, nullable=False)
100
+ status: Mapped[str] = mapped_column(String, default="active")
101
+ last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
102
+ created_by_user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
103
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
104
+ expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
105
+ revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
106
+
107
+
108
+ class ClientSession(Base):
109
+ __tablename__ = "client_sessions"
110
+
111
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
112
+ client_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
113
+ access_token_id: Mapped[str] = mapped_column(String, unique=True, index=True)
114
+ status: Mapped[str] = mapped_column(String, default="active")
115
+ scopes: Mapped[list[str]] = mapped_column(JSON, default=list)
116
+ issued_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
117
+ expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
118
+ last_seen_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
119
+ session_metadata: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
120
+
121
+
122
+ class AuditLog(Base):
123
+ __tablename__ = "audit_logs"
124
+
125
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
126
+ actor_type: Mapped[str] = mapped_column(String, index=True)
127
+ actor_id: Mapped[str] = mapped_column(String, index=True)
128
+ event_type: Mapped[str] = mapped_column(String, index=True)
129
+ resource_type: Mapped[str] = mapped_column(String, index=True)
130
+ resource_id: Mapped[str | None] = mapped_column(String, nullable=True)
131
+ request_id: Mapped[str | None] = mapped_column(String, nullable=True)
132
+ tool_name: Mapped[str | None] = mapped_column(String, nullable=True)
133
+ outcome: Mapped[str] = mapped_column(String, default="success")
134
+ ip: Mapped[str | None] = mapped_column(String, nullable=True)
135
+ user_agent: Mapped[str | None] = mapped_column(String, nullable=True)
136
+ audit_metadata: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
137
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
@@ -0,0 +1,34 @@
1
+ import uuid
2
+ from datetime import datetime, UTC
3
+ from typing import Dict, Any, List, Optional
4
+ from sqlalchemy.orm import Mapped, mapped_column
5
+ from sqlalchemy import String, DateTime, UUID, JSON, func
6
+ from pydantic import Field
7
+
8
+ from .base import Base, BaseModelMeta
9
+
10
+ class DocumentSchema(BaseModelMeta):
11
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
12
+ title: str
13
+ content: str
14
+ doc_type: str # enum: markdown, code, api_spec, config
15
+ source_path: str
16
+ chunks: Dict[str, Any] = Field(default_factory=dict) # JSON list of chunks
17
+ embedding: Optional[List[float]] = None # vector(default 768) stored as JSON list
18
+ project: str
19
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
20
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
21
+
22
+ class Document(Base):
23
+ __tablename__ = "documents"
24
+
25
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
26
+ title: Mapped[str] = mapped_column(String)
27
+ content: Mapped[str] = mapped_column(String)
28
+ doc_type: Mapped[str] = mapped_column(String, index=True)
29
+ source_path: Mapped[str] = mapped_column(String)
30
+ chunks: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
31
+ embedding: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True)
32
+ project: Mapped[str] = mapped_column(String, index=True)
33
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
34
+ updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
minder/models/error.py ADDED
@@ -0,0 +1,32 @@
1
+ import uuid
2
+ from datetime import datetime, UTC
3
+ from typing import Dict, Any, List, Optional
4
+ from sqlalchemy.orm import Mapped, mapped_column
5
+ from sqlalchemy import String, Boolean, DateTime, UUID, JSON, func
6
+ from pydantic import Field
7
+
8
+ from .base import Base, BaseModelMeta
9
+
10
+ class ErrorSchema(BaseModelMeta):
11
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
12
+ error_code: str
13
+ error_message: str
14
+ stack_trace: Optional[str] = None
15
+ context: Dict[str, Any] = Field(default_factory=dict)
16
+ resolution: Optional[str] = None
17
+ embedding: Optional[List[float]] = None # vector(default 768) stored as JSON list
18
+ resolved: bool = False
19
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
20
+
21
+ class Error(Base):
22
+ __tablename__ = "errors"
23
+
24
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
25
+ error_code: Mapped[str] = mapped_column(String, index=True)
26
+ error_message: Mapped[str] = mapped_column(String)
27
+ stack_trace: Mapped[Optional[str]] = mapped_column(String, nullable=True)
28
+ context: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
29
+ resolution: Mapped[Optional[str]] = mapped_column(String, nullable=True)
30
+ embedding: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True) # vector as JSON fallback
31
+ resolved: Mapped[bool] = mapped_column(Boolean, default=False)
32
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
minder/models/graph.py ADDED
@@ -0,0 +1,114 @@
1
+ """
2
+ Knowledge Graph SQLAlchemy models.
3
+
4
+ GraphNode: any entity in the knowledge graph (module, file, service, owner).
5
+ GraphEdge: directed relationship between two nodes (depends_on, imports, calls, owns).
6
+
7
+ v2 schema adds repo_id + branch columns so nodes from different repositories
8
+ and branches are stored independently. UniqueConstraint is now
9
+ (repo_id, branch, node_type, name).
10
+
11
+ Migration from v1 is handled by KnowledgeGraphStore.init_db() which calls
12
+ _migrate_graph_v2() on first boot when the columns are absent.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import uuid
18
+ from datetime import datetime, UTC
19
+ from typing import Any
20
+
21
+ from pydantic import Field
22
+ from sqlalchemy import DateTime, Float, JSON, String, UUID, UniqueConstraint, func
23
+ from sqlalchemy.orm import Mapped, mapped_column
24
+
25
+ from .base import Base, BaseModelMeta
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Pydantic schemas
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ class GraphNodeSchema(BaseModelMeta):
34
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
35
+ repo_id: str = ""
36
+ branch: str = ""
37
+ node_type: str # module | file | service | owner | route | api_endpoint | websocket_endpoint | mq_topic | …
38
+ name: str
39
+ metadata: dict[str, Any] = Field(default_factory=dict)
40
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
41
+
42
+
43
+ class GraphEdgeSchema(BaseModelMeta):
44
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
45
+ repo_id: str = ""
46
+ source_id: uuid.UUID
47
+ target_id: uuid.UUID
48
+ relation: str # depends_on | owns | imports | calls | exposes_route | publishes | consumes | cross_repo_calls
49
+ weight: float = 1.0
50
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # SQLAlchemy ORM models
55
+ # ---------------------------------------------------------------------------
56
+
57
+
58
+ class GraphNode(Base):
59
+ """A node in the knowledge graph — module, file, service, or owner.
60
+
61
+ v2: scoped by (repo_id, branch) so nodes from different repositories or
62
+ branches never collide. repo_id="" and branch="" denotes global/shared
63
+ nodes (e.g. external packages used by many repos).
64
+ """
65
+
66
+ __tablename__ = "graph_nodes"
67
+ __table_args__ = (
68
+ # v2 constraint: repo_id + branch + type + name must be unique
69
+ UniqueConstraint(
70
+ "repo_id", "branch", "node_type", "name",
71
+ name="uq_graph_node_repo_branch_type_name",
72
+ ),
73
+ )
74
+
75
+ id: Mapped[uuid.UUID] = mapped_column(
76
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
77
+ )
78
+ # Scope columns (v2) — empty string = "global / no specific repo"
79
+ repo_id: Mapped[str] = mapped_column(String, index=True, default="", server_default="")
80
+ branch: Mapped[str] = mapped_column(String, index=True, default="", server_default="")
81
+ node_type: Mapped[str] = mapped_column(String, index=True)
82
+ name: Mapped[str] = mapped_column(String, index=True)
83
+ node_metadata: Mapped[dict] = mapped_column("metadata", JSON, default=dict)
84
+ created_at: Mapped[datetime] = mapped_column(
85
+ DateTime(timezone=True), server_default=func.now()
86
+ )
87
+
88
+
89
+ class GraphEdge(Base):
90
+ """A directed edge between two graph nodes.
91
+
92
+ v2: repo_id added for efficient repo-scoped edge queries.
93
+ """
94
+
95
+ __tablename__ = "graph_edges"
96
+ __table_args__ = (
97
+ UniqueConstraint(
98
+ "repo_id", "source_id", "target_id", "relation",
99
+ name="uq_graph_edge_repo_src_tgt_rel",
100
+ ),
101
+ )
102
+
103
+ id: Mapped[uuid.UUID] = mapped_column(
104
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
105
+ )
106
+ # Scope column (v2)
107
+ repo_id: Mapped[str] = mapped_column(String, index=True, default="", server_default="")
108
+ source_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
109
+ target_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
110
+ relation: Mapped[str] = mapped_column(String, index=True)
111
+ weight: Mapped[float] = mapped_column(Float, default=1.0)
112
+ created_at: Mapped[datetime] = mapped_column(
113
+ DateTime(timezone=True), server_default=func.now()
114
+ )
@@ -0,0 +1,32 @@
1
+ import uuid
2
+ from datetime import datetime, UTC
3
+ from typing import Dict, Any, Optional
4
+ from sqlalchemy.orm import Mapped, mapped_column
5
+ from sqlalchemy import String, Integer, DateTime, UUID, JSON, func
6
+ from pydantic import Field
7
+
8
+ from .base import Base, BaseModelMeta
9
+
10
+ class HistorySchema(BaseModelMeta):
11
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
12
+ session_id: uuid.UUID
13
+ role: str # enum: user, assistant, system, tool
14
+ content: str
15
+ reasoning_trace: Optional[str] = None
16
+ tool_calls: Dict[str, Any] = Field(default_factory=dict) # JSON list of tool calls
17
+ tokens_used: int = 0
18
+ latency_ms: int = 0
19
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
20
+
21
+ class History(Base):
22
+ __tablename__ = "history"
23
+
24
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
25
+ session_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), index=True)
26
+ role: Mapped[str] = mapped_column(String)
27
+ content: Mapped[str] = mapped_column(String)
28
+ reasoning_trace: Mapped[Optional[str]] = mapped_column(String, nullable=True)
29
+ tool_calls: Mapped[Dict[str, Any]] = mapped_column(JSON, default=dict)
30
+ tokens_used: Mapped[int] = mapped_column(Integer, default=0)
31
+ latency_ms: Mapped[int] = mapped_column(Integer, default=0)
32
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
minder/models/job.py ADDED
@@ -0,0 +1,62 @@
1
+ import uuid
2
+ from datetime import UTC, datetime
3
+ from typing import Any
4
+
5
+ from pydantic import Field
6
+ from sqlalchemy import DateTime, JSON, String, UUID, func
7
+ from sqlalchemy.orm import Mapped, mapped_column
8
+
9
+ from .base import Base, BaseModelMeta
10
+
11
+
12
+ class AdminJobSchema(BaseModelMeta):
13
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
14
+ job_type: str
15
+ title: str
16
+ status: str = "queued"
17
+ requested_by_user_id: uuid.UUID | None = None
18
+ payload: dict[str, Any] = Field(default_factory=dict)
19
+ result_payload: dict[str, Any] | None = None
20
+ error_message: str | None = None
21
+ progress_current: int = 0
22
+ progress_total: int = 0
23
+ message: str | None = None
24
+ events: list[dict[str, Any]] = Field(default_factory=list)
25
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
26
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
27
+ started_at: datetime | None = None
28
+ finished_at: datetime | None = None
29
+
30
+
31
+ class AdminJob(Base):
32
+ __tablename__ = "admin_jobs"
33
+
34
+ id: Mapped[uuid.UUID] = mapped_column(
35
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
36
+ )
37
+ company_id: Mapped[str] = mapped_column(String, index=True, default="default")
38
+ job_type: Mapped[str] = mapped_column(String, index=True)
39
+ title: Mapped[str] = mapped_column(String)
40
+ status: Mapped[str] = mapped_column(String, index=True, default="queued")
41
+ requested_by_user_id: Mapped[uuid.UUID | None] = mapped_column(
42
+ UUID(as_uuid=True), index=True, nullable=True
43
+ )
44
+ payload: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
45
+ result_payload: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
46
+ error_message: Mapped[str | None] = mapped_column(String, nullable=True)
47
+ progress_current: Mapped[int] = mapped_column(default=0)
48
+ progress_total: Mapped[int] = mapped_column(default=0)
49
+ message: Mapped[str | None] = mapped_column(String, nullable=True)
50
+ events: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
51
+ created_at: Mapped[datetime] = mapped_column(
52
+ DateTime(timezone=True), server_default=func.now()
53
+ )
54
+ updated_at: Mapped[datetime] = mapped_column(
55
+ DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
56
+ )
57
+ started_at: Mapped[datetime | None] = mapped_column(
58
+ DateTime(timezone=True), nullable=True
59
+ )
60
+ finished_at: Mapped[datetime | None] = mapped_column(
61
+ DateTime(timezone=True), nullable=True
62
+ )
@@ -0,0 +1,41 @@
1
+ import uuid
2
+ from datetime import datetime, UTC
3
+ from typing import List
4
+ from sqlalchemy.orm import Mapped, mapped_column
5
+ from sqlalchemy import String, DateTime, UUID, JSON, Text, func
6
+ from pydantic import Field
7
+
8
+ from .base import Base, BaseModelMeta
9
+
10
+
11
+ # Pydantic Schema
12
+ class PromptSchema(BaseModelMeta):
13
+ id: uuid.UUID = Field(default_factory=uuid.uuid4)
14
+ name: str
15
+ title: str
16
+ description: str
17
+ content_template: str
18
+ arguments: List[str] = Field(default_factory=list)
19
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
20
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
21
+
22
+
23
+ # SQLAlchemy Model
24
+ class Prompt(Base):
25
+ __tablename__ = "prompts"
26
+
27
+ id: Mapped[uuid.UUID] = mapped_column(
28
+ UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
29
+ )
30
+ company_id: Mapped[str] = mapped_column(String, index=True, default="default")
31
+ name: Mapped[str] = mapped_column(String, unique=True, index=True)
32
+ title: Mapped[str] = mapped_column(String)
33
+ description: Mapped[str] = mapped_column(String)
34
+ content_template: Mapped[str] = mapped_column(Text)
35
+ arguments: Mapped[list[str]] = mapped_column(JSON, default=list)
36
+ created_at: Mapped[datetime] = mapped_column(
37
+ DateTime(timezone=True), server_default=func.now()
38
+ )
39
+ updated_at: Mapped[datetime] = mapped_column(
40
+ DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
41
+ )