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.
@@ -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)