ospac 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.
Potentially problematic release.
This version of ospac might be problematic. Click here for more details.
- ospac/__init__.py +19 -0
- ospac/cli/__init__.py +5 -0
- ospac/cli/commands.py +554 -0
- ospac/core/compatibility_matrix.py +332 -0
- ospac/models/__init__.py +12 -0
- ospac/models/compliance.py +161 -0
- ospac/models/license.py +82 -0
- ospac/models/policy.py +97 -0
- ospac/pipeline/__init__.py +14 -0
- ospac/pipeline/data_generator.py +530 -0
- ospac/pipeline/llm_analyzer.py +338 -0
- ospac/pipeline/llm_providers.py +463 -0
- ospac/pipeline/spdx_processor.py +283 -0
- ospac/runtime/__init__.py +11 -0
- ospac/runtime/engine.py +127 -0
- ospac/runtime/evaluator.py +72 -0
- ospac/runtime/loader.py +54 -0
- ospac/utils/__init__.py +3 -0
- ospac-0.1.0.dist-info/METADATA +269 -0
- ospac-0.1.0.dist-info/RECORD +25 -0
- ospac-0.1.0.dist-info/WHEEL +5 -0
- ospac-0.1.0.dist-info/entry_points.txt +2 -0
- ospac-0.1.0.dist-info/licenses/AUTHORS.md +9 -0
- ospac-0.1.0.dist-info/licenses/LICENSE +201 -0
- ospac-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM provider implementations for OSPAC license analysis.
|
|
3
|
+
Supports OpenAI, Anthropic Claude, and local Ollama.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import asyncio
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Dict, List, Any, Optional
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class LLMConfig:
|
|
18
|
+
"""Configuration for LLM providers."""
|
|
19
|
+
provider: str # "openai", "claude", "ollama"
|
|
20
|
+
model: str
|
|
21
|
+
api_key: Optional[str] = None
|
|
22
|
+
base_url: Optional[str] = None
|
|
23
|
+
max_tokens: int = 4000
|
|
24
|
+
temperature: float = 0.1
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LLMProvider(ABC):
|
|
28
|
+
"""Abstract base class for LLM providers."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, config: LLMConfig):
|
|
31
|
+
self.config = config
|
|
32
|
+
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
async def analyze_license(self, license_id: str, license_text: str) -> Dict[str, Any]:
|
|
36
|
+
"""Analyze a license using the LLM provider."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
async def extract_compatibility_rules(self, license_id: str, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
|
41
|
+
"""Extract compatibility rules for a license."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def _get_system_prompt(self) -> str:
|
|
45
|
+
"""Get the system prompt for license analysis."""
|
|
46
|
+
return """You are an expert in software licensing and open source compliance.
|
|
47
|
+
Your task is to analyze software licenses and provide detailed, accurate information about:
|
|
48
|
+
- License obligations and requirements
|
|
49
|
+
- Compatibility with other licenses
|
|
50
|
+
- Usage restrictions and permissions
|
|
51
|
+
- Patent grants and trademark rules
|
|
52
|
+
|
|
53
|
+
Always provide information in structured JSON format.
|
|
54
|
+
Be precise and accurate - licensing compliance is critical."""
|
|
55
|
+
|
|
56
|
+
def _get_analysis_prompt(self, license_id: str, license_text: str) -> str:
|
|
57
|
+
"""Get the analysis prompt for a specific license."""
|
|
58
|
+
return f"""Analyze the following license and provide detailed information in JSON format.
|
|
59
|
+
|
|
60
|
+
License ID: {license_id}
|
|
61
|
+
License Text (first 3000 chars):
|
|
62
|
+
{license_text[:3000]}
|
|
63
|
+
|
|
64
|
+
Provide a JSON response with the following structure:
|
|
65
|
+
{{
|
|
66
|
+
"license_id": "{license_id}",
|
|
67
|
+
"category": "permissive|copyleft_weak|copyleft_strong|proprietary|public_domain",
|
|
68
|
+
"permissions": {{
|
|
69
|
+
"commercial_use": true/false,
|
|
70
|
+
"distribution": true/false,
|
|
71
|
+
"modification": true/false,
|
|
72
|
+
"patent_grant": true/false,
|
|
73
|
+
"private_use": true/false
|
|
74
|
+
}},
|
|
75
|
+
"conditions": {{
|
|
76
|
+
"disclose_source": true/false,
|
|
77
|
+
"include_license": true/false,
|
|
78
|
+
"include_copyright": true/false,
|
|
79
|
+
"include_notice": true/false,
|
|
80
|
+
"state_changes": true/false,
|
|
81
|
+
"same_license": true/false,
|
|
82
|
+
"network_use_disclosure": true/false
|
|
83
|
+
}},
|
|
84
|
+
"limitations": {{
|
|
85
|
+
"liability": true/false,
|
|
86
|
+
"warranty": true/false,
|
|
87
|
+
"trademark_use": true/false
|
|
88
|
+
}},
|
|
89
|
+
"compatibility": {{
|
|
90
|
+
"can_combine_with_permissive": true/false,
|
|
91
|
+
"can_combine_with_weak_copyleft": true/false,
|
|
92
|
+
"can_combine_with_strong_copyleft": true/false,
|
|
93
|
+
"static_linking_restrictions": "none|weak|strong",
|
|
94
|
+
"dynamic_linking_restrictions": "none|weak|strong"
|
|
95
|
+
}},
|
|
96
|
+
"obligations": [
|
|
97
|
+
"List of specific obligations when using this license"
|
|
98
|
+
],
|
|
99
|
+
"key_requirements": [
|
|
100
|
+
"List of key requirements for compliance"
|
|
101
|
+
]
|
|
102
|
+
}}"""
|
|
103
|
+
|
|
104
|
+
def _get_compatibility_prompt(self, license_id: str, analysis: Dict[str, Any]) -> str:
|
|
105
|
+
"""Get the compatibility rules prompt."""
|
|
106
|
+
return f"""Based on the {license_id} license with category {analysis.get('category', 'unknown')},
|
|
107
|
+
provide detailed compatibility rules in JSON format:
|
|
108
|
+
|
|
109
|
+
{{
|
|
110
|
+
"static_linking": {{
|
|
111
|
+
"compatible_with": ["list of compatible license IDs or categories"],
|
|
112
|
+
"incompatible_with": ["list of incompatible license IDs or categories"],
|
|
113
|
+
"requires_review": ["list of licenses requiring case-by-case review"]
|
|
114
|
+
}},
|
|
115
|
+
"dynamic_linking": {{
|
|
116
|
+
"compatible_with": ["list"],
|
|
117
|
+
"incompatible_with": ["list"],
|
|
118
|
+
"requires_review": ["list"]
|
|
119
|
+
}},
|
|
120
|
+
"distribution": {{
|
|
121
|
+
"can_distribute_with": ["list"],
|
|
122
|
+
"cannot_distribute_with": ["list"],
|
|
123
|
+
"special_requirements": ["list of special requirements"]
|
|
124
|
+
}},
|
|
125
|
+
"contamination_effect": "none|module|derivative|full",
|
|
126
|
+
"notes": "Additional compatibility notes"
|
|
127
|
+
}}"""
|
|
128
|
+
|
|
129
|
+
def _parse_json_response(self, response_text: str, license_id: str) -> Dict[str, Any]:
|
|
130
|
+
"""Parse JSON from LLM response."""
|
|
131
|
+
try:
|
|
132
|
+
# Find JSON in response
|
|
133
|
+
json_start = response_text.find("{")
|
|
134
|
+
json_end = response_text.rfind("}") + 1
|
|
135
|
+
if json_start >= 0 and json_end > json_start:
|
|
136
|
+
json_str = response_text[json_start:json_end]
|
|
137
|
+
return json.loads(json_str)
|
|
138
|
+
else:
|
|
139
|
+
self.logger.warning(f"Could not extract JSON from LLM response for {license_id}")
|
|
140
|
+
return self._get_fallback_analysis(license_id)
|
|
141
|
+
except json.JSONDecodeError as e:
|
|
142
|
+
self.logger.error(f"Failed to parse LLM response for {license_id}: {e}")
|
|
143
|
+
self.logger.debug(f"Response content: {response_text[:500]}")
|
|
144
|
+
return self._get_fallback_analysis(license_id)
|
|
145
|
+
|
|
146
|
+
def _get_fallback_analysis(self, license_id: str) -> Dict[str, Any]:
|
|
147
|
+
"""Get fallback analysis for when LLM fails."""
|
|
148
|
+
analysis = {
|
|
149
|
+
"license_id": license_id,
|
|
150
|
+
"category": "permissive",
|
|
151
|
+
"permissions": {
|
|
152
|
+
"commercial_use": True,
|
|
153
|
+
"distribution": True,
|
|
154
|
+
"modification": True,
|
|
155
|
+
"patent_grant": False,
|
|
156
|
+
"private_use": True
|
|
157
|
+
},
|
|
158
|
+
"conditions": {
|
|
159
|
+
"disclose_source": False,
|
|
160
|
+
"include_license": True,
|
|
161
|
+
"include_copyright": True,
|
|
162
|
+
"include_notice": False,
|
|
163
|
+
"state_changes": False,
|
|
164
|
+
"same_license": False,
|
|
165
|
+
"network_use_disclosure": False
|
|
166
|
+
},
|
|
167
|
+
"limitations": {
|
|
168
|
+
"liability": True,
|
|
169
|
+
"warranty": True,
|
|
170
|
+
"trademark_use": False
|
|
171
|
+
},
|
|
172
|
+
"compatibility": {
|
|
173
|
+
"can_combine_with_permissive": True,
|
|
174
|
+
"can_combine_with_weak_copyleft": True,
|
|
175
|
+
"can_combine_with_strong_copyleft": False,
|
|
176
|
+
"static_linking_restrictions": "none",
|
|
177
|
+
"dynamic_linking_restrictions": "none"
|
|
178
|
+
},
|
|
179
|
+
"obligations": ["Include license text", "Include copyright notice"],
|
|
180
|
+
"key_requirements": ["Attribution required"]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Customize based on known patterns
|
|
184
|
+
if "GPL" in license_id:
|
|
185
|
+
analysis["category"] = "copyleft_strong"
|
|
186
|
+
analysis["conditions"]["disclose_source"] = True
|
|
187
|
+
analysis["conditions"]["same_license"] = True
|
|
188
|
+
elif "LGPL" in license_id:
|
|
189
|
+
analysis["category"] = "copyleft_weak"
|
|
190
|
+
analysis["conditions"]["disclose_source"] = True
|
|
191
|
+
elif "AGPL" in license_id:
|
|
192
|
+
analysis["category"] = "copyleft_strong"
|
|
193
|
+
analysis["conditions"]["network_use_disclosure"] = True
|
|
194
|
+
elif "Apache" in license_id:
|
|
195
|
+
analysis["permissions"]["patent_grant"] = True
|
|
196
|
+
elif "CC0" in license_id or "Unlicense" in license_id:
|
|
197
|
+
analysis["category"] = "public_domain"
|
|
198
|
+
|
|
199
|
+
return analysis
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class OpenAIProvider(LLMProvider):
|
|
203
|
+
"""OpenAI LLM provider using OpenAI API."""
|
|
204
|
+
|
|
205
|
+
def __init__(self, config: LLMConfig):
|
|
206
|
+
super().__init__(config)
|
|
207
|
+
try:
|
|
208
|
+
import openai
|
|
209
|
+
self.client = openai.AsyncOpenAI(api_key=config.api_key)
|
|
210
|
+
self.available = True
|
|
211
|
+
except ImportError:
|
|
212
|
+
self.logger.error("OpenAI package not installed. Install with: pip install openai")
|
|
213
|
+
self.available = False
|
|
214
|
+
except Exception as e:
|
|
215
|
+
self.logger.error(f"Failed to initialize OpenAI client: {e}")
|
|
216
|
+
self.available = False
|
|
217
|
+
|
|
218
|
+
async def analyze_license(self, license_id: str, license_text: str) -> Dict[str, Any]:
|
|
219
|
+
"""Analyze license using OpenAI."""
|
|
220
|
+
if not self.available:
|
|
221
|
+
self.logger.warning(f"OpenAI not available, returning fallback for {license_id}")
|
|
222
|
+
return self._get_fallback_analysis(license_id)
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
response = await self.client.chat.completions.create(
|
|
226
|
+
model=self.config.model,
|
|
227
|
+
messages=[
|
|
228
|
+
{"role": "system", "content": self._get_system_prompt()},
|
|
229
|
+
{"role": "user", "content": self._get_analysis_prompt(license_id, license_text)}
|
|
230
|
+
],
|
|
231
|
+
max_tokens=self.config.max_tokens,
|
|
232
|
+
temperature=self.config.temperature
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
response_text = response.choices[0].message.content
|
|
236
|
+
return self._parse_json_response(response_text, license_id)
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
self.logger.error(f"OpenAI analysis failed for {license_id}: {e}")
|
|
240
|
+
return self._get_fallback_analysis(license_id)
|
|
241
|
+
|
|
242
|
+
async def extract_compatibility_rules(self, license_id: str, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
|
243
|
+
"""Extract compatibility rules using OpenAI."""
|
|
244
|
+
if not self.available:
|
|
245
|
+
return self._get_default_compatibility_rules(license_id, analysis)
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
response = await self.client.chat.completions.create(
|
|
249
|
+
model=self.config.model,
|
|
250
|
+
messages=[
|
|
251
|
+
{"role": "user", "content": self._get_compatibility_prompt(license_id, analysis)}
|
|
252
|
+
],
|
|
253
|
+
max_tokens=self.config.max_tokens,
|
|
254
|
+
temperature=self.config.temperature
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
response_text = response.choices[0].message.content
|
|
258
|
+
return self._parse_json_response(response_text, license_id)
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
self.logger.error(f"OpenAI compatibility extraction failed for {license_id}: {e}")
|
|
262
|
+
return self._get_default_compatibility_rules(license_id, analysis)
|
|
263
|
+
|
|
264
|
+
def _get_default_compatibility_rules(self, license_id: str, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
|
265
|
+
"""Get default compatibility rules."""
|
|
266
|
+
category = analysis.get("category", "permissive")
|
|
267
|
+
|
|
268
|
+
if category == "permissive":
|
|
269
|
+
return {
|
|
270
|
+
"static_linking": {
|
|
271
|
+
"compatible_with": ["category:any"],
|
|
272
|
+
"incompatible_with": [],
|
|
273
|
+
"requires_review": []
|
|
274
|
+
},
|
|
275
|
+
"dynamic_linking": {
|
|
276
|
+
"compatible_with": ["category:any"],
|
|
277
|
+
"incompatible_with": [],
|
|
278
|
+
"requires_review": []
|
|
279
|
+
},
|
|
280
|
+
"contamination_effect": "none",
|
|
281
|
+
"notes": "Permissive license with minimal restrictions"
|
|
282
|
+
}
|
|
283
|
+
elif category == "copyleft_strong":
|
|
284
|
+
return {
|
|
285
|
+
"static_linking": {
|
|
286
|
+
"compatible_with": [license_id, "category:copyleft_strong"],
|
|
287
|
+
"incompatible_with": ["category:permissive", "category:proprietary"],
|
|
288
|
+
"requires_review": ["category:copyleft_weak"]
|
|
289
|
+
},
|
|
290
|
+
"dynamic_linking": {
|
|
291
|
+
"compatible_with": ["category:any"],
|
|
292
|
+
"incompatible_with": [],
|
|
293
|
+
"requires_review": ["category:proprietary"]
|
|
294
|
+
},
|
|
295
|
+
"contamination_effect": "full",
|
|
296
|
+
"notes": "Strong copyleft with viral effect"
|
|
297
|
+
}
|
|
298
|
+
else:
|
|
299
|
+
return {
|
|
300
|
+
"static_linking": {
|
|
301
|
+
"compatible_with": ["category:any"],
|
|
302
|
+
"incompatible_with": [],
|
|
303
|
+
"requires_review": []
|
|
304
|
+
},
|
|
305
|
+
"dynamic_linking": {
|
|
306
|
+
"compatible_with": ["category:any"],
|
|
307
|
+
"incompatible_with": [],
|
|
308
|
+
"requires_review": []
|
|
309
|
+
},
|
|
310
|
+
"contamination_effect": "none",
|
|
311
|
+
"notes": "Default compatibility rules"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class ClaudeProvider(LLMProvider):
|
|
316
|
+
"""Anthropic Claude LLM provider using Anthropic API."""
|
|
317
|
+
|
|
318
|
+
def __init__(self, config: LLMConfig):
|
|
319
|
+
super().__init__(config)
|
|
320
|
+
try:
|
|
321
|
+
import anthropic
|
|
322
|
+
self.client = anthropic.AsyncAnthropic(api_key=config.api_key)
|
|
323
|
+
self.available = True
|
|
324
|
+
except ImportError:
|
|
325
|
+
self.logger.error("Anthropic package not installed. Install with: pip install anthropic")
|
|
326
|
+
self.available = False
|
|
327
|
+
except Exception as e:
|
|
328
|
+
self.logger.error(f"Failed to initialize Claude client: {e}")
|
|
329
|
+
self.available = False
|
|
330
|
+
|
|
331
|
+
async def analyze_license(self, license_id: str, license_text: str) -> Dict[str, Any]:
|
|
332
|
+
"""Analyze license using Claude."""
|
|
333
|
+
if not self.available:
|
|
334
|
+
self.logger.warning(f"Claude not available, returning fallback for {license_id}")
|
|
335
|
+
return self._get_fallback_analysis(license_id)
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
message = await self.client.messages.create(
|
|
339
|
+
model=self.config.model,
|
|
340
|
+
max_tokens=self.config.max_tokens,
|
|
341
|
+
temperature=self.config.temperature,
|
|
342
|
+
system=self._get_system_prompt(),
|
|
343
|
+
messages=[
|
|
344
|
+
{"role": "user", "content": self._get_analysis_prompt(license_id, license_text)}
|
|
345
|
+
]
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
response_text = message.content[0].text
|
|
349
|
+
return self._parse_json_response(response_text, license_id)
|
|
350
|
+
|
|
351
|
+
except Exception as e:
|
|
352
|
+
self.logger.error(f"Claude analysis failed for {license_id}: {e}")
|
|
353
|
+
return self._get_fallback_analysis(license_id)
|
|
354
|
+
|
|
355
|
+
async def extract_compatibility_rules(self, license_id: str, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
|
356
|
+
"""Extract compatibility rules using Claude."""
|
|
357
|
+
if not self.available:
|
|
358
|
+
return self._get_default_compatibility_rules(license_id, analysis)
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
message = await self.client.messages.create(
|
|
362
|
+
model=self.config.model,
|
|
363
|
+
max_tokens=self.config.max_tokens,
|
|
364
|
+
temperature=self.config.temperature,
|
|
365
|
+
messages=[
|
|
366
|
+
{"role": "user", "content": self._get_compatibility_prompt(license_id, analysis)}
|
|
367
|
+
]
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
response_text = message.content[0].text
|
|
371
|
+
return self._parse_json_response(response_text, license_id)
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
self.logger.error(f"Claude compatibility extraction failed for {license_id}: {e}")
|
|
375
|
+
return self._get_default_compatibility_rules(license_id, analysis)
|
|
376
|
+
|
|
377
|
+
def _get_default_compatibility_rules(self, license_id: str, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
|
378
|
+
"""Get default compatibility rules (same as OpenAI)."""
|
|
379
|
+
return OpenAIProvider._get_default_compatibility_rules(self, license_id, analysis)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class OllamaProvider(LLMProvider):
|
|
383
|
+
"""Local Ollama LLM provider."""
|
|
384
|
+
|
|
385
|
+
def __init__(self, config: LLMConfig):
|
|
386
|
+
super().__init__(config)
|
|
387
|
+
try:
|
|
388
|
+
import ollama
|
|
389
|
+
# Test connection
|
|
390
|
+
models = ollama.list()
|
|
391
|
+
available_models = [model.model for model in models.models]
|
|
392
|
+
|
|
393
|
+
if config.model not in available_models:
|
|
394
|
+
self.logger.warning(f"Model {config.model} not found. Available: {available_models}")
|
|
395
|
+
self.available = False
|
|
396
|
+
else:
|
|
397
|
+
self.client = ollama
|
|
398
|
+
self.available = True
|
|
399
|
+
|
|
400
|
+
except ImportError:
|
|
401
|
+
self.logger.error("Ollama package not installed. Install with: pip install ollama")
|
|
402
|
+
self.available = False
|
|
403
|
+
except Exception as e:
|
|
404
|
+
self.logger.error(f"Failed to initialize Ollama client: {e}")
|
|
405
|
+
self.available = False
|
|
406
|
+
|
|
407
|
+
async def analyze_license(self, license_id: str, license_text: str) -> Dict[str, Any]:
|
|
408
|
+
"""Analyze license using Ollama."""
|
|
409
|
+
if not self.available:
|
|
410
|
+
self.logger.warning(f"Ollama not available, returning fallback for {license_id}")
|
|
411
|
+
return self._get_fallback_analysis(license_id)
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
response = self.client.chat(
|
|
415
|
+
model=self.config.model,
|
|
416
|
+
messages=[
|
|
417
|
+
{'role': 'system', 'content': self._get_system_prompt()},
|
|
418
|
+
{'role': 'user', 'content': self._get_analysis_prompt(license_id, license_text)}
|
|
419
|
+
]
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
response_text = response['message']['content']
|
|
423
|
+
return self._parse_json_response(response_text, license_id)
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
self.logger.error(f"Ollama analysis failed for {license_id}: {e}")
|
|
427
|
+
return self._get_fallback_analysis(license_id)
|
|
428
|
+
|
|
429
|
+
async def extract_compatibility_rules(self, license_id: str, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
|
430
|
+
"""Extract compatibility rules using Ollama."""
|
|
431
|
+
if not self.available:
|
|
432
|
+
return self._get_default_compatibility_rules(license_id, analysis)
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
response = self.client.chat(
|
|
436
|
+
model=self.config.model,
|
|
437
|
+
messages=[
|
|
438
|
+
{'role': 'user', 'content': self._get_compatibility_prompt(license_id, analysis)}
|
|
439
|
+
]
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
response_text = response['message']['content']
|
|
443
|
+
return self._parse_json_response(response_text, license_id)
|
|
444
|
+
|
|
445
|
+
except Exception as e:
|
|
446
|
+
self.logger.error(f"Ollama compatibility extraction failed for {license_id}: {e}")
|
|
447
|
+
return self._get_default_compatibility_rules(license_id, analysis)
|
|
448
|
+
|
|
449
|
+
def _get_default_compatibility_rules(self, license_id: str, analysis: Dict[str, Any]) -> Dict[str, Any]:
|
|
450
|
+
"""Get default compatibility rules (same as OpenAI)."""
|
|
451
|
+
return OpenAIProvider._get_default_compatibility_rules(self, license_id, analysis)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def create_llm_provider(config: LLMConfig) -> LLMProvider:
|
|
455
|
+
"""Factory function to create appropriate LLM provider."""
|
|
456
|
+
if config.provider.lower() == "openai":
|
|
457
|
+
return OpenAIProvider(config)
|
|
458
|
+
elif config.provider.lower() == "claude":
|
|
459
|
+
return ClaudeProvider(config)
|
|
460
|
+
elif config.provider.lower() == "ollama":
|
|
461
|
+
return OllamaProvider(config)
|
|
462
|
+
else:
|
|
463
|
+
raise ValueError(f"Unknown LLM provider: {config.provider}")
|