yuho 5.0.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 (91) hide show
  1. yuho/__init__.py +16 -0
  2. yuho/ast/__init__.py +196 -0
  3. yuho/ast/builder.py +926 -0
  4. yuho/ast/constant_folder.py +280 -0
  5. yuho/ast/dead_code.py +199 -0
  6. yuho/ast/exhaustiveness.py +503 -0
  7. yuho/ast/nodes.py +907 -0
  8. yuho/ast/overlap.py +291 -0
  9. yuho/ast/reachability.py +293 -0
  10. yuho/ast/scope_analysis.py +490 -0
  11. yuho/ast/transformer.py +490 -0
  12. yuho/ast/type_check.py +471 -0
  13. yuho/ast/type_inference.py +425 -0
  14. yuho/ast/visitor.py +239 -0
  15. yuho/cli/__init__.py +14 -0
  16. yuho/cli/commands/__init__.py +1 -0
  17. yuho/cli/commands/api.py +431 -0
  18. yuho/cli/commands/ast_viz.py +334 -0
  19. yuho/cli/commands/check.py +218 -0
  20. yuho/cli/commands/config.py +311 -0
  21. yuho/cli/commands/contribute.py +122 -0
  22. yuho/cli/commands/diff.py +487 -0
  23. yuho/cli/commands/explain.py +240 -0
  24. yuho/cli/commands/fmt.py +253 -0
  25. yuho/cli/commands/generate.py +316 -0
  26. yuho/cli/commands/graph.py +410 -0
  27. yuho/cli/commands/init.py +120 -0
  28. yuho/cli/commands/library.py +656 -0
  29. yuho/cli/commands/lint.py +503 -0
  30. yuho/cli/commands/lsp.py +36 -0
  31. yuho/cli/commands/preview.py +377 -0
  32. yuho/cli/commands/repl.py +444 -0
  33. yuho/cli/commands/serve.py +44 -0
  34. yuho/cli/commands/test.py +528 -0
  35. yuho/cli/commands/transpile.py +121 -0
  36. yuho/cli/commands/wizard.py +370 -0
  37. yuho/cli/completions.py +182 -0
  38. yuho/cli/error_formatter.py +193 -0
  39. yuho/cli/main.py +1064 -0
  40. yuho/config/__init__.py +46 -0
  41. yuho/config/loader.py +235 -0
  42. yuho/config/mask.py +194 -0
  43. yuho/config/schema.py +147 -0
  44. yuho/library/__init__.py +84 -0
  45. yuho/library/index.py +328 -0
  46. yuho/library/install.py +699 -0
  47. yuho/library/lockfile.py +330 -0
  48. yuho/library/package.py +421 -0
  49. yuho/library/resolver.py +791 -0
  50. yuho/library/signature.py +335 -0
  51. yuho/llm/__init__.py +45 -0
  52. yuho/llm/config.py +75 -0
  53. yuho/llm/factory.py +123 -0
  54. yuho/llm/prompts.py +146 -0
  55. yuho/llm/providers.py +383 -0
  56. yuho/llm/utils.py +470 -0
  57. yuho/lsp/__init__.py +14 -0
  58. yuho/lsp/code_action_handler.py +518 -0
  59. yuho/lsp/completion_handler.py +85 -0
  60. yuho/lsp/diagnostics.py +100 -0
  61. yuho/lsp/hover_handler.py +130 -0
  62. yuho/lsp/server.py +1425 -0
  63. yuho/mcp/__init__.py +10 -0
  64. yuho/mcp/server.py +1452 -0
  65. yuho/parser/__init__.py +8 -0
  66. yuho/parser/source_location.py +108 -0
  67. yuho/parser/wrapper.py +311 -0
  68. yuho/testing/__init__.py +48 -0
  69. yuho/testing/coverage.py +274 -0
  70. yuho/testing/fixtures.py +263 -0
  71. yuho/transpile/__init__.py +52 -0
  72. yuho/transpile/alloy_transpiler.py +546 -0
  73. yuho/transpile/base.py +100 -0
  74. yuho/transpile/blocks_transpiler.py +338 -0
  75. yuho/transpile/english_transpiler.py +470 -0
  76. yuho/transpile/graphql_transpiler.py +404 -0
  77. yuho/transpile/json_transpiler.py +217 -0
  78. yuho/transpile/jsonld_transpiler.py +250 -0
  79. yuho/transpile/latex_preamble.py +161 -0
  80. yuho/transpile/latex_transpiler.py +406 -0
  81. yuho/transpile/latex_utils.py +206 -0
  82. yuho/transpile/mermaid_transpiler.py +357 -0
  83. yuho/transpile/registry.py +275 -0
  84. yuho/verify/__init__.py +43 -0
  85. yuho/verify/alloy.py +352 -0
  86. yuho/verify/combined.py +218 -0
  87. yuho/verify/z3_solver.py +1155 -0
  88. yuho-5.0.0.dist-info/METADATA +186 -0
  89. yuho-5.0.0.dist-info/RECORD +91 -0
  90. yuho-5.0.0.dist-info/WHEEL +4 -0
  91. yuho-5.0.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,335 @@
1
+ """
2
+ Ed25519 signature verification for Yuho packages.
3
+
4
+ Provides cryptographic signing and verification of statute packages
5
+ to ensure authenticity and integrity.
6
+ """
7
+
8
+ from typing import Optional, Tuple
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ import base64
12
+ import hashlib
13
+ import json
14
+ import logging
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ # Try to import cryptography library
20
+ try:
21
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
22
+ Ed25519PrivateKey,
23
+ Ed25519PublicKey,
24
+ )
25
+ from cryptography.hazmat.primitives import serialization
26
+ from cryptography.exceptions import InvalidSignature
27
+ CRYPTO_AVAILABLE = True
28
+ except ImportError:
29
+ CRYPTO_AVAILABLE = False
30
+ Ed25519PrivateKey = None
31
+ Ed25519PublicKey = None
32
+
33
+
34
+ @dataclass
35
+ class KeyPair:
36
+ """Ed25519 key pair for signing packages."""
37
+ private_key: bytes # PEM-encoded private key
38
+ public_key: bytes # PEM-encoded public key
39
+ key_id: str # Fingerprint/identifier for the key
40
+
41
+
42
+ @dataclass
43
+ class Signature:
44
+ """Package signature with metadata."""
45
+ signature: str # Base64-encoded signature
46
+ key_id: str # Key identifier used to sign
47
+ algorithm: str # Always "ed25519"
48
+ content_hash: str # SHA-256 hash of signed content
49
+
50
+
51
+ class SignatureManager:
52
+ """
53
+ Manages Ed25519 key pairs and package signatures.
54
+
55
+ Keys are stored in ~/.yuho/keys/ directory.
56
+ """
57
+
58
+ DEFAULT_KEY_DIR = Path.home() / ".yuho" / "keys"
59
+
60
+ def __init__(self, key_dir: Optional[Path] = None):
61
+ """
62
+ Initialize signature manager.
63
+
64
+ Args:
65
+ key_dir: Directory for key storage
66
+ """
67
+ self.key_dir = key_dir or self.DEFAULT_KEY_DIR
68
+ self.key_dir.mkdir(parents=True, exist_ok=True)
69
+
70
+ @staticmethod
71
+ def is_available() -> bool:
72
+ """Check if cryptography library is available."""
73
+ return CRYPTO_AVAILABLE
74
+
75
+ def generate_keypair(self, name: str = "default") -> KeyPair:
76
+ """
77
+ Generate a new Ed25519 key pair.
78
+
79
+ Args:
80
+ name: Key name for storage
81
+
82
+ Returns:
83
+ Generated KeyPair
84
+
85
+ Raises:
86
+ ImportError: If cryptography not installed
87
+ """
88
+ if not CRYPTO_AVAILABLE:
89
+ raise ImportError(
90
+ "Cryptography library not installed. "
91
+ "Install with: pip install cryptography"
92
+ )
93
+
94
+ # Generate private key
95
+ private_key = Ed25519PrivateKey.generate()
96
+ public_key = private_key.public_key()
97
+
98
+ # Serialize to PEM
99
+ private_pem = private_key.private_bytes(
100
+ encoding=serialization.Encoding.PEM,
101
+ format=serialization.PrivateFormat.PKCS8,
102
+ encryption_algorithm=serialization.NoEncryption(),
103
+ )
104
+
105
+ public_pem = public_key.public_bytes(
106
+ encoding=serialization.Encoding.PEM,
107
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
108
+ )
109
+
110
+ # Generate key ID (fingerprint of public key)
111
+ key_id = hashlib.sha256(public_pem).hexdigest()[:16]
112
+
113
+ keypair = KeyPair(
114
+ private_key=private_pem,
115
+ public_key=public_pem,
116
+ key_id=key_id,
117
+ )
118
+
119
+ # Save to disk
120
+ self._save_keypair(name, keypair)
121
+
122
+ return keypair
123
+
124
+ def _save_keypair(self, name: str, keypair: KeyPair) -> None:
125
+ """Save key pair to disk."""
126
+ private_path = self.key_dir / f"{name}.key"
127
+ public_path = self.key_dir / f"{name}.pub"
128
+
129
+ private_path.write_bytes(keypair.private_key)
130
+ private_path.chmod(0o600) # Owner read/write only
131
+
132
+ public_path.write_bytes(keypair.public_key)
133
+
134
+ def load_keypair(self, name: str = "default") -> Optional[KeyPair]:
135
+ """
136
+ Load key pair from disk.
137
+
138
+ Args:
139
+ name: Key name
140
+
141
+ Returns:
142
+ KeyPair or None if not found
143
+ """
144
+ private_path = self.key_dir / f"{name}.key"
145
+ public_path = self.key_dir / f"{name}.pub"
146
+
147
+ if not private_path.exists() or not public_path.exists():
148
+ return None
149
+
150
+ private_pem = private_path.read_bytes()
151
+ public_pem = public_path.read_bytes()
152
+ key_id = hashlib.sha256(public_pem).hexdigest()[:16]
153
+
154
+ return KeyPair(
155
+ private_key=private_pem,
156
+ public_key=public_pem,
157
+ key_id=key_id,
158
+ )
159
+
160
+ def load_public_key(self, name: str = "default") -> Optional[bytes]:
161
+ """Load just the public key."""
162
+ public_path = self.key_dir / f"{name}.pub"
163
+ if public_path.exists():
164
+ return public_path.read_bytes()
165
+ return None
166
+
167
+ def list_keys(self) -> list:
168
+ """List available key names."""
169
+ keys = []
170
+ for path in self.key_dir.glob("*.pub"):
171
+ keys.append(path.stem)
172
+ return keys
173
+
174
+ def sign_content(self, content: bytes, keypair: KeyPair) -> Signature:
175
+ """
176
+ Sign content with Ed25519 private key.
177
+
178
+ Args:
179
+ content: Content to sign
180
+ keypair: Key pair to use
181
+
182
+ Returns:
183
+ Signature object
184
+ """
185
+ if not CRYPTO_AVAILABLE:
186
+ raise ImportError("Cryptography library not installed")
187
+
188
+ # Load private key
189
+ private_key = serialization.load_pem_private_key(
190
+ keypair.private_key,
191
+ password=None,
192
+ )
193
+
194
+ # Sign
195
+ signature_bytes = private_key.sign(content)
196
+
197
+ return Signature(
198
+ signature=base64.b64encode(signature_bytes).decode("ascii"),
199
+ key_id=keypair.key_id,
200
+ algorithm="ed25519",
201
+ content_hash=hashlib.sha256(content).hexdigest(),
202
+ )
203
+
204
+ def verify_signature(
205
+ self,
206
+ content: bytes,
207
+ signature: Signature,
208
+ public_key_pem: bytes,
209
+ ) -> Tuple[bool, str]:
210
+ """
211
+ Verify content signature.
212
+
213
+ Args:
214
+ content: Content that was signed
215
+ signature: Signature to verify
216
+ public_key_pem: PEM-encoded public key
217
+
218
+ Returns:
219
+ Tuple of (is_valid, message)
220
+ """
221
+ if not CRYPTO_AVAILABLE:
222
+ return (False, "Cryptography library not installed")
223
+
224
+ # Verify content hash
225
+ content_hash = hashlib.sha256(content).hexdigest()
226
+ if content_hash != signature.content_hash:
227
+ return (False, "Content hash mismatch - content may have been modified")
228
+
229
+ try:
230
+ # Load public key
231
+ public_key = serialization.load_pem_public_key(public_key_pem)
232
+
233
+ # Verify signature
234
+ signature_bytes = base64.b64decode(signature.signature)
235
+ public_key.verify(signature_bytes, content)
236
+
237
+ return (True, "Signature verified successfully")
238
+
239
+ except InvalidSignature:
240
+ return (False, "Invalid signature - content may have been tampered with")
241
+ except Exception as e:
242
+ return (False, f"Verification error: {e}")
243
+
244
+
245
+ def sign_package(
246
+ package_path: Path,
247
+ key_name: str = "default",
248
+ key_dir: Optional[Path] = None,
249
+ ) -> Tuple[bool, str]:
250
+ """
251
+ Sign a package file.
252
+
253
+ Args:
254
+ package_path: Path to .yhpkg file
255
+ key_name: Name of key to use
256
+ key_dir: Key directory
257
+
258
+ Returns:
259
+ Tuple of (success, message)
260
+ """
261
+ manager = SignatureManager(key_dir)
262
+
263
+ keypair = manager.load_keypair(key_name)
264
+ if not keypair:
265
+ return (False, f"Key '{key_name}' not found. Generate with: yuho key generate")
266
+
267
+ try:
268
+ content = package_path.read_bytes()
269
+ signature = manager.sign_content(content, keypair)
270
+
271
+ # Write signature file
272
+ sig_path = package_path.with_suffix(".yhpkg.sig")
273
+ sig_data = {
274
+ "signature": signature.signature,
275
+ "key_id": signature.key_id,
276
+ "algorithm": signature.algorithm,
277
+ "content_hash": signature.content_hash,
278
+ }
279
+ sig_path.write_text(json.dumps(sig_data, indent=2))
280
+
281
+ return (True, f"Package signed with key {signature.key_id}")
282
+
283
+ except Exception as e:
284
+ return (False, f"Signing failed: {e}")
285
+
286
+
287
+ def verify_package(
288
+ package_path: Path,
289
+ trusted_keys_dir: Optional[Path] = None,
290
+ ) -> Tuple[bool, str]:
291
+ """
292
+ Verify a package signature.
293
+
294
+ Args:
295
+ package_path: Path to .yhpkg file
296
+ trusted_keys_dir: Directory containing trusted public keys
297
+
298
+ Returns:
299
+ Tuple of (is_valid, message)
300
+ """
301
+ manager = SignatureManager(trusted_keys_dir)
302
+
303
+ sig_path = package_path.with_suffix(".yhpkg.sig")
304
+ if not sig_path.exists():
305
+ return (False, "No signature file found")
306
+
307
+ try:
308
+ # Load signature
309
+ sig_data = json.loads(sig_path.read_text())
310
+ signature = Signature(
311
+ signature=sig_data["signature"],
312
+ key_id=sig_data["key_id"],
313
+ algorithm=sig_data["algorithm"],
314
+ content_hash=sig_data["content_hash"],
315
+ )
316
+
317
+ # Find matching public key
318
+ public_key = None
319
+ for key_name in manager.list_keys():
320
+ key_pem = manager.load_public_key(key_name)
321
+ if key_pem:
322
+ key_id = hashlib.sha256(key_pem).hexdigest()[:16]
323
+ if key_id == signature.key_id:
324
+ public_key = key_pem
325
+ break
326
+
327
+ if not public_key:
328
+ return (False, f"No trusted key found for key_id {signature.key_id}")
329
+
330
+ # Verify
331
+ content = package_path.read_bytes()
332
+ return manager.verify_signature(content, signature, public_key)
333
+
334
+ except Exception as e:
335
+ return (False, f"Verification failed: {e}")
yuho/llm/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ """
2
+ Yuho LLM module - local-first LLM integration.
3
+
4
+ Supports:
5
+ - Ollama (local)
6
+ - HuggingFace Transformers (local)
7
+ - OpenAI API (cloud)
8
+ - Anthropic API (cloud)
9
+
10
+ Local providers are preferred, with cloud fallback.
11
+ """
12
+
13
+ from yuho.llm.config import LLMConfig
14
+ from yuho.llm.providers import (
15
+ LLMProvider,
16
+ OllamaProvider,
17
+ HuggingFaceProvider,
18
+ )
19
+ from yuho.llm.factory import get_provider, LLMProviderFactory
20
+ from yuho.llm.prompts import (
21
+ STATUTE_EXPLANATION_PROMPT,
22
+ STATUTE_TO_YUHO_PROMPT,
23
+ )
24
+ from yuho.llm.utils import (
25
+ ResponseCache,
26
+ RateLimiter,
27
+ TokenCounter,
28
+ PromptCompressor,
29
+ )
30
+
31
+ __all__ = [
32
+ "LLMConfig",
33
+ "LLMProvider",
34
+ "OllamaProvider",
35
+ "HuggingFaceProvider",
36
+ "get_provider",
37
+ "LLMProviderFactory",
38
+ "STATUTE_EXPLANATION_PROMPT",
39
+ "STATUTE_TO_YUHO_PROMPT",
40
+ # Utilities
41
+ "ResponseCache",
42
+ "RateLimiter",
43
+ "TokenCounter",
44
+ "PromptCompressor",
45
+ ]
yuho/llm/config.py ADDED
@@ -0,0 +1,75 @@
1
+ """
2
+ LLM configuration dataclass.
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Optional, List
7
+ from enum import Enum
8
+
9
+
10
+ class ProviderType(Enum):
11
+ """Supported LLM providers."""
12
+ OLLAMA = "ollama"
13
+ HUGGINGFACE = "huggingface"
14
+ OPENAI = "openai"
15
+ ANTHROPIC = "anthropic"
16
+
17
+
18
+ @dataclass
19
+ class LLMConfig:
20
+ """
21
+ Configuration for LLM provider.
22
+
23
+ Attributes:
24
+ provider: The provider type (ollama, huggingface, openai, anthropic)
25
+ model_name: Model identifier (e.g., "llama3", "gpt-4", "claude-3-opus")
26
+ api_key: API key for cloud providers
27
+ base_url: Base URL for API (useful for custom endpoints)
28
+ ollama_host: Ollama server host (default: localhost)
29
+ ollama_port: Ollama server port (default: 11434)
30
+ huggingface_cache: Cache directory for HuggingFace models
31
+ max_tokens: Default max tokens for generation
32
+ temperature: Generation temperature
33
+ fallback_providers: List of fallback providers to try
34
+ """
35
+
36
+ provider: str = "ollama"
37
+ model_name: str = "llama3"
38
+ api_key: Optional[str] = None
39
+ base_url: Optional[str] = None
40
+ ollama_host: str = "localhost"
41
+ ollama_port: int = 11434
42
+ huggingface_cache: Optional[str] = None
43
+ max_tokens: int = 2048
44
+ temperature: float = 0.7
45
+ fallback_providers: List[str] = field(default_factory=list)
46
+
47
+ @property
48
+ def provider_type(self) -> ProviderType:
49
+ """Get provider type enum."""
50
+ return ProviderType(self.provider.lower())
51
+
52
+ @property
53
+ def ollama_url(self) -> str:
54
+ """Get full Ollama API URL."""
55
+ return f"http://{self.ollama_host}:{self.ollama_port}"
56
+
57
+ @classmethod
58
+ def from_dict(cls, data: dict) -> "LLMConfig":
59
+ """Create config from dictionary."""
60
+ return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
61
+
62
+ def to_dict(self) -> dict:
63
+ """Convert to dictionary."""
64
+ return {
65
+ "provider": self.provider,
66
+ "model_name": self.model_name,
67
+ "api_key": "***" if self.api_key else None, # Redact API key
68
+ "base_url": self.base_url,
69
+ "ollama_host": self.ollama_host,
70
+ "ollama_port": self.ollama_port,
71
+ "huggingface_cache": self.huggingface_cache,
72
+ "max_tokens": self.max_tokens,
73
+ "temperature": self.temperature,
74
+ "fallback_providers": self.fallback_providers,
75
+ }
yuho/llm/factory.py ADDED
@@ -0,0 +1,123 @@
1
+ """
2
+ LLM provider factory with fallback support.
3
+ """
4
+
5
+ import logging
6
+ from typing import Optional, List
7
+
8
+ from yuho.llm.config import LLMConfig, ProviderType
9
+ from yuho.llm.providers import (
10
+ LLMProvider,
11
+ OllamaProvider,
12
+ HuggingFaceProvider,
13
+ OpenAIProvider,
14
+ AnthropicProvider,
15
+ )
16
+ from yuho.config.mask import mask_error
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class LLMProviderFactory:
22
+ """
23
+ Factory for creating LLM providers from config.
24
+
25
+ Supports automatic fallback to alternative providers.
26
+ """
27
+
28
+ _provider_classes = {
29
+ ProviderType.OLLAMA: OllamaProvider,
30
+ ProviderType.HUGGINGFACE: HuggingFaceProvider,
31
+ ProviderType.OPENAI: OpenAIProvider,
32
+ ProviderType.ANTHROPIC: AnthropicProvider,
33
+ }
34
+
35
+ @classmethod
36
+ def create(cls, config: LLMConfig) -> LLMProvider:
37
+ """
38
+ Create a provider from config.
39
+
40
+ Args:
41
+ config: LLM configuration
42
+
43
+ Returns:
44
+ An LLMProvider instance
45
+
46
+ Raises:
47
+ ValueError: If provider type is unknown
48
+ """
49
+ provider_type = config.provider_type
50
+ provider_class = cls._provider_classes.get(provider_type)
51
+
52
+ if not provider_class:
53
+ raise ValueError(f"Unknown provider type: {config.provider}")
54
+
55
+ return provider_class(config)
56
+
57
+ @classmethod
58
+ def create_with_fallback(cls, config: LLMConfig) -> LLMProvider:
59
+ """
60
+ Create a provider with automatic fallback.
61
+
62
+ Tries the primary provider first, then falls back to
63
+ configured alternatives if unavailable.
64
+ """
65
+ # Try primary provider
66
+ try:
67
+ primary = cls.create(config)
68
+ if primary.is_available():
69
+ logger.info(f"Using {config.provider} provider")
70
+ return primary
71
+ except Exception as e:
72
+ logger.warning(f"Primary provider {config.provider} failed: {mask_error(e)}")
73
+
74
+ # Try fallback providers
75
+ for fallback_name in config.fallback_providers:
76
+ try:
77
+ fallback_config = LLMConfig(
78
+ provider=fallback_name,
79
+ model_name=config.model_name,
80
+ api_key=config.api_key,
81
+ base_url=config.base_url,
82
+ ollama_host=config.ollama_host,
83
+ ollama_port=config.ollama_port,
84
+ huggingface_cache=config.huggingface_cache,
85
+ max_tokens=config.max_tokens,
86
+ temperature=config.temperature,
87
+ )
88
+ fallback = cls.create(fallback_config)
89
+ if fallback.is_available():
90
+ logger.info(f"Falling back to {fallback_name} provider")
91
+ return fallback
92
+ except Exception as e:
93
+ logger.warning(f"Fallback provider {fallback_name} failed: {mask_error(e)}")
94
+
95
+ # Last resort: try Ollama with default settings
96
+ try:
97
+ default_ollama = OllamaProvider(LLMConfig(provider="ollama", model_name="llama3"))
98
+ if default_ollama.is_available():
99
+ logger.info("Falling back to default Ollama")
100
+ return default_ollama
101
+ except Exception:
102
+ pass
103
+
104
+ raise RuntimeError(
105
+ "No LLM provider available. Install and configure at least one of: "
106
+ "Ollama, HuggingFace, OpenAI, or Anthropic"
107
+ )
108
+
109
+
110
+ def get_provider(config: Optional[LLMConfig] = None) -> LLMProvider:
111
+ """
112
+ Get an LLM provider, using fallback if needed.
113
+
114
+ Args:
115
+ config: Optional config. Uses defaults if not provided.
116
+
117
+ Returns:
118
+ An LLMProvider instance
119
+ """
120
+ if config is None:
121
+ config = LLMConfig()
122
+
123
+ return LLMProviderFactory.create_with_fallback(config)