powerbi-ontology-extractor 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.
- cli/__init__.py +1 -0
- cli/pbi_ontology_cli.py +286 -0
- powerbi_ontology/__init__.py +38 -0
- powerbi_ontology/analyzer.py +420 -0
- powerbi_ontology/chat.py +303 -0
- powerbi_ontology/cli.py +530 -0
- powerbi_ontology/contract_builder.py +269 -0
- powerbi_ontology/dax_parser.py +305 -0
- powerbi_ontology/export/__init__.py +17 -0
- powerbi_ontology/export/contract_to_owl.py +408 -0
- powerbi_ontology/export/fabric_iq.py +243 -0
- powerbi_ontology/export/fabric_iq_to_owl.py +463 -0
- powerbi_ontology/export/json_schema.py +110 -0
- powerbi_ontology/export/ontoguard.py +177 -0
- powerbi_ontology/export/owl.py +522 -0
- powerbi_ontology/extractor.py +368 -0
- powerbi_ontology/mcp_config.py +237 -0
- powerbi_ontology/mcp_models.py +166 -0
- powerbi_ontology/mcp_server.py +1106 -0
- powerbi_ontology/ontology_diff.py +776 -0
- powerbi_ontology/ontology_generator.py +406 -0
- powerbi_ontology/review.py +556 -0
- powerbi_ontology/schema_mapper.py +369 -0
- powerbi_ontology/semantic_debt.py +584 -0
- powerbi_ontology/utils/__init__.py +13 -0
- powerbi_ontology/utils/pbix_reader.py +558 -0
- powerbi_ontology/utils/visualizer.py +332 -0
- powerbi_ontology_extractor-0.1.0.dist-info/METADATA +507 -0
- powerbi_ontology_extractor-0.1.0.dist-info/RECORD +33 -0
- powerbi_ontology_extractor-0.1.0.dist-info/WHEEL +5 -0
- powerbi_ontology_extractor-0.1.0.dist-info/entry_points.txt +4 -0
- powerbi_ontology_extractor-0.1.0.dist-info/licenses/LICENSE +21 -0
- powerbi_ontology_extractor-0.1.0.dist-info/top_level.txt +2 -0
powerbi_ontology/chat.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ontology Chat - AI-powered Q&A for Power BI ontologies.
|
|
3
|
+
|
|
4
|
+
Allows users to ask questions about loaded ontologies in natural language.
|
|
5
|
+
Uses OpenAI API (or compatible) to generate answers based on ontology context.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import Optional, List, Dict
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
from .ontology_generator import Ontology
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ChatMessage:
|
|
18
|
+
"""Represents a single chat message."""
|
|
19
|
+
role: str # "user" or "assistant"
|
|
20
|
+
content: str
|
|
21
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ChatSession:
|
|
26
|
+
"""Manages chat history for a session."""
|
|
27
|
+
messages: List[ChatMessage] = field(default_factory=list)
|
|
28
|
+
ontology_name: str = ""
|
|
29
|
+
user_role: str = "Analyst"
|
|
30
|
+
|
|
31
|
+
def add_message(self, role: str, content: str) -> ChatMessage:
|
|
32
|
+
"""Add a message to the chat history."""
|
|
33
|
+
msg = ChatMessage(role=role, content=content)
|
|
34
|
+
self.messages.append(msg)
|
|
35
|
+
return msg
|
|
36
|
+
|
|
37
|
+
def get_history(self, limit: int = 10) -> List[Dict[str, str]]:
|
|
38
|
+
"""Get recent chat history in OpenAI format."""
|
|
39
|
+
recent = self.messages[-limit:] if limit else self.messages
|
|
40
|
+
return [{"role": m.role, "content": m.content} for m in recent]
|
|
41
|
+
|
|
42
|
+
def clear(self):
|
|
43
|
+
"""Clear chat history."""
|
|
44
|
+
self.messages.clear()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OntologyChat:
|
|
48
|
+
"""
|
|
49
|
+
AI-powered chat for exploring Power BI ontologies.
|
|
50
|
+
|
|
51
|
+
Uses OpenAI API to answer questions about ontology structure,
|
|
52
|
+
entities, relationships, measures, and business rules.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
SYSTEM_PROMPT = """Ты - эксперт по Power BI онтологиям и семантическим моделям данных.
|
|
56
|
+
Твоя задача - отвечать на вопросы пользователя о загруженной онтологии.
|
|
57
|
+
|
|
58
|
+
КОНТЕКСТ ОНТОЛОГИИ:
|
|
59
|
+
{ontology_context}
|
|
60
|
+
|
|
61
|
+
РОЛЬ ПОЛЬЗОВАТЕЛЯ: {user_role}
|
|
62
|
+
|
|
63
|
+
ИНСТРУКЦИИ:
|
|
64
|
+
1. Отвечай на русском языке, если вопрос на русском, иначе на английском
|
|
65
|
+
2. Используй ТОЛЬКО информацию из предоставленного контекста онтологии
|
|
66
|
+
3. Если информации недостаточно, честно скажи об этом
|
|
67
|
+
4. Форматируй ответы с использованием markdown (списки, таблицы, код)
|
|
68
|
+
5. Для DAX формул используй блоки кода
|
|
69
|
+
6. Будь кратким, но информативным
|
|
70
|
+
7. Если спрашивают о правах доступа, учитывай роль пользователя
|
|
71
|
+
|
|
72
|
+
ПРИМЕРЫ ВОПРОСОВ:
|
|
73
|
+
- "Какие entities есть в онтологии?"
|
|
74
|
+
- "Как связаны Customer и Sales?"
|
|
75
|
+
- "Покажи все DAX меры"
|
|
76
|
+
- "Какие бизнес-правила определены?"
|
|
77
|
+
- "Что может делать роль Analyst?"
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
api_key: Optional[str] = None,
|
|
83
|
+
model: Optional[str] = None,
|
|
84
|
+
base_url: Optional[str] = None,
|
|
85
|
+
):
|
|
86
|
+
"""
|
|
87
|
+
Initialize the Ontology Chat.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
api_key: OpenAI API key (defaults to OPENAI_API_KEY env var)
|
|
91
|
+
model: Model to use (defaults to gpt-4o-mini)
|
|
92
|
+
base_url: Custom API base URL (for Ollama or other providers)
|
|
93
|
+
"""
|
|
94
|
+
self.api_key = api_key or os.getenv("OPENAI_API_KEY")
|
|
95
|
+
self.model = model or os.getenv("OPENAI_MODEL", "gpt-4o-mini")
|
|
96
|
+
self.base_url = base_url or os.getenv("OLLAMA_BASE_URL")
|
|
97
|
+
self._client = None
|
|
98
|
+
self.session = ChatSession()
|
|
99
|
+
|
|
100
|
+
def _get_client(self):
|
|
101
|
+
"""Lazy load OpenAI client."""
|
|
102
|
+
if self._client is None:
|
|
103
|
+
try:
|
|
104
|
+
import openai
|
|
105
|
+
|
|
106
|
+
if self.base_url:
|
|
107
|
+
# Local model (Ollama) or custom endpoint
|
|
108
|
+
self._client = openai.OpenAI(
|
|
109
|
+
base_url=self.base_url,
|
|
110
|
+
api_key=self.api_key or "not-needed",
|
|
111
|
+
timeout=60,
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
# Standard OpenAI
|
|
115
|
+
if not self.api_key:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
"OPENAI_API_KEY not set. Please set it in .env file or environment."
|
|
118
|
+
)
|
|
119
|
+
self._client = openai.OpenAI(api_key=self.api_key)
|
|
120
|
+
|
|
121
|
+
except ImportError:
|
|
122
|
+
raise ImportError(
|
|
123
|
+
"openai package not installed. Run: pip install openai"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return self._client
|
|
127
|
+
|
|
128
|
+
def build_context(self, ontology: Ontology) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Build context string from ontology for the system prompt.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
ontology: The loaded ontology
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Formatted context string
|
|
137
|
+
"""
|
|
138
|
+
lines = []
|
|
139
|
+
|
|
140
|
+
# Basic info
|
|
141
|
+
lines.append(f"ОНТОЛОГИЯ: {ontology.name} v{ontology.version}")
|
|
142
|
+
lines.append(f"Источник: {ontology.source}")
|
|
143
|
+
lines.append("")
|
|
144
|
+
|
|
145
|
+
# Entities
|
|
146
|
+
lines.append(f"ENTITIES ({len(ontology.entities)}):")
|
|
147
|
+
for entity in ontology.entities:
|
|
148
|
+
props = [p.name for p in entity.properties]
|
|
149
|
+
props_str = ", ".join(props[:5])
|
|
150
|
+
if len(props) > 5:
|
|
151
|
+
props_str += f"... (+{len(props)-5})"
|
|
152
|
+
lines.append(f" - {entity.name} ({entity.entity_type}): {props_str}")
|
|
153
|
+
if entity.description:
|
|
154
|
+
lines.append(f" Описание: {entity.description}")
|
|
155
|
+
lines.append("")
|
|
156
|
+
|
|
157
|
+
# Relationships
|
|
158
|
+
lines.append(f"RELATIONSHIPS ({len(ontology.relationships)}):")
|
|
159
|
+
for rel in ontology.relationships:
|
|
160
|
+
lines.append(
|
|
161
|
+
f" - {rel.from_entity}.{rel.from_property} → "
|
|
162
|
+
f"{rel.to_entity}.{rel.to_property} ({rel.relationship_type}, {rel.cardinality})"
|
|
163
|
+
)
|
|
164
|
+
lines.append("")
|
|
165
|
+
|
|
166
|
+
# Business Rules (DAX measures)
|
|
167
|
+
if ontology.business_rules:
|
|
168
|
+
lines.append(f"BUSINESS RULES / DAX MEASURES ({len(ontology.business_rules)}):")
|
|
169
|
+
for rule in ontology.business_rules[:20]: # Limit to avoid context overflow
|
|
170
|
+
lines.append(f" - {rule.name} [{rule.classification}]")
|
|
171
|
+
if rule.condition:
|
|
172
|
+
# Truncate long DAX formulas
|
|
173
|
+
cond = rule.condition[:100] + "..." if len(rule.condition) > 100 else rule.condition
|
|
174
|
+
lines.append(f" Formula: {cond}")
|
|
175
|
+
if len(ontology.business_rules) > 20:
|
|
176
|
+
lines.append(f" ... и ещё {len(ontology.business_rules) - 20} правил")
|
|
177
|
+
lines.append("")
|
|
178
|
+
|
|
179
|
+
# Metadata
|
|
180
|
+
if ontology.metadata:
|
|
181
|
+
lines.append("METADATA:")
|
|
182
|
+
for key, value in list(ontology.metadata.items())[:5]:
|
|
183
|
+
lines.append(f" - {key}: {value}")
|
|
184
|
+
|
|
185
|
+
return "\n".join(lines)
|
|
186
|
+
|
|
187
|
+
def ask(
|
|
188
|
+
self,
|
|
189
|
+
question: str,
|
|
190
|
+
ontology: Ontology,
|
|
191
|
+
user_role: str = "Analyst",
|
|
192
|
+
include_history: bool = True,
|
|
193
|
+
) -> str:
|
|
194
|
+
"""
|
|
195
|
+
Ask a question about the ontology.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
question: User's question in natural language
|
|
199
|
+
ontology: The loaded ontology to query
|
|
200
|
+
user_role: User's role for permission context
|
|
201
|
+
include_history: Whether to include chat history
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
AI-generated answer
|
|
205
|
+
"""
|
|
206
|
+
client = self._get_client()
|
|
207
|
+
|
|
208
|
+
# Update session
|
|
209
|
+
self.session.ontology_name = ontology.name
|
|
210
|
+
self.session.user_role = user_role
|
|
211
|
+
|
|
212
|
+
# Build system prompt with context
|
|
213
|
+
context = self.build_context(ontology)
|
|
214
|
+
system_prompt = self.SYSTEM_PROMPT.format(
|
|
215
|
+
ontology_context=context,
|
|
216
|
+
user_role=user_role,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Build messages
|
|
220
|
+
messages = [{"role": "system", "content": system_prompt}]
|
|
221
|
+
|
|
222
|
+
# Add history if requested
|
|
223
|
+
if include_history and self.session.messages:
|
|
224
|
+
messages.extend(self.session.get_history(limit=6))
|
|
225
|
+
|
|
226
|
+
# Add current question
|
|
227
|
+
messages.append({"role": "user", "content": question})
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
# Call OpenAI API
|
|
231
|
+
response = client.chat.completions.create(
|
|
232
|
+
model=self.model,
|
|
233
|
+
messages=messages,
|
|
234
|
+
temperature=0.3, # Lower temperature for more factual answers
|
|
235
|
+
max_tokens=1000,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
answer = response.choices[0].message.content or ""
|
|
239
|
+
|
|
240
|
+
# Save to history
|
|
241
|
+
self.session.add_message("user", question)
|
|
242
|
+
self.session.add_message("assistant", answer)
|
|
243
|
+
|
|
244
|
+
return answer
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
error_msg = f"Ошибка при обращении к API: {str(e)}"
|
|
248
|
+
return error_msg
|
|
249
|
+
|
|
250
|
+
def get_suggestions(self, ontology: Ontology) -> List[str]:
|
|
251
|
+
"""
|
|
252
|
+
Get suggested questions based on ontology content.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
ontology: The loaded ontology
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
List of suggested questions
|
|
259
|
+
"""
|
|
260
|
+
suggestions = [
|
|
261
|
+
"Какие entities есть в онтологии?",
|
|
262
|
+
"Покажи все relationships между entities",
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
# Add entity-specific suggestions
|
|
266
|
+
if ontology.entities:
|
|
267
|
+
first_entity = ontology.entities[0].name
|
|
268
|
+
suggestions.append(f"Расскажи подробнее о {first_entity}")
|
|
269
|
+
|
|
270
|
+
if len(ontology.entities) > 1:
|
|
271
|
+
second_entity = ontology.entities[1].name
|
|
272
|
+
suggestions.append(f"Как связаны {first_entity} и {second_entity}?")
|
|
273
|
+
|
|
274
|
+
# Add measure-specific suggestions
|
|
275
|
+
if ontology.business_rules:
|
|
276
|
+
suggestions.append("Какие DAX меры определены?")
|
|
277
|
+
suggestions.append("Покажи формулы для расчёта продаж")
|
|
278
|
+
|
|
279
|
+
# Add permission suggestions
|
|
280
|
+
suggestions.append("Какие права доступа есть у роли Analyst?")
|
|
281
|
+
|
|
282
|
+
return suggestions[:6] # Limit to 6 suggestions
|
|
283
|
+
|
|
284
|
+
def clear_history(self):
|
|
285
|
+
"""Clear chat history."""
|
|
286
|
+
self.session.clear()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def create_chat(
|
|
290
|
+
api_key: Optional[str] = None,
|
|
291
|
+
model: Optional[str] = None,
|
|
292
|
+
) -> OntologyChat:
|
|
293
|
+
"""
|
|
294
|
+
Factory function to create OntologyChat instance.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
api_key: OpenAI API key (optional, uses env var if not provided)
|
|
298
|
+
model: Model name (optional, defaults to gpt-4o-mini)
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Configured OntologyChat instance
|
|
302
|
+
"""
|
|
303
|
+
return OntologyChat(api_key=api_key, model=model)
|