kryten-llm 0.2.2__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,496 @@
1
+ """Configuration management for kryten-llm."""
2
+
3
+ from kryten import KrytenConfig # type: ignore[import-untyped]
4
+ from pydantic import BaseModel, Field
5
+
6
+ # ============================================================================
7
+ # LLM-Specific Configuration Models
8
+ # ============================================================================
9
+
10
+
11
+ class PersonalityConfig(BaseModel):
12
+ """Bot personality configuration."""
13
+
14
+ character_name: str = Field(default="CynthiaRothbot", description="Bot character name")
15
+ character_description: str = Field(
16
+ default="legendary martial artist and actress",
17
+ description="Character description for LLM context",
18
+ )
19
+ personality_traits: list[str] = Field(
20
+ default=["confident", "action-oriented", "pithy", "martial arts expert"],
21
+ description="List of personality traits",
22
+ )
23
+ expertise: list[str] = Field(
24
+ default=["kung fu", "action movies", "martial arts", "B-movies"],
25
+ description="Areas of expertise",
26
+ )
27
+ response_style: str = Field(default="short and punchy", description="Desired response style")
28
+ name_variations: list[str] = Field(
29
+ default=["cynthia", "rothrock", "cynthiarothbot"],
30
+ description="Alternative names that trigger mentions",
31
+ )
32
+
33
+
34
+ class LLMProvider(BaseModel):
35
+ """LLM provider configuration.
36
+
37
+ Phase 3 enhancement: Added priority, max_retries, and custom_headers
38
+ to support multi-provider fallback strategy (REQ-001, REQ-003, REQ-007, REQ-024).
39
+ """
40
+
41
+ name: str = Field(description="Provider identifier")
42
+ type: str = Field(description="Provider type: openai_compatible, openrouter, anthropic")
43
+ base_url: str = Field(description="API base URL")
44
+ api_key: str = Field(description="API key for authentication")
45
+ model: str = Field(description="Model identifier")
46
+ max_tokens: int = Field(default=256, description="Maximum tokens in response", ge=1, le=4096)
47
+ temperature: float = Field(default=0.8, description="Sampling temperature", ge=0.0, le=2.0)
48
+ timeout_seconds: int = Field(default=30, description="Request timeout", ge=1, le=120)
49
+ max_retries: int = Field(
50
+ default=3, description="Maximum retry attempts per provider", ge=0, le=10
51
+ )
52
+ priority: int = Field(
53
+ default=99, description="Provider priority (lower number = higher priority)", ge=1
54
+ )
55
+ custom_headers: dict[str, str] | None = Field(
56
+ default=None, description="Custom HTTP headers for provider"
57
+ )
58
+ fallback: str | None = Field(
59
+ default=None,
60
+ description="Fallback provider name on failure (deprecated, use priority instead)",
61
+ )
62
+
63
+
64
+ class Trigger(BaseModel):
65
+ """Trigger word configuration.
66
+
67
+ Phase 3 enhancement: Added preferred_provider to support trigger-specific
68
+ provider selection (REQ-004, REQ-022).
69
+ """
70
+
71
+ name: str = Field(description="Trigger identifier")
72
+ patterns: list[str] = Field(description="List of regex patterns or strings to match")
73
+ probability: float = Field(
74
+ default=1.0, description="Probability of responding (0.0-1.0)", ge=0.0, le=1.0
75
+ )
76
+ cooldown_seconds: int = Field(
77
+ default=300, description="Cooldown between trigger activations", ge=0
78
+ )
79
+ context: str = Field(default="", description="Additional context to inject into prompt")
80
+ response_style: str | None = Field(
81
+ default=None, description="Override response style for this trigger"
82
+ )
83
+ max_responses_per_hour: int = Field(
84
+ default=10, description="Maximum responses per hour for this trigger", ge=0
85
+ )
86
+ priority: int = Field(
87
+ default=5, description="Trigger priority (higher = more important)", ge=1, le=10
88
+ )
89
+ enabled: bool = Field(default=True, description="Whether trigger is active")
90
+ llm_provider: str | None = Field(
91
+ default=None, description="Specific LLM provider for this trigger (deprecated)"
92
+ )
93
+ preferred_provider: str | None = Field(
94
+ default=None, description="Preferred LLM provider for this trigger (Phase 3)"
95
+ )
96
+
97
+
98
+ class RateLimits(BaseModel):
99
+ """Rate limiting configuration."""
100
+
101
+ global_max_per_minute: int = Field(default=2, ge=0)
102
+ global_max_per_hour: int = Field(default=20, ge=0)
103
+ global_cooldown_seconds: int = Field(default=15, ge=0)
104
+ user_max_per_hour: int = Field(default=5, ge=0)
105
+ user_cooldown_seconds: int = Field(default=60, ge=0)
106
+ mention_cooldown_seconds: int = Field(default=120, ge=0)
107
+ admin_cooldown_multiplier: float = Field(default=0.5, ge=0.0, le=1.0)
108
+ admin_limit_multiplier: float = Field(default=2.0, ge=1.0)
109
+
110
+
111
+ class MessageProcessing(BaseModel):
112
+ """Message processing configuration."""
113
+
114
+ max_message_length: int = Field(default=240, ge=1, le=255)
115
+ split_delay_seconds: int = Field(default=2, ge=0, le=10)
116
+ filter_emoji: bool = Field(default=False)
117
+ max_emoji_per_message: int = Field(default=3, ge=0)
118
+
119
+
120
+ class TestingConfig(BaseModel):
121
+ """Testing and development configuration."""
122
+
123
+ dry_run: bool = Field(default=False)
124
+ log_responses: bool = Field(default=True)
125
+ log_file: str = Field(default="logs/llm-responses.jsonl")
126
+ send_to_chat: bool = Field(default=True)
127
+
128
+
129
+ class ContextConfig(BaseModel):
130
+ """Context management configuration.
131
+
132
+ Phase 3: Controls video and chat history context injection into prompts
133
+ (REQ-008 through REQ-013, REQ-023).
134
+ """
135
+
136
+ chat_history_size: int = Field(
137
+ default=30, ge=0, le=100, description="Number of messages to buffer"
138
+ )
139
+ context_window_chars: int = Field(
140
+ default=12000, ge=1000, description="Approximate context limit in characters"
141
+ )
142
+ include_video_context: bool = Field(
143
+ default=True, description="Include current video in prompts"
144
+ )
145
+ include_chat_history: bool = Field(default=True, description="Include recent chat in prompts")
146
+ max_video_title_length: int = Field(
147
+ default=200, ge=50, le=500, description="Maximum video title length"
148
+ )
149
+ max_chat_history_in_prompt: int = Field(
150
+ default=10, ge=0, le=50, description="Maximum chat messages in prompt"
151
+ )
152
+
153
+
154
+ class FormattingConfig(BaseModel):
155
+ """Response formatting configuration.
156
+
157
+ Phase 4: Controls intelligent response formatting (REQ-001 through REQ-008).
158
+ """
159
+
160
+ max_message_length: int = Field(
161
+ default=255, ge=100, le=500, description="Maximum message length"
162
+ )
163
+ continuation_indicator: str = Field(
164
+ default=" ...", description="Continuation indicator for multi-part messages"
165
+ )
166
+ enable_emoji_limiting: bool = Field(default=False, description="Enable emoji count limiting")
167
+ max_emoji_per_message: int | None = Field(
168
+ default=None, ge=1, description="Maximum emoji per message (if enabled)"
169
+ )
170
+ remove_self_references: bool = Field(
171
+ default=True, description="Remove self-referential phrases"
172
+ )
173
+ remove_llm_artifacts: bool = Field(default=True, description="Remove common LLM artifacts")
174
+ artifact_patterns: list[str] = Field(
175
+ default=[
176
+ r"^Here's ",
177
+ r"^Let me ",
178
+ r"^Sure!\\s*",
179
+ r"\\bAs an AI\\b",
180
+ r"^I think ",
181
+ r"^In my opinion ",
182
+ ],
183
+ description="Regex patterns for LLM artifacts to remove",
184
+ )
185
+
186
+
187
+ class ValidationConfig(BaseModel):
188
+ """Response validation configuration.
189
+
190
+ Phase 4: Controls response quality validation (REQ-009 through REQ-015).
191
+ """
192
+
193
+ min_length: int = Field(default=10, ge=1, description="Minimum response length in characters")
194
+ max_length: int = Field(
195
+ default=2000, ge=100, description="Maximum response length before splitting"
196
+ )
197
+ check_repetition: bool = Field(default=True, description="Check for repetitive responses")
198
+ repetition_history_size: int = Field(
199
+ default=10, ge=1, le=50, description="Number of responses to track for repetition"
200
+ )
201
+ repetition_threshold: float = Field(
202
+ default=0.9, ge=0.0, le=1.0, description="Similarity threshold for repetition (0.0-1.0)"
203
+ )
204
+ check_relevance: bool = Field(default=False, description="Check response relevance to input")
205
+ relevance_threshold: float = Field(
206
+ default=0.5, ge=0.0, le=1.0, description="Minimum relevance score"
207
+ )
208
+ inappropriate_patterns: list[str] = Field(
209
+ default=[], description="Regex patterns for inappropriate content"
210
+ )
211
+ check_inappropriate: bool = Field(default=False, description="Check for inappropriate content")
212
+
213
+
214
+ class MessageWindow(BaseModel):
215
+ """Time window for message rate limiting.
216
+
217
+ Phase 4: Used by spam detection (REQ-016).
218
+ """
219
+
220
+ seconds: int = Field(ge=1, description="Time window in seconds")
221
+ max_messages: int = Field(ge=1, description="Maximum messages allowed in window")
222
+
223
+
224
+ class SpamDetectionConfig(BaseModel):
225
+ """Spam detection configuration.
226
+
227
+ Phase 4: Controls user spam detection and penalties (REQ-016 through REQ-022).
228
+ Supports both structured MessageWindow format and simple threshold format from config.json.
229
+ """
230
+
231
+ enabled: bool = Field(default=True, description="Enable spam detection")
232
+
233
+ # Rate limiting windows
234
+ message_windows: list[MessageWindow] = Field(
235
+ default_factory=lambda: [
236
+ MessageWindow(seconds=60, max_messages=5),
237
+ MessageWindow(seconds=300, max_messages=10),
238
+ MessageWindow(seconds=900, max_messages=20),
239
+ ],
240
+ description="Message rate limit windows",
241
+ )
242
+
243
+ # Identical message detection - supports both formats
244
+ identical_message_window: MessageWindow | None = Field(
245
+ default=None, description="Window for identical message detection (structured format)"
246
+ )
247
+ identical_message_threshold: int = Field(
248
+ default=3, ge=1, description="Max identical messages before spam (simple format)"
249
+ )
250
+
251
+ # Mention spam detection - supports both formats
252
+ mention_spam_window: MessageWindow | int = Field(
253
+ default=30, description="Window for mention spam - int (seconds) or MessageWindow"
254
+ )
255
+ mention_spam_threshold: int = Field(
256
+ default=3, ge=1, description="Max mentions in window before spam"
257
+ )
258
+
259
+ # Penalty configuration
260
+ initial_penalty: int = Field(
261
+ default=30, ge=1, description="Initial penalty duration in seconds"
262
+ )
263
+ penalty_multiplier: float = Field(
264
+ default=2.0, ge=1.0, description="Penalty duration multiplier"
265
+ )
266
+ max_penalty: int = Field(default=600, ge=60, description="Maximum penalty duration in seconds")
267
+ penalty_durations: list[int] | None = Field(
268
+ default=None,
269
+ description="Explicit penalty durations (overrides initial_penalty/multiplier if set)",
270
+ )
271
+
272
+ # Reset and exemptions
273
+ clean_period: int = Field(
274
+ default=600, ge=60, description="Clean period to reset offense counts in seconds"
275
+ )
276
+ admin_exempt_ranks: list[int] = Field(
277
+ default=[3, 4, 5], description="User ranks exempt from spam detection"
278
+ )
279
+
280
+ # Backwards compatibility aliases
281
+ @property
282
+ def max_penalty_duration(self) -> int:
283
+ """Alias for max_penalty."""
284
+ return self.max_penalty
285
+
286
+ @property
287
+ def clean_period_for_reset(self) -> int:
288
+ """Alias for clean_period."""
289
+ return self.clean_period
290
+
291
+ @property
292
+ def admin_ranks(self) -> list[int]:
293
+ """Alias for admin_exempt_ranks."""
294
+ return self.admin_exempt_ranks
295
+
296
+ def get_identical_message_window(self) -> MessageWindow:
297
+ """Get identical message window, handling both formats."""
298
+ if self.identical_message_window:
299
+ return self.identical_message_window
300
+ # Create from simple threshold
301
+ return MessageWindow(seconds=300, max_messages=self.identical_message_threshold)
302
+
303
+ def get_mention_spam_window(self) -> MessageWindow:
304
+ """Get mention spam window, handling both formats."""
305
+ if isinstance(self.mention_spam_window, MessageWindow):
306
+ return self.mention_spam_window
307
+ # Create from simple int (seconds) + threshold
308
+ return MessageWindow(
309
+ seconds=self.mention_spam_window, max_messages=self.mention_spam_threshold
310
+ )
311
+
312
+ def get_penalty_durations(self) -> list[int]:
313
+ """Get penalty durations, calculating if not explicit."""
314
+ if self.penalty_durations:
315
+ return self.penalty_durations
316
+ # Calculate from initial_penalty and multiplier
317
+ durations = []
318
+ current: float = self.initial_penalty
319
+ while current <= self.max_penalty:
320
+ durations.append(int(current))
321
+ current = current * self.penalty_multiplier
322
+ return durations or [self.initial_penalty]
323
+
324
+
325
+ class ErrorHandlingConfig(BaseModel):
326
+ """Error handling configuration.
327
+
328
+ Phase 4: Controls error handling and fallback responses (REQ-023 through REQ-028).
329
+ """
330
+
331
+ enable_fallback_responses: bool = Field(
332
+ default=False, description="Enable fallback responses on errors"
333
+ )
334
+ fallback_messages: list[str] = Field(
335
+ default=[
336
+ "I'm having trouble thinking right now. Try again later!",
337
+ "My circuits are a bit scrambled. Give me a moment!",
338
+ "ERROR: Brain.exe has stopped responding.",
339
+ ],
340
+ description="Fallback messages for errors",
341
+ )
342
+ log_full_context: bool = Field(default=True, description="Log full context on errors")
343
+ generate_correlation_ids: bool = Field(
344
+ default=True, description="Generate correlation IDs for request tracking"
345
+ )
346
+
347
+
348
+ class ServiceMetadata(BaseModel):
349
+ """Service discovery and monitoring configuration.
350
+
351
+ Phase 5: Service discovery configuration (REQ-009).
352
+ Controls how the service announces itself to the Kryten ecosystem
353
+ and publishes health/heartbeat information.
354
+ """
355
+
356
+ service_name: str = Field(default="llm", description="Service identifier for discovery")
357
+
358
+ service_version: str = Field(default="1.0.0", description="Service version string")
359
+
360
+ heartbeat_interval_seconds: int = Field(
361
+ default=10, ge=1, le=60, description="Heartbeat publishing interval in seconds"
362
+ )
363
+
364
+ enable_service_discovery: bool = Field(
365
+ default=True, description="Enable service discovery announcements"
366
+ )
367
+
368
+ enable_heartbeats: bool = Field(
369
+ default=True, description="Enable periodic heartbeat publishing"
370
+ )
371
+
372
+ graceful_shutdown_timeout_seconds: int = Field(
373
+ default=30, ge=5, le=120, description="Maximum time to wait for graceful shutdown"
374
+ )
375
+
376
+
377
+ # ============================================================================
378
+ # Main Configuration (Extends KrytenConfig)
379
+ # ============================================================================
380
+
381
+
382
+ class RetryStrategy(BaseModel):
383
+ """Retry strategy configuration for LLM providers.
384
+
385
+ Phase 3: Exponential backoff configuration (REQ-003).
386
+ """
387
+
388
+ initial_delay: float = Field(
389
+ default=1.0, ge=0.1, le=10.0, description="Initial retry delay in seconds"
390
+ )
391
+ multiplier: float = Field(
392
+ default=2.0, ge=1.0, le=5.0, description="Delay multiplier for exponential backoff"
393
+ )
394
+ max_delay: float = Field(
395
+ default=30.0, ge=1.0, le=120.0, description="Maximum retry delay in seconds"
396
+ )
397
+
398
+
399
+ class LLMConfig(KrytenConfig):
400
+ """Extended configuration for kryten-llm service.
401
+
402
+ Inherits NATS and channel configuration from KrytenConfig.
403
+ Adds LLM-specific settings for personality, providers, triggers, etc.
404
+
405
+ Phase 3 enhancements: Multi-provider support with fallback, retry strategy,
406
+ and default provider priority order (REQ-002, REQ-003, REQ-021).
407
+ """
408
+
409
+ # LLM-specific configuration
410
+ personality: PersonalityConfig = Field(
411
+ default_factory=PersonalityConfig, description="Bot personality configuration"
412
+ )
413
+ llm_providers: dict[str, LLMProvider] = Field(description="LLM provider configurations")
414
+ default_provider: str = Field(default="local", description="Default LLM provider name")
415
+ default_provider_priority: list[str] = Field(
416
+ default_factory=list, description="Default provider priority order (Phase 3)"
417
+ )
418
+ retry_strategy: RetryStrategy = Field(
419
+ default_factory=RetryStrategy, description="Retry strategy for provider failures (Phase 3)"
420
+ )
421
+ triggers: list[Trigger] = Field(default_factory=list, description="Trigger word configurations")
422
+ rate_limits: RateLimits = Field(
423
+ default_factory=RateLimits, description="Rate limiting configuration"
424
+ )
425
+ message_processing: MessageProcessing = Field(
426
+ default_factory=MessageProcessing, description="Message processing settings"
427
+ )
428
+ testing: TestingConfig = Field(
429
+ default_factory=TestingConfig, description="Testing configuration"
430
+ )
431
+ context: ContextConfig = Field(
432
+ default_factory=ContextConfig, description="Context management settings"
433
+ )
434
+ formatting: FormattingConfig = Field(
435
+ default_factory=FormattingConfig, description="Response formatting settings (Phase 4)"
436
+ )
437
+ validation: ValidationConfig = Field(
438
+ default_factory=ValidationConfig, description="Response validation settings (Phase 4)"
439
+ )
440
+ spam_detection: SpamDetectionConfig = Field(
441
+ default_factory=SpamDetectionConfig, description="Spam detection settings (Phase 4)"
442
+ )
443
+ error_handling: ErrorHandlingConfig = Field(
444
+ default_factory=ErrorHandlingConfig, description="Error handling settings (Phase 4)"
445
+ )
446
+ service_metadata: ServiceMetadata = Field(
447
+ default_factory=ServiceMetadata,
448
+ description="Service discovery and monitoring settings (Phase 5)",
449
+ )
450
+
451
+ def validate_config(self) -> tuple[bool, list[str]]:
452
+ """Validate configuration and return (is_valid, errors)."""
453
+ errors = []
454
+
455
+ # Validate default provider exists
456
+ if self.default_provider not in self.llm_providers:
457
+ errors.append(f"Default provider '{self.default_provider}' not found in llm_providers")
458
+
459
+ # Validate fallback providers exist
460
+ for provider_name, provider in self.llm_providers.items():
461
+ if provider.fallback and provider.fallback not in self.llm_providers:
462
+ errors.append(
463
+ f"Provider '{provider_name}' has invalid fallback '{provider.fallback}'"
464
+ )
465
+
466
+ # Validate trigger LLM providers
467
+ for trigger in self.triggers:
468
+ if trigger.llm_provider and trigger.llm_provider not in self.llm_providers:
469
+ errors.append(
470
+ f"Trigger '{trigger.name}' has invalid llm_provider '{trigger.llm_provider}'"
471
+ )
472
+
473
+ return (len(errors) == 0, errors)
474
+
475
+ def model_dump(self, **kwargs: object) -> dict[str, object]:
476
+ """Override to transform service_metadata to service for KrytenClient compatibility.
477
+
478
+ KrytenClient expects a 'service' key with specific field names.
479
+ This transforms our 'service_metadata' structure to match.
480
+ """
481
+ data: dict[str, object] = super().model_dump(**kwargs)
482
+
483
+ # Transform service_metadata to service format expected by KrytenClient
484
+ if "service_metadata" in data:
485
+ sm = data["service_metadata"]
486
+ if isinstance(sm, dict):
487
+ data["service"] = {
488
+ "name": sm.get("service_name", "llm"),
489
+ "version": sm.get("service_version", "1.0.0"),
490
+ "heartbeat_interval": sm.get("heartbeat_interval_seconds", 30),
491
+ "enable_heartbeat": sm.get("enable_heartbeats", True),
492
+ "enable_discovery": sm.get("enable_service_discovery", True),
493
+ "enable_lifecycle": True, # Always enable lifecycle events
494
+ }
495
+
496
+ return data
@@ -0,0 +1,16 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class TriggerResult:
6
+ """Result of trigger detection."""
7
+
8
+ triggered: bool
9
+ trigger_type: str | None = None
10
+ trigger_name: str | None = None
11
+ cleaned_message: str | None = None
12
+ context: str | None = None
13
+ priority: int = 5
14
+
15
+ def __bool__(self) -> bool:
16
+ return self.triggered
@@ -0,0 +1,59 @@
1
+ """Phase 3 data models for multi-provider LLM and context management."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from typing import Optional
6
+
7
+
8
+ @dataclass
9
+ class VideoMetadata:
10
+ """Current video information from CyTube.
11
+
12
+ Phase 3: Tracks current video for context injection (REQ-008, REQ-009).
13
+ """
14
+
15
+ title: str
16
+ duration: int # seconds
17
+ type: str # "yt", "vm", "dm", etc.
18
+ queued_by: str
19
+ timestamp: datetime
20
+
21
+
22
+ @dataclass
23
+ class ChatMessage:
24
+ """A chat message for history buffer.
25
+
26
+ Phase 3: Stored in rolling buffer for context injection (REQ-010).
27
+ """
28
+
29
+ username: str
30
+ message: str
31
+ timestamp: datetime
32
+
33
+
34
+ @dataclass
35
+ class LLMRequest:
36
+ """Request to LLM provider.
37
+
38
+ Phase 3: Enhanced with preferred_provider for trigger-specific routing (REQ-004).
39
+ """
40
+
41
+ system_prompt: str
42
+ user_prompt: str
43
+ temperature: float = 0.7
44
+ max_tokens: int = 500
45
+ preferred_provider: Optional[str] = None
46
+
47
+
48
+ @dataclass
49
+ class LLMResponse:
50
+ """Response from LLM provider.
51
+
52
+ Phase 3: Includes provider metrics for logging and monitoring (REQ-006).
53
+ """
54
+
55
+ content: str
56
+ provider_used: str
57
+ model_used: str
58
+ tokens_used: Optional[int] = None
59
+ response_time: float = 0.0