intentkit 0.7.4rc1__py3-none-any.whl → 0.7.5.dev1__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/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  A powerful platform for building AI agents with blockchain and cryptocurrency capabilities.
4
4
  """
5
5
 
6
- __version__ = "0.7.4-rc.1"
6
+ __version__ = "0.7.5-dev1"
7
7
  __author__ = "hyacinthus"
8
8
  __email__ = "hyacinthus@gmail.com"
9
9
 
intentkit/clients/cdp.py CHANGED
@@ -87,7 +87,7 @@ class CdpClient:
87
87
  return self._wallet_provider
88
88
  agent: Agent = await self._skill_store.get_agent_config(self._agent_id)
89
89
  agent_data: AgentData = await self._skill_store.get_agent_data(self._agent_id)
90
- network_id = agent.network_id or agent.cdp_network_id
90
+ network_id = agent.network_id
91
91
 
92
92
  # Get credentials from skill store system config
93
93
  api_key_id = self._skill_store.get_system_config("cdp_api_key_id")
intentkit/core/agent.py CHANGED
@@ -2,19 +2,305 @@ import logging
2
2
  import time
3
3
  from datetime import datetime, timedelta, timezone
4
4
  from decimal import Decimal
5
- from typing import Dict, List
5
+ from typing import Any, Dict, List, Optional
6
6
 
7
+ from aiogram import Bot
8
+ from aiogram.exceptions import TelegramConflictError, TelegramUnauthorizedError
9
+ from aiogram.utils.token import TokenValidationError
7
10
  from sqlalchemy import func, select, text, update
8
11
 
9
- from intentkit.models.agent import Agent, AgentAutonomous, AgentTable
10
- from intentkit.models.agent_data import AgentQuotaTable
12
+ from intentkit.abstracts.skill import SkillStoreABC
13
+ from intentkit.clients.cdp import get_cdp_client
14
+ from intentkit.config.config import config
15
+ from intentkit.models.agent import (
16
+ Agent,
17
+ AgentAutonomous,
18
+ AgentCreate,
19
+ AgentResponse,
20
+ AgentTable,
21
+ AgentUpdate,
22
+ )
23
+ from intentkit.models.agent_data import AgentData, AgentQuota, AgentQuotaTable
11
24
  from intentkit.models.credit import CreditEventTable, EventType, UpstreamType
12
25
  from intentkit.models.db import get_session
26
+ from intentkit.models.skill import (
27
+ AgentSkillData,
28
+ AgentSkillDataCreate,
29
+ ThreadSkillData,
30
+ ThreadSkillDataCreate,
31
+ )
13
32
  from intentkit.utils.error import IntentKitAPIError
33
+ from intentkit.utils.slack_alert import send_slack_message
14
34
 
15
35
  logger = logging.getLogger(__name__)
16
36
 
17
37
 
38
+ async def _process_agent_post_actions(
39
+ agent: Agent, is_new: bool = True, slack_message: str | None = None
40
+ ) -> AgentData:
41
+ """Process common actions after agent creation or update.
42
+
43
+ Args:
44
+ agent: The agent that was created or updated
45
+ is_new: Whether the agent is newly created
46
+ slack_message: Optional custom message for Slack notification
47
+
48
+ Returns:
49
+ AgentData: The processed agent data
50
+ """
51
+ if config.cdp_api_key_id and agent.wallet_provider == "cdp":
52
+ cdp_client = await get_cdp_client(agent.id, agent_store)
53
+ await cdp_client.get_wallet_provider()
54
+
55
+ # Get new agent data
56
+ # FIXME: refuse to change wallet provider
57
+ if agent.wallet_provider == "readonly":
58
+ agent_data = await AgentData.patch(
59
+ agent.id,
60
+ {
61
+ "evm_wallet_address": agent.readonly_wallet_address,
62
+ },
63
+ )
64
+ else:
65
+ agent_data = await AgentData.get(agent.id)
66
+
67
+ # Send Slack notification
68
+ slack_message = slack_message or ("Agent Created" if is_new else "Agent Updated")
69
+ try:
70
+ _send_agent_notification(agent, agent_data, slack_message)
71
+ except Exception as e:
72
+ logger.error("Failed to send Slack notification: %s", e)
73
+
74
+ return agent_data
75
+
76
+
77
+ async def _process_telegram_config(
78
+ agent: AgentUpdate, existing_agent: Optional[Agent], agent_data: AgentData
79
+ ) -> AgentData:
80
+ """Process telegram configuration for an agent.
81
+
82
+ Args:
83
+ agent: The agent with telegram configuration
84
+ existing_agent: The existing agent (if updating)
85
+ agent_data: The agent data to update
86
+
87
+ Returns:
88
+ AgentData: The updated agent data
89
+ """
90
+ changes = agent.model_dump(exclude_unset=True)
91
+ if not changes.get("telegram_entrypoint_enabled"):
92
+ return agent_data
93
+
94
+ if not changes.get("telegram_config") or not changes.get("telegram_config").get(
95
+ "token"
96
+ ):
97
+ return agent_data
98
+
99
+ tg_bot_token = changes.get("telegram_config").get("token")
100
+
101
+ if existing_agent and existing_agent.telegram_config.get("token") == tg_bot_token:
102
+ return agent_data
103
+
104
+ try:
105
+ bot = Bot(token=tg_bot_token)
106
+ bot_info = await bot.get_me()
107
+ agent_data.telegram_id = str(bot_info.id)
108
+ agent_data.telegram_username = bot_info.username
109
+ agent_data.telegram_name = bot_info.first_name
110
+ if bot_info.last_name:
111
+ agent_data.telegram_name = f"{bot_info.first_name} {bot_info.last_name}"
112
+ await agent_data.save()
113
+ try:
114
+ await bot.close()
115
+ except Exception:
116
+ pass
117
+ return agent_data
118
+ except (
119
+ TelegramUnauthorizedError,
120
+ TelegramConflictError,
121
+ TokenValidationError,
122
+ ) as req_err:
123
+ logger.error(
124
+ f"Unauthorized err getting telegram bot username with token {tg_bot_token}: {req_err}",
125
+ )
126
+ return agent_data
127
+ except Exception as e:
128
+ logger.error(
129
+ f"Error getting telegram bot username with token {tg_bot_token}: {e}",
130
+ )
131
+ return agent_data
132
+
133
+
134
+ def _send_agent_notification(agent: Agent, agent_data: AgentData, message: str) -> None:
135
+ """Send a notification about agent creation or update.
136
+
137
+ Args:
138
+ agent: The agent that was created or updated
139
+ agent_data: The agent data to update
140
+ message: The notification message
141
+ """
142
+ # Format autonomous configurations - show only enabled ones with their id, name, and schedule
143
+ autonomous_formatted = ""
144
+ if agent.autonomous:
145
+ enabled_autonomous = [auto for auto in agent.autonomous if auto.enabled]
146
+ if enabled_autonomous:
147
+ autonomous_items = []
148
+ for auto in enabled_autonomous:
149
+ schedule = (
150
+ f"cron: {auto.cron}" if auto.cron else f"minutes: {auto.minutes}"
151
+ )
152
+ autonomous_items.append(
153
+ f"• {auto.id}: {auto.name or 'Unnamed'} ({schedule})"
154
+ )
155
+ autonomous_formatted = "\n".join(autonomous_items)
156
+ else:
157
+ autonomous_formatted = "No enabled autonomous configurations"
158
+ else:
159
+ autonomous_formatted = "None"
160
+
161
+ # Format skills - find categories with enabled: true and list skills in public/private states
162
+ skills_formatted = ""
163
+ if agent.skills:
164
+ enabled_categories = []
165
+ for category, skill_config in agent.skills.items():
166
+ if skill_config and skill_config.get("enabled") is True:
167
+ skills_list = []
168
+ states = skill_config.get("states", {})
169
+ public_skills = [
170
+ skill for skill, state in states.items() if state == "public"
171
+ ]
172
+ private_skills = [
173
+ skill for skill, state in states.items() if state == "private"
174
+ ]
175
+
176
+ if public_skills:
177
+ skills_list.append(f" Public: {', '.join(public_skills)}")
178
+ if private_skills:
179
+ skills_list.append(f" Private: {', '.join(private_skills)}")
180
+
181
+ if skills_list:
182
+ enabled_categories.append(
183
+ f"• {category}:\n{chr(10).join(skills_list)}"
184
+ )
185
+
186
+ if enabled_categories:
187
+ skills_formatted = "\n".join(enabled_categories)
188
+ else:
189
+ skills_formatted = "No enabled skills"
190
+ else:
191
+ skills_formatted = "None"
192
+
193
+ send_slack_message(
194
+ message,
195
+ attachments=[
196
+ {
197
+ "color": "good",
198
+ "fields": [
199
+ {"title": "ID", "short": True, "value": agent.id},
200
+ {"title": "Name", "short": True, "value": agent.name},
201
+ {"title": "Model", "short": True, "value": agent.model},
202
+ {
203
+ "title": "Network",
204
+ "short": True,
205
+ "value": agent.network_id or "Default",
206
+ },
207
+ {
208
+ "title": "X Username",
209
+ "short": True,
210
+ "value": agent_data.twitter_username,
211
+ },
212
+ {
213
+ "title": "Telegram Enabled",
214
+ "short": True,
215
+ "value": str(agent.telegram_entrypoint_enabled),
216
+ },
217
+ {
218
+ "title": "Telegram Username",
219
+ "short": True,
220
+ "value": agent_data.telegram_username,
221
+ },
222
+ {
223
+ "title": "Wallet Address",
224
+ "value": agent_data.evm_wallet_address,
225
+ },
226
+ {
227
+ "title": "Autonomous",
228
+ "value": autonomous_formatted,
229
+ },
230
+ {
231
+ "title": "Skills",
232
+ "value": skills_formatted,
233
+ },
234
+ ],
235
+ }
236
+ ],
237
+ )
238
+
239
+
240
+ async def deploy_agent(
241
+ agent_id: str, agent: AgentUpdate, owner: Optional[str] = None
242
+ ) -> AgentResponse:
243
+ """Override an existing agent.
244
+
245
+ Use input to override agent configuration. If some fields are not provided, they will be reset to default values.
246
+
247
+ Args:
248
+ agent_id: ID of the agent to update
249
+ agent: Agent update configuration
250
+ owner: Optional owner for the agent
251
+
252
+ Returns:
253
+ AgentResponse: Updated agent configuration with additional processed data
254
+
255
+ Raises:
256
+ HTTPException:
257
+ - 400: Invalid agent ID format
258
+ - 404: Agent not found
259
+ - 403: Permission denied (if owner mismatch)
260
+ - 500: Database error
261
+ """
262
+ existing_agent = await Agent.get(agent_id)
263
+ if not existing_agent:
264
+ agent = AgentCreate.model_validate(input)
265
+ agent.id = agent_id
266
+ if owner:
267
+ agent.owner = owner
268
+ else:
269
+ agent.owner = "system"
270
+ # Check for existing agent by upstream_id, forward compatibility, raise error after 3.0
271
+ existing = await agent.get_by_upstream_id()
272
+ if existing:
273
+ agent_data = await AgentData.get(existing.id)
274
+ agent_response = await AgentResponse.from_agent(existing, agent_data)
275
+ return agent_response
276
+
277
+ # Create new agent
278
+ latest_agent = await agent.create()
279
+ # Process common post-creation actions
280
+ agent_data = await _process_agent_post_actions(
281
+ latest_agent, True, "Agent Created"
282
+ )
283
+ agent_data = await _process_telegram_config(input, None, agent_data)
284
+ agent_response = await AgentResponse.from_agent(latest_agent, agent_data)
285
+
286
+ return agent_response
287
+
288
+ if owner and owner != existing_agent.owner:
289
+ raise IntentKitAPIError(403, "forbidden", "forbidden")
290
+
291
+ # Update agent
292
+ latest_agent = await agent.override(agent_id)
293
+
294
+ # Process common post-update actions
295
+ agent_data = await _process_agent_post_actions(
296
+ latest_agent, False, "Agent Overridden"
297
+ )
298
+ agent_data = await _process_telegram_config(agent, existing_agent, agent_data)
299
+ agent_response = await AgentResponse.from_agent(latest_agent, agent_data)
300
+
301
+ return agent_response
302
+
303
+
18
304
  async def agent_action_cost(agent_id: str) -> Dict[str, Decimal]:
19
305
  """
20
306
  Calculate various action cost metrics for an agent based on past three days of credit events.
@@ -192,6 +478,182 @@ async def agent_action_cost(agent_id: str) -> Dict[str, Decimal]:
192
478
  return result
193
479
 
194
480
 
481
+ class AgentStore(SkillStoreABC):
482
+ """Implementation of skill data storage operations.
483
+
484
+ This class provides concrete implementations for storing and retrieving
485
+ skill-related data for both agents and threads.
486
+ """
487
+
488
+ @staticmethod
489
+ def get_system_config(key: str) -> Any:
490
+ # TODO: maybe need a whitelist here
491
+ if hasattr(config, key):
492
+ return getattr(config, key)
493
+ return None
494
+
495
+ @staticmethod
496
+ async def get_agent_config(agent_id: str) -> Optional[Agent]:
497
+ return await Agent.get(agent_id)
498
+
499
+ @staticmethod
500
+ async def get_agent_data(agent_id: str) -> AgentData:
501
+ return await AgentData.get(agent_id)
502
+
503
+ @staticmethod
504
+ async def set_agent_data(agent_id: str, data: Dict) -> AgentData:
505
+ return await AgentData.patch(agent_id, data)
506
+
507
+ @staticmethod
508
+ async def get_agent_quota(agent_id: str) -> AgentQuota:
509
+ return await AgentQuota.get(agent_id)
510
+
511
+ @staticmethod
512
+ async def get_agent_skill_data(
513
+ agent_id: str, skill: str, key: str
514
+ ) -> Optional[Dict[str, Any]]:
515
+ """Get skill data for an agent.
516
+
517
+ Args:
518
+ agent_id: ID of the agent
519
+ skill: Name of the skill
520
+ key: Data key
521
+
522
+ Returns:
523
+ Dictionary containing the skill data if found, None otherwise
524
+ """
525
+ return await AgentSkillData.get(agent_id, skill, key)
526
+
527
+ @staticmethod
528
+ async def save_agent_skill_data(
529
+ agent_id: str, skill: str, key: str, data: Dict[str, Any]
530
+ ) -> None:
531
+ """Save or update skill data for an agent.
532
+
533
+ Args:
534
+ agent_id: ID of the agent
535
+ skill: Name of the skill
536
+ key: Data key
537
+ data: JSON data to store
538
+ """
539
+ skill_data = AgentSkillDataCreate(
540
+ agent_id=agent_id,
541
+ skill=skill,
542
+ key=key,
543
+ data=data,
544
+ )
545
+ await skill_data.save()
546
+
547
+ @staticmethod
548
+ async def delete_agent_skill_data(agent_id: str, skill: str, key: str) -> None:
549
+ """Delete skill data for an agent.
550
+
551
+ Args:
552
+ agent_id: ID of the agent
553
+ skill: Name of the skill
554
+ key: Data key
555
+ """
556
+ await AgentSkillData.delete(agent_id, skill, key)
557
+
558
+ @staticmethod
559
+ async def get_thread_skill_data(
560
+ thread_id: str, skill: str, key: str
561
+ ) -> Optional[Dict[str, Any]]:
562
+ """Get skill data for a thread.
563
+
564
+ Args:
565
+ thread_id: ID of the thread
566
+ skill: Name of the skill
567
+ key: Data key
568
+
569
+ Returns:
570
+ Dictionary containing the skill data if found, None otherwise
571
+ """
572
+ return await ThreadSkillData.get(thread_id, skill, key)
573
+
574
+ @staticmethod
575
+ async def save_thread_skill_data(
576
+ thread_id: str,
577
+ agent_id: str,
578
+ skill: str,
579
+ key: str,
580
+ data: Dict[str, Any],
581
+ ) -> None:
582
+ """Save or update skill data for a thread.
583
+
584
+ Args:
585
+ thread_id: ID of the thread
586
+ agent_id: ID of the agent that owns this thread
587
+ skill: Name of the skill
588
+ key: Data key
589
+ data: JSON data to store
590
+ """
591
+ skill_data = ThreadSkillDataCreate(
592
+ thread_id=thread_id,
593
+ agent_id=agent_id,
594
+ skill=skill,
595
+ key=key,
596
+ data=data,
597
+ )
598
+ await skill_data.save()
599
+
600
+ @staticmethod
601
+ async def list_autonomous_tasks(agent_id: str) -> List[AgentAutonomous]:
602
+ """List all autonomous tasks for an agent.
603
+
604
+ Args:
605
+ agent_id: ID of the agent
606
+
607
+ Returns:
608
+ List[AgentAutonomous]: List of autonomous task configurations
609
+ """
610
+ return await list_autonomous_tasks(agent_id)
611
+
612
+ @staticmethod
613
+ async def add_autonomous_task(
614
+ agent_id: str, task: AgentAutonomous
615
+ ) -> AgentAutonomous:
616
+ """Add a new autonomous task to an agent.
617
+
618
+ Args:
619
+ agent_id: ID of the agent
620
+ task: Autonomous task configuration
621
+
622
+ Returns:
623
+ AgentAutonomous: The created task
624
+ """
625
+ return await add_autonomous_task(agent_id, task)
626
+
627
+ @staticmethod
628
+ async def delete_autonomous_task(agent_id: str, task_id: str) -> None:
629
+ """Delete an autonomous task from an agent.
630
+
631
+ Args:
632
+ agent_id: ID of the agent
633
+ task_id: ID of the task to delete
634
+ """
635
+ await delete_autonomous_task(agent_id, task_id)
636
+
637
+ @staticmethod
638
+ async def update_autonomous_task(
639
+ agent_id: str, task_id: str, task_updates: dict
640
+ ) -> AgentAutonomous:
641
+ """Update an autonomous task for an agent.
642
+
643
+ Args:
644
+ agent_id: ID of the agent
645
+ task_id: ID of the task to update
646
+ task_updates: Dictionary containing fields to update
647
+
648
+ Returns:
649
+ AgentAutonomous: The updated task
650
+ """
651
+ return await update_autonomous_task(agent_id, task_id, task_updates)
652
+
653
+
654
+ agent_store = AgentStore()
655
+
656
+
195
657
  async def update_agent_action_cost():
196
658
  """
197
659
  Update action costs for all agents.
intentkit/core/engine.py CHANGED
@@ -35,6 +35,7 @@ from sqlalchemy.exc import SQLAlchemyError
35
35
 
36
36
  from intentkit.abstracts.graph import AgentContext, AgentError, AgentState
37
37
  from intentkit.config.config import config
38
+ from intentkit.core.agent import agent_store
38
39
  from intentkit.core.chat import clear_thread_memory
39
40
  from intentkit.core.credit import expense_message, expense_skill
40
41
  from intentkit.core.node import PreModelNode, post_model_node
@@ -42,7 +43,6 @@ from intentkit.core.prompt import (
42
43
  create_formatted_prompt_function,
43
44
  explain_prompt,
44
45
  )
45
- from intentkit.core.skill import skill_store
46
46
  from intentkit.models.agent import Agent, AgentTable
47
47
  from intentkit.models.agent_data import AgentData, AgentQuota
48
48
  from intentkit.models.app_setting import AppSetting, SystemMessageType
@@ -54,7 +54,7 @@ from intentkit.models.chat import (
54
54
  )
55
55
  from intentkit.models.credit import CreditAccount, OwnerType
56
56
  from intentkit.models.db import get_langgraph_checkpointer, get_session
57
- from intentkit.models.llm import LLMModelInfo, LLMProvider
57
+ from intentkit.models.llm import LLMModelInfo, LLMProvider, create_llm_model
58
58
  from intentkit.models.skill import AgentSkillData, ThreadSkillData
59
59
  from intentkit.models.user import User
60
60
  from intentkit.utils.error import IntentKitAPIError
@@ -71,9 +71,7 @@ _agents_updated: dict[str, datetime] = {}
71
71
  _private_agents_updated: dict[str, datetime] = {}
72
72
 
73
73
 
74
- async def create_agent(
75
- agent: Agent, is_private: bool = False, has_search: bool = False
76
- ) -> CompiledStateGraph:
74
+ async def create_agent(agent: Agent, is_private: bool = False) -> CompiledStateGraph:
77
75
  """Create an AI agent with specified configuration and tools.
78
76
 
79
77
  This function:
@@ -92,9 +90,6 @@ async def create_agent(
92
90
  """
93
91
  agent_data = await AgentData.get(agent.id)
94
92
 
95
- # ==== Initialize LLM using the LLM abstraction.
96
- from intentkit.models.llm import create_llm_model
97
-
98
93
  # Create the LLM model instance
99
94
  llm_model = await create_llm_model(
100
95
  model_name=agent.model,
@@ -123,7 +118,7 @@ async def create_agent(
123
118
  skill_module = importlib.import_module(f"intentkit.skills.{k}")
124
119
  if hasattr(skill_module, "get_skills"):
125
120
  skill_tools = await skill_module.get_skills(
126
- v, is_private, skill_store, agent_id=agent.id
121
+ v, is_private, agent_store, agent_id=agent.id
127
122
  )
128
123
  if skill_tools and len(skill_tools) > 0:
129
124
  tools.extend(skill_tools)
@@ -137,8 +132,7 @@ async def create_agent(
137
132
 
138
133
  # Add search tools if requested
139
134
  if (
140
- has_search
141
- and llm_model.info.provider == LLMProvider.OPENAI
135
+ llm_model.info.provider == LLMProvider.OPENAI
142
136
  and llm_model.info.supports_search
143
137
  and not agent.model.startswith(
144
138
  "gpt-5"
@@ -159,7 +153,7 @@ async def create_agent(
159
153
  model=llm,
160
154
  short_term_memory_strategy=agent.short_term_memory_strategy,
161
155
  max_tokens=input_token_limit // 2,
162
- max_summary_tokens=2048, # later we can let agent to set this
156
+ max_summary_tokens=2048,
163
157
  )
164
158
 
165
159
  # Create ReAct Agent using the LLM and CDP Agentkit tools.
@@ -202,21 +196,8 @@ async def initialize_agent(aid, is_private=False):
202
196
  if not agent:
203
197
  raise HTTPException(status_code=404, detail="Agent not found")
204
198
 
205
- # Determine if search should be enabled based on model capabilities
206
- from intentkit.models.llm import create_llm_model
207
-
208
- llm_model = await create_llm_model(
209
- model_name=agent.model,
210
- temperature=agent.temperature,
211
- frequency_penalty=agent.frequency_penalty,
212
- presence_penalty=agent.presence_penalty,
213
- )
214
- has_search = (
215
- llm_model.info.provider == LLMProvider.OPENAI and llm_model.info.supports_search
216
- )
217
-
218
199
  # Create the agent using the new create_agent function
219
- executor = await create_agent(agent, is_private, has_search)
200
+ executor = await create_agent(agent, is_private)
220
201
 
221
202
  # Cache the agent executor
222
203
  if is_private:
intentkit/core/prompt.py CHANGED
@@ -106,7 +106,7 @@ def _build_wallet_section(agent: Agent, agent_data: AgentData) -> str:
106
106
  return ""
107
107
 
108
108
  wallet_parts = []
109
- network_id = agent.network_id or agent.cdp_network_id
109
+ network_id = agent.network_id
110
110
 
111
111
  if agent_data.evm_wallet_address and network_id != "solana":
112
112
  wallet_parts.append(