intentkit 0.7.5.dev13__py3-none-any.whl → 0.7.5.dev15__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 intentkit might be problematic. Click here for more details.

intentkit/models/llm.py CHANGED
@@ -1,8 +1,10 @@
1
+ import csv
1
2
  import json
2
3
  import logging
3
4
  from datetime import datetime, timezone
4
5
  from decimal import ROUND_HALF_UP, Decimal
5
6
  from enum import Enum
7
+ from pathlib import Path
6
8
  from typing import Annotated, Any, Optional
7
9
 
8
10
  from intentkit.models.app_setting import AppSetting
@@ -13,6 +15,7 @@ from intentkit.utils.error import IntentKitLookUpError
13
15
  from langchain_core.language_models import LanguageModelLike
14
16
  from pydantic import BaseModel, ConfigDict, Field
15
17
  from sqlalchemy import Boolean, Column, DateTime, Integer, Numeric, String, func, select
18
+ from sqlalchemy.ext.asyncio import AsyncSession
16
19
 
17
20
  logger = logging.getLogger(__name__)
18
21
 
@@ -20,6 +23,74 @@ _credit_per_usdc = None
20
23
  FOURPLACES = Decimal("0.0001")
21
24
 
22
25
 
26
+ def _parse_bool(value: Optional[str]) -> bool:
27
+ if value is None:
28
+ return False
29
+ return value.strip().lower() in {"true", "1", "yes"}
30
+
31
+
32
+ def _parse_optional_int(value: Optional[str]) -> Optional[int]:
33
+ if value is None:
34
+ return None
35
+ value = value.strip()
36
+ return int(value) if value else None
37
+
38
+
39
+ def _load_default_llm_models() -> dict[str, "LLMModelInfo"]:
40
+ """Load default LLM models from a CSV file."""
41
+
42
+ path = Path(__file__).with_name("llm.csv")
43
+ if not path.exists():
44
+ logger.warning("Default LLM CSV not found at %s", path)
45
+ return {}
46
+
47
+ defaults: dict[str, "LLMModelInfo"] = {}
48
+ with path.open(newline="", encoding="utf-8") as csvfile:
49
+ reader = csv.DictReader(csvfile)
50
+ for row in reader:
51
+ try:
52
+ timestamp = datetime.now(timezone.utc)
53
+ model = LLMModelInfo(
54
+ id=row["id"],
55
+ name=row["name"],
56
+ provider=LLMProvider(row["provider"]),
57
+ enabled=_parse_bool(row.get("enabled")),
58
+ input_price=Decimal(row["input_price"]),
59
+ output_price=Decimal(row["output_price"]),
60
+ price_level=_parse_optional_int(row.get("price_level")),
61
+ context_length=int(row["context_length"]),
62
+ output_length=int(row["output_length"]),
63
+ intelligence=int(row["intelligence"]),
64
+ speed=int(row["speed"]),
65
+ supports_image_input=_parse_bool(row.get("supports_image_input")),
66
+ supports_skill_calls=_parse_bool(row.get("supports_skill_calls")),
67
+ supports_structured_output=_parse_bool(
68
+ row.get("supports_structured_output")
69
+ ),
70
+ has_reasoning=_parse_bool(row.get("has_reasoning")),
71
+ supports_search=_parse_bool(row.get("supports_search")),
72
+ supports_temperature=_parse_bool(row.get("supports_temperature")),
73
+ supports_frequency_penalty=_parse_bool(
74
+ row.get("supports_frequency_penalty")
75
+ ),
76
+ supports_presence_penalty=_parse_bool(
77
+ row.get("supports_presence_penalty")
78
+ ),
79
+ api_base=row.get("api_base", "").strip() or None,
80
+ timeout=int(row.get("timeout", "") or 180),
81
+ created_at=timestamp,
82
+ updated_at=timestamp,
83
+ )
84
+ except Exception as exc:
85
+ logger.error(
86
+ "Failed to load default LLM model %s: %s", row.get("id"), exc
87
+ )
88
+ continue
89
+ defaults[model.id] = model
90
+
91
+ return defaults
92
+
93
+
23
94
  class LLMProvider(str, Enum):
24
95
  OPENAI = "openai"
25
96
  DEEPSEEK = "deepseek"
@@ -210,6 +281,26 @@ class LLMModelInfo(BaseModel):
210
281
  # Not found anywhere
211
282
  raise IntentKitLookUpError(f"Model {model_id} not found")
212
283
 
284
+ @classmethod
285
+ async def get_all(cls, session: AsyncSession | None = None) -> list["LLMModelInfo"]:
286
+ """Return all models merged from defaults and database overrides."""
287
+
288
+ if session is None:
289
+ async with get_session() as db:
290
+ return await cls.get_all(session=db)
291
+
292
+ models: dict[str, "LLMModelInfo"] = {
293
+ model_id: model.model_copy(deep=True)
294
+ for model_id, model in AVAILABLE_MODELS.items()
295
+ }
296
+
297
+ result = await session.execute(select(LLMModelInfoTable))
298
+ for row in result.scalars():
299
+ model_info = cls.model_validate(row)
300
+ models[model_info.id] = model_info
301
+
302
+ return list(models.values())
303
+
213
304
  async def calculate_cost(self, input_tokens: int, output_tokens: int) -> Decimal:
214
305
  global _credit_per_usdc
215
306
  if not _credit_per_usdc:
@@ -230,325 +321,8 @@ class LLMModelInfo(BaseModel):
230
321
  return (input_cost + output_cost).quantize(FOURPLACES, rounding=ROUND_HALF_UP)
231
322
 
232
323
 
233
- # Define all available models
234
- AVAILABLE_MODELS = {
235
- # OpenAI models
236
- "gpt-4o": LLMModelInfo(
237
- id="gpt-4o",
238
- name="GPT-4o",
239
- provider=LLMProvider.OPENAI,
240
- input_price=Decimal("2.50"), # per 1M input tokens
241
- output_price=Decimal("10.00"), # per 1M output tokens
242
- context_length=128000,
243
- output_length=4096,
244
- intelligence=4,
245
- speed=3,
246
- supports_image_input=True,
247
- supports_skill_calls=True,
248
- supports_structured_output=True,
249
- supports_search=True,
250
- supports_frequency_penalty=False,
251
- supports_presence_penalty=False,
252
- ),
253
- "gpt-4o-mini": LLMModelInfo(
254
- id="gpt-4o-mini",
255
- name="GPT-4o Mini",
256
- provider=LLMProvider.OPENAI,
257
- input_price=Decimal("0.15"), # per 1M input tokens
258
- output_price=Decimal("0.60"), # per 1M output tokens
259
- context_length=128000,
260
- output_length=4096,
261
- intelligence=3,
262
- speed=4,
263
- supports_image_input=False,
264
- supports_skill_calls=True,
265
- supports_structured_output=True,
266
- supports_search=True,
267
- supports_frequency_penalty=False,
268
- supports_presence_penalty=False,
269
- ),
270
- "gpt-5-nano": LLMModelInfo(
271
- id="gpt-5-nano",
272
- name="GPT-5 Nano",
273
- provider=LLMProvider.OPENAI,
274
- input_price=Decimal("0.05"), # per 1M input tokens
275
- output_price=Decimal("0.4"), # per 1M output tokens
276
- context_length=400000,
277
- output_length=128000,
278
- intelligence=3,
279
- speed=5,
280
- supports_image_input=True,
281
- supports_skill_calls=True,
282
- supports_structured_output=True,
283
- supports_temperature=False,
284
- supports_frequency_penalty=False,
285
- supports_presence_penalty=False,
286
- ),
287
- "gpt-5-mini": LLMModelInfo(
288
- id="gpt-5-mini",
289
- name="GPT-5 Mini",
290
- provider=LLMProvider.OPENAI,
291
- input_price=Decimal("0.25"), # per 1M input tokens
292
- output_price=Decimal("2"), # per 1M output tokens
293
- context_length=400000,
294
- output_length=128000,
295
- intelligence=4,
296
- speed=4,
297
- supports_image_input=True,
298
- supports_skill_calls=True,
299
- supports_structured_output=True,
300
- supports_search=True,
301
- supports_temperature=False,
302
- supports_frequency_penalty=False,
303
- supports_presence_penalty=False,
304
- ),
305
- "gpt-5": LLMModelInfo(
306
- id="gpt-5",
307
- name="GPT-5",
308
- provider=LLMProvider.OPENAI,
309
- input_price=Decimal("1.25"), # per 1M input tokens
310
- output_price=Decimal("10.00"), # per 1M output tokens
311
- context_length=400000,
312
- output_length=128000,
313
- intelligence=5,
314
- speed=3,
315
- supports_image_input=True,
316
- supports_skill_calls=True,
317
- supports_structured_output=True,
318
- supports_search=True,
319
- supports_temperature=False,
320
- supports_frequency_penalty=False,
321
- supports_presence_penalty=False,
322
- ),
323
- "gpt-4.1-nano": LLMModelInfo(
324
- id="gpt-4.1-nano",
325
- name="GPT-4.1 Nano",
326
- provider=LLMProvider.OPENAI,
327
- input_price=Decimal("0.1"), # per 1M input tokens
328
- output_price=Decimal("0.4"), # per 1M output tokens
329
- context_length=128000,
330
- output_length=4096,
331
- intelligence=3,
332
- speed=5,
333
- supports_image_input=False,
334
- supports_skill_calls=True,
335
- supports_structured_output=True,
336
- supports_frequency_penalty=False,
337
- supports_presence_penalty=False,
338
- ),
339
- "gpt-4.1-mini": LLMModelInfo(
340
- id="gpt-4.1-mini",
341
- name="GPT-4.1 Mini",
342
- provider=LLMProvider.OPENAI,
343
- input_price=Decimal("0.4"), # per 1M input tokens
344
- output_price=Decimal("1.6"), # per 1M output tokens
345
- context_length=128000,
346
- output_length=4096,
347
- intelligence=4,
348
- speed=4,
349
- supports_image_input=False,
350
- supports_skill_calls=True,
351
- supports_structured_output=True,
352
- supports_search=True,
353
- supports_frequency_penalty=False,
354
- supports_presence_penalty=False,
355
- ),
356
- "gpt-4.1": LLMModelInfo(
357
- id="gpt-4.1",
358
- name="GPT-4.1",
359
- provider=LLMProvider.OPENAI,
360
- input_price=Decimal("2.00"), # per 1M input tokens
361
- output_price=Decimal("8.00"), # per 1M output tokens
362
- context_length=128000,
363
- output_length=4096,
364
- intelligence=5,
365
- speed=3,
366
- supports_image_input=True,
367
- supports_skill_calls=True,
368
- supports_structured_output=True,
369
- supports_search=True,
370
- supports_frequency_penalty=False,
371
- supports_presence_penalty=False,
372
- ),
373
- "o4-mini": LLMModelInfo(
374
- id="o4-mini",
375
- name="OpenAI o4-mini",
376
- provider=LLMProvider.OPENAI,
377
- input_price=Decimal("1.10"), # per 1M input tokens
378
- output_price=Decimal("4.40"), # per 1M output tokens
379
- context_length=128000,
380
- output_length=4096,
381
- intelligence=4,
382
- speed=3,
383
- supports_image_input=False,
384
- supports_skill_calls=True,
385
- supports_structured_output=True,
386
- has_reasoning=True, # Has strong reasoning capabilities
387
- supports_temperature=False,
388
- supports_frequency_penalty=False,
389
- supports_presence_penalty=False,
390
- ),
391
- # Deepseek models
392
- "deepseek-chat": LLMModelInfo(
393
- id="deepseek-chat",
394
- name="Deepseek V3 (0324)",
395
- provider=LLMProvider.DEEPSEEK,
396
- input_price=Decimal("0.27"),
397
- output_price=Decimal("1.10"),
398
- context_length=60000,
399
- output_length=4096,
400
- intelligence=4,
401
- speed=3,
402
- supports_image_input=False,
403
- supports_skill_calls=True,
404
- supports_structured_output=True,
405
- api_base="https://api.deepseek.com",
406
- timeout=300,
407
- ),
408
- "deepseek-reasoner": LLMModelInfo(
409
- id="deepseek-reasoner",
410
- name="Deepseek R1",
411
- provider=LLMProvider.DEEPSEEK,
412
- input_price=Decimal("0.55"),
413
- output_price=Decimal("2.19"),
414
- context_length=60000,
415
- output_length=4096,
416
- intelligence=4,
417
- speed=2,
418
- supports_image_input=False,
419
- supports_skill_calls=True,
420
- supports_structured_output=True,
421
- has_reasoning=True, # Has strong reasoning capabilities
422
- api_base="https://api.deepseek.com",
423
- timeout=300,
424
- ),
425
- # XAI models
426
- "grok-2": LLMModelInfo(
427
- id="grok-2",
428
- name="Grok 2",
429
- provider=LLMProvider.XAI,
430
- input_price=Decimal("2"),
431
- output_price=Decimal("10"),
432
- context_length=120000,
433
- output_length=4096,
434
- intelligence=3,
435
- speed=3,
436
- supports_image_input=False,
437
- supports_skill_calls=True,
438
- supports_structured_output=True,
439
- timeout=180,
440
- ),
441
- "grok-3": LLMModelInfo(
442
- id="grok-3",
443
- name="Grok 3",
444
- provider=LLMProvider.XAI,
445
- input_price=Decimal("3"),
446
- output_price=Decimal("15"),
447
- context_length=131072,
448
- output_length=4096,
449
- intelligence=5,
450
- speed=3,
451
- supports_image_input=False,
452
- supports_skill_calls=True,
453
- supports_structured_output=True,
454
- supports_search=True,
455
- timeout=180,
456
- ),
457
- "grok-3-mini": LLMModelInfo(
458
- id="grok-3-mini",
459
- name="Grok 3 Mini",
460
- provider=LLMProvider.XAI,
461
- input_price=Decimal("0.3"),
462
- output_price=Decimal("0.5"),
463
- context_length=131072,
464
- output_length=4096,
465
- intelligence=5,
466
- speed=3,
467
- supports_image_input=False,
468
- supports_skill_calls=True,
469
- supports_structured_output=True,
470
- has_reasoning=True, # Has strong reasoning capabilities
471
- supports_frequency_penalty=False,
472
- supports_presence_penalty=False, # Grok-3-mini doesn't support presence_penalty
473
- timeout=180,
474
- ),
475
- # Eternal AI models
476
- "eternalai": LLMModelInfo(
477
- id="eternalai",
478
- name="Eternal AI (Llama-3.3-70B)",
479
- provider=LLMProvider.ETERNAL,
480
- input_price=Decimal("0.25"),
481
- output_price=Decimal("0.75"),
482
- context_length=60000,
483
- output_length=4096,
484
- intelligence=4,
485
- speed=3,
486
- supports_image_input=False,
487
- supports_skill_calls=True,
488
- supports_structured_output=True,
489
- api_base="https://api.eternalai.org/v1",
490
- timeout=300,
491
- ),
492
- # Reigent models
493
- "reigent": LLMModelInfo(
494
- id="reigent",
495
- name="REI Network",
496
- provider=LLMProvider.REIGENT,
497
- input_price=Decimal("0.50"), # Placeholder price, update with actual pricing
498
- output_price=Decimal("1.50"), # Placeholder price, update with actual pricing
499
- context_length=32000,
500
- output_length=4096,
501
- intelligence=4,
502
- speed=3,
503
- supports_image_input=False,
504
- supports_skill_calls=True,
505
- supports_structured_output=True,
506
- supports_temperature=False,
507
- supports_frequency_penalty=False,
508
- supports_presence_penalty=False,
509
- api_base="https://api.reisearch.box/v1",
510
- timeout=300,
511
- ),
512
- # Venice models
513
- "venice-uncensored": LLMModelInfo(
514
- id="venice-uncensored",
515
- name="Venice Uncensored",
516
- provider=LLMProvider.VENICE,
517
- input_price=Decimal("0.50"), # Placeholder price, update with actual pricing
518
- output_price=Decimal("2.00"), # Placeholder price, update with actual pricing
519
- context_length=32000,
520
- output_length=4096,
521
- intelligence=3,
522
- speed=3,
523
- supports_image_input=False,
524
- supports_skill_calls=True,
525
- supports_structured_output=True,
526
- supports_temperature=True,
527
- supports_frequency_penalty=False,
528
- supports_presence_penalty=False,
529
- api_base="https://api.venice.ai/api/v1",
530
- timeout=300,
531
- ),
532
- "venice-llama-4-maverick-17b": LLMModelInfo(
533
- id="venice-llama-4-maverick-17b",
534
- name="Venice Llama-4 Maverick 17B",
535
- provider=LLMProvider.VENICE,
536
- input_price=Decimal("1.50"),
537
- output_price=Decimal("6.00"),
538
- context_length=32000,
539
- output_length=4096,
540
- intelligence=3,
541
- speed=3,
542
- supports_image_input=False,
543
- supports_skill_calls=True,
544
- supports_structured_output=True,
545
- supports_temperature=True,
546
- supports_frequency_penalty=False,
547
- supports_presence_penalty=False,
548
- api_base="https://api.venice.ai/api/v1",
549
- timeout=300,
550
- ),
551
- }
324
+ # Default models loaded from CSV
325
+ AVAILABLE_MODELS = _load_default_llm_models()
552
326
 
553
327
 
554
328
  class LLMModel(BaseModel):
@@ -563,7 +337,7 @@ class LLMModel(BaseModel):
563
337
  async def model_info(self) -> LLMModelInfo:
564
338
  """Get the model information with caching.
565
339
 
566
- First tries to get from cache, then database, then AVAILABLE_MODELS.
340
+ First tries to get from cache, then database, then default models loaded from CSV.
567
341
  Raises ValueError if model is not found anywhere.
568
342
  """
569
343
  model_info = await LLMModelInfo.get(self.model_name)
intentkit/models/skill.py CHANGED
@@ -1,6 +1,9 @@
1
+ import csv
1
2
  import json
3
+ import logging
2
4
  from datetime import datetime, timezone
3
5
  from decimal import Decimal
6
+ from pathlib import Path
4
7
  from typing import Annotated, Any, Dict, Optional
5
8
 
6
9
  from intentkit.models.base import Base
@@ -19,6 +22,9 @@ from sqlalchemy import (
19
22
  select,
20
23
  )
21
24
  from sqlalchemy.dialects.postgresql import JSON, JSONB
25
+ from sqlalchemy.ext.asyncio import AsyncSession
26
+
27
+ logger = logging.getLogger(__name__)
22
28
 
23
29
 
24
30
  class AgentSkillDataTable(Base):
@@ -349,6 +355,76 @@ class ThreadSkillData(ThreadSkillDataCreate):
349
355
  await db.commit()
350
356
 
351
357
 
358
+ def _skill_parse_bool(value: Optional[str]) -> bool:
359
+ if value is None:
360
+ return False
361
+ return value.strip().lower() in {"true", "1", "yes"}
362
+
363
+
364
+ def _skill_parse_optional_int(value: Optional[str]) -> Optional[int]:
365
+ if value is None:
366
+ return None
367
+ value = value.strip()
368
+ return int(value) if value else None
369
+
370
+
371
+ def _skill_parse_decimal(value: Optional[str], default: str = "0") -> Decimal:
372
+ value = (value or "").strip()
373
+ if not value:
374
+ value = default
375
+ return Decimal(value)
376
+
377
+
378
+ def _load_default_skills() -> tuple[dict[str, "Skill"], dict[tuple[str, str], "Skill"]]:
379
+ """Load default skills from CSV into lookup maps."""
380
+
381
+ path = Path(__file__).with_name("skills.csv")
382
+ if not path.exists():
383
+ logger.warning("Default skills CSV not found at %s", path)
384
+ return {}, {}
385
+
386
+ by_name: dict[str, "Skill"] = {}
387
+ by_category_config: dict[tuple[str, str], "Skill"] = {}
388
+
389
+ with path.open(newline="", encoding="utf-8") as csvfile:
390
+ reader = csv.DictReader(csvfile)
391
+ for row in reader:
392
+ try:
393
+ timestamp = datetime.now(timezone.utc)
394
+ price_default = row.get("price") or "1"
395
+ skill = Skill(
396
+ name=row["name"],
397
+ category=row["category"],
398
+ config_name=row.get("config_name") or None,
399
+ enabled=_skill_parse_bool(row.get("enabled")),
400
+ price_level=_skill_parse_optional_int(row.get("price_level")),
401
+ price=_skill_parse_decimal(row.get("price"), default="1"),
402
+ price_self_key=_skill_parse_decimal(
403
+ row.get("price_self_key"), default=price_default
404
+ ),
405
+ rate_limit_count=_skill_parse_optional_int(
406
+ row.get("rate_limit_count")
407
+ ),
408
+ rate_limit_minutes=_skill_parse_optional_int(
409
+ row.get("rate_limit_minutes")
410
+ ),
411
+ author=row.get("author") or None,
412
+ created_at=timestamp,
413
+ updated_at=timestamp,
414
+ )
415
+ except Exception as exc:
416
+ logger.error(
417
+ "Failed to load default skill %s: %s", row.get("name"), exc
418
+ )
419
+ continue
420
+
421
+ by_name[skill.name] = skill
422
+ if skill.config_name:
423
+ by_category_config[(skill.category, skill.config_name)] = skill
424
+
425
+ return by_name, by_category_config
426
+
427
+
352
428
  class SkillTable(Base):
353
429
  """Database table model for Skill."""
354
430
 
@@ -447,18 +523,21 @@ class Skill(BaseModel):
447
523
  stmt = select(SkillTable).where(SkillTable.name == name)
448
524
  skill = await session.scalar(stmt)
449
525
 
450
- # If skill doesn't exist, return None
451
- if not skill:
452
- return None
526
+ # If skill exists in database, convert and cache it
527
+ if skill:
528
+ skill_model = Skill.model_validate(skill)
529
+ await redis.set(cache_key, skill_model.model_dump_json(), ex=cache_ttl)
530
+ return skill_model
453
531
 
454
- # Convert to Skill model
455
- skill_model = Skill.model_validate(skill)
456
-
457
- # Cache the skill in Redis
532
+ # Fallback to default skills loaded from CSV
533
+ default_skill = DEFAULT_SKILLS_BY_NAME.get(name)
534
+ if default_skill:
535
+ skill_model = default_skill.model_copy(deep=True)
458
536
  await redis.set(cache_key, skill_model.model_dump_json(), ex=cache_ttl)
459
-
460
537
  return skill_model
461
538
 
539
+ return None
540
+
462
541
  @staticmethod
463
542
  async def get_by_config_name(category: str, config_name: str) -> Optional["Skill"]:
464
543
  """Get a skill by category and config_name.
@@ -477,9 +556,37 @@ class Skill(BaseModel):
477
556
  )
478
557
  skill = await session.scalar(stmt)
479
558
 
480
- # If skill doesn't exist, return None
481
- if not skill:
482
- return None
559
+ # If skill exists in database, return it
560
+ if skill:
561
+ return Skill.model_validate(skill)
562
+
563
+ # Fallback to default skills loaded from CSV
564
+ default_skill = DEFAULT_SKILLS_BY_CATEGORY_CONFIG.get((category, config_name))
565
+ if default_skill:
566
+ return default_skill.model_copy(deep=True)
567
+
568
+ return None
569
+
570
+ @classmethod
571
+ async def get_all(cls, session: AsyncSession | None = None) -> list["Skill"]:
572
+ """Return all skills merged from defaults and database overrides."""
573
+
574
+ if session is None:
575
+ async with get_session() as db:
576
+ return await cls.get_all(session=db)
577
+
578
+ skills: dict[str, "Skill"] = {
579
+ name: skill.model_copy(deep=True)
580
+ for name, skill in DEFAULT_SKILLS_BY_NAME.items()
581
+ }
582
+
583
+ result = await session.execute(select(SkillTable))
584
+ for row in result.scalars():
585
+ skill_model = cls.model_validate(row)
586
+ skills[skill_model.name] = skill_model
587
+
588
+ return list(skills.values())
589
+
483
590
 
484
- # Convert to Skill model
485
- return Skill.model_validate(skill)
591
+ # Default skills loaded from CSV
592
+ DEFAULT_SKILLS_BY_NAME, DEFAULT_SKILLS_BY_CATEGORY_CONFIG = _load_default_skills()