diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c09c07f..2f53e18d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## v0.8.33 - 2025-11-14 + +### Bug Fixes +- Fixed lifi bug in token execution +- Updated code formatting with ruff + +### Documentation +- Updated changelog + +**Full Changelog**: https://github.com/crestalnetwork/intentkit/compare/v0.8.32...v0.8.33 + ## v0.8.32 - 2025-11-14 ### Fixes diff --git a/LLM.md b/LLM.md index 2f3ac28d..77657045 100644 --- a/LLM.md +++ b/LLM.md @@ -35,6 +35,7 @@ IntentKit is an autonomous agent framework that enables creation and management - Package manager: uv - Virtual environment: .venv, please use `source .venv/bin/activate` at least once to active virtual environment before running any command. - Lint: ruff, run `ruff format & ruff check --fix` after your final edit. +- Language Server: BasedPyright, please make sure the changed files have no `basedpyright` errors. - API framework: fastapi, Doc in https://fastapi.tiangolo.com/ - DB ORM: SQLAlchemy 2.0, please check the 2.0 api for use, do not use the legacy way. Doc in https://docs.sqlalchemy.org/en/20/ - Model: Pydantic V2, Also be careful not to use the obsolete V1 interface. Doc in https://docs.pydantic.dev/latest/ diff --git a/intentkit/core/draft.py b/intentkit/core/draft.py new file mode 100644 index 00000000..5dab1b86 --- /dev/null +++ b/intentkit/core/draft.py @@ -0,0 +1,201 @@ +"""Service functions for agent draft operations.""" + +from __future__ import annotations + +from epyxid import XID +from fastapi import status +from sqlalchemy import desc, select +from sqlalchemy.ext.asyncio import AsyncSession + +from intentkit.models.agent import Agent, AgentUserInput +from intentkit.models.draft import AgentDraft, AgentDraftTable +from intentkit.utils.error import IntentKitAPIError + + +async def update_agent_draft( + *, + agent_id: str, + user_id: str, + input: AgentUserInput, + db: AsyncSession, +) -> AgentDraft: + """Update the latest draft for the specified agent with partial field updates. + + This function only updates fields that are explicitly provided in the input, + leaving other fields unchanged. This is more efficient than override as it + reduces context usage and minimizes the risk of accidentally changing fields. + """ + query = ( + select(AgentDraftTable) + .where(AgentDraftTable.agent_id == agent_id, AgentDraftTable.owner == user_id) + .order_by(desc(AgentDraftTable.created_at)) + .limit(1) + ) + + result = await db.execute(query) + latest_draft = result.scalar_one_or_none() + + if not latest_draft: + raise IntentKitAPIError( + status.HTTP_404_NOT_FOUND, + "DraftNotFound", + "No drafts found for this agent", + ) + + # Get only the fields that are explicitly provided (exclude_unset=True) + update_data = input.model_dump(exclude_unset=True) + + if latest_draft.deployed_at is not None: + # Create new draft version if current one is deployed + draft_id = str(XID()) + + # Start with existing draft data and merge updates + draft_data = AgentUserInput.model_validate(latest_draft).model_dump() + draft_data.update(update_data) + + updated_input = AgentUserInput.model_validate(draft_data) + + draft_table = AgentDraftTable( + id=draft_id, + agent_id=agent_id, + owner=user_id, + version=updated_input.hash(), + last_draft_id=latest_draft.id, + project_id=latest_draft.project_id, + **draft_data, + ) + + db.add(draft_table) + await db.commit() + await db.refresh(draft_table) + + return AgentDraft.model_validate(draft_table) + + # Update existing draft in-place + for key, value in update_data.items(): + setattr(latest_draft, key, value) + + # Update version hash based on updated data + updated_input = AgentUserInput.model_validate(latest_draft) + latest_draft.version = updated_input.hash() + + await db.commit() + await db.refresh(latest_draft) + + return AgentDraft.model_validate(latest_draft) + + +async def override_agent_draft( + *, + agent_id: str, + user_id: str, + input: AgentUserInput, + db: AsyncSession, +) -> AgentDraft: + """Override the latest draft for the specified agent.""" + query = ( + select(AgentDraftTable) + .where(AgentDraftTable.agent_id == agent_id, AgentDraftTable.owner == user_id) + .order_by(desc(AgentDraftTable.created_at)) + .limit(1) + ) + + result = await db.execute(query) + latest_draft = result.scalar_one_or_none() + + if not latest_draft: + raise IntentKitAPIError( + status.HTTP_404_NOT_FOUND, + "DraftNotFound", + "No drafts found for this agent", + ) + + if latest_draft.deployed_at is not None: + draft_id = str(XID()) + + draft_table = AgentDraftTable( + id=draft_id, + agent_id=agent_id, + owner=user_id, + version=input.hash(), + last_draft_id=latest_draft.id, + project_id=latest_draft.project_id, + **input.model_dump(), + ) + + db.add(draft_table) + await db.commit() + await db.refresh(draft_table) + + return AgentDraft.model_validate(draft_table) + + for key, value in input.model_dump().items(): + setattr(latest_draft, key, value) + + latest_draft.version = input.hash() + + await db.commit() + await db.refresh(latest_draft) + + return AgentDraft.model_validate(latest_draft) + + +async def get_agent_latest_draft( + *, + agent_id: str, + user_id: str, + db: AsyncSession, +) -> AgentDraft: + """Return the latest draft for the specified agent.""" + query = ( + select(AgentDraftTable) + .where(AgentDraftTable.agent_id == agent_id, AgentDraftTable.owner == user_id) + .order_by(desc(AgentDraftTable.created_at)) + .limit(1) + ) + + result = await db.execute(query) + latest_draft = result.scalar_one_or_none() + + if latest_draft: + return AgentDraft.model_validate(latest_draft) + + agent = await Agent.get(agent_id) + + if not agent: + raise IntentKitAPIError( + status.HTTP_404_NOT_FOUND, + "AgentNotFound", + "No drafts found for this agent", + ) + + if agent.owner != user_id: + raise IntentKitAPIError( + status.HTTP_403_FORBIDDEN, + "Forbidden", + "Not your agent", + ) + + draft_id = str(XID()) + + agent_dict = agent.model_dump() + input_dict: dict[str, object] = {} + for key in AgentUserInput.model_fields: + if key in agent_dict: + input_dict[key] = agent_dict[key] + input = AgentUserInput.model_validate(input_dict) + + draft_table = AgentDraftTable( + id=draft_id, + agent_id=agent_id, + owner=user_id, + version=input.hash(), + deployed_at=agent.updated_at, + **input.model_dump(), + ) + + db.add(draft_table) + await db.commit() + await db.refresh(draft_table) + + return AgentDraft.model_validate(draft_table) diff --git a/intentkit/core/draft_chat.py b/intentkit/core/draft_chat.py new file mode 100644 index 00000000..ba344342 --- /dev/null +++ b/intentkit/core/draft_chat.py @@ -0,0 +1,118 @@ +"""Utilities for streaming draft agent conversations.""" + +import logging +import time +from datetime import datetime, timedelta, timezone +from typing import AsyncGenerator + +from langgraph.graph.state import CompiledStateGraph +from sqlalchemy import desc, select +from sqlalchemy.ext.asyncio import AsyncSession + +from intentkit.core.engine import build_agent, stream_agent_raw +from intentkit.models.agent import Agent +from intentkit.models.agent_data import AgentData +from intentkit.models.chat import ChatMessage, ChatMessageCreate +from intentkit.models.db import get_session +from intentkit.models.draft import AgentDraft, AgentDraftTable +from intentkit.utils.error import IntentKitAPIError + +logger = logging.getLogger(__name__) + + +_draft_executors: dict[str, CompiledStateGraph] = {} +_draft_updated_at: dict[str, datetime] = {} +_draft_cached_at: dict[str, datetime] = {} + +_CACHE_TTL = timedelta(days=1) + + +async def stream_draft( + agent_id: str, message: ChatMessageCreate +) -> AsyncGenerator[ChatMessage, None]: + """Stream chat messages for the latest draft of an agent.""" + + draft = await _get_latest_draft(agent_id) + agent = _agent_from_draft(draft) + executor, cold_start_cost = await _get_draft_executor(agent, draft) + + if not message.agent_id: + message.agent_id = agent.id + message.cold_start_cost = cold_start_cost + + async for chat_message in stream_agent_raw(message, agent, executor): + yield chat_message + + +async def _get_latest_draft(agent_id: str) -> AgentDraft: + async with get_session() as session: + result = await _execute_latest_draft_query(session, agent_id) + draft_row = result.scalar_one_or_none() + + if not draft_row: + raise IntentKitAPIError( + status_code=404, + key="DraftNotFound", + message=f"No draft found for agent {agent_id}", + ) + + return AgentDraft.model_validate(draft_row) + + +async def _execute_latest_draft_query(session: AsyncSession, agent_id: str): + statement = ( + select(AgentDraftTable) + .where(AgentDraftTable.agent_id == agent_id) + .order_by(desc(AgentDraftTable.updated_at)) + .limit(1) + ) + return await session.execute(statement) + + +def _agent_from_draft(draft: AgentDraft) -> Agent: + data = draft.model_dump() + data.pop("id", None) + data.pop("agent_id", None) + data.pop("last_draft_id", None) + data["id"] = draft.agent_id + data["owner"] = draft.owner + data["deployed_at"] = draft.deployed_at + data["created_at"] = draft.created_at + data["updated_at"] = draft.updated_at + data["version"] = draft.version + return Agent.model_validate(data) + + +async def _get_draft_executor( + agent: Agent, draft: AgentDraft +) -> tuple[CompiledStateGraph, float]: + now = datetime.now(timezone.utc) + _cleanup_cache(now) + + cached_executor = _draft_executors.get(agent.id) + cached_updated = _draft_updated_at.get(agent.id) + cold_start_cost = 0.0 + + if not cached_executor or cached_updated != draft.updated_at: + start = time.perf_counter() + agent_data = AgentData(id=agent.id) + cached_executor = await build_agent(agent, agent_data) + cold_start_cost = time.perf_counter() - start + _draft_executors[agent.id] = cached_executor + _draft_updated_at[agent.id] = draft.updated_at + _draft_cached_at[agent.id] = now + logger.info("Initialized draft executor for agent %s", agent.id) + else: + _draft_cached_at[agent.id] = now + + return cached_executor, cold_start_cost + + +def _cleanup_cache(now: datetime) -> None: + expired_before = now - _CACHE_TTL + for agent_id, cached_time in list(_draft_cached_at.items()): + if cached_time < expired_before: + _draft_cached_at.pop(agent_id, None) + _draft_updated_at.pop(agent_id, None) + _draft_executors.pop(agent_id, None) + logger.debug("Removed expired draft executor for agent %s", agent_id) diff --git a/intentkit/core/manager/__init__.py b/intentkit/core/manager/__init__.py new file mode 100644 index 00000000..b2a7f9a0 --- /dev/null +++ b/intentkit/core/manager/__init__.py @@ -0,0 +1,25 @@ +"""Manager module for agent management operations.""" + +from intentkit.core.manager.engine import stream_manager +from intentkit.core.manager.service import ( + agent_draft_json_schema, + get_latest_public_info, + get_skills_hierarchical_text, +) +from intentkit.core.manager.skills import ( + get_agent_latest_draft_skill, + get_agent_latest_public_info_skill, + update_agent_draft_skill, + update_public_info_skill, +) + +__all__ = [ + "stream_manager", + "agent_draft_json_schema", + "get_skills_hierarchical_text", + "get_latest_public_info", + "get_agent_latest_draft_skill", + "get_agent_latest_public_info_skill", + "update_agent_draft_skill", + "update_public_info_skill", +] diff --git a/intentkit/core/manager/engine.py b/intentkit/core/manager/engine.py new file mode 100644 index 00000000..66312f61 --- /dev/null +++ b/intentkit/core/manager/engine.py @@ -0,0 +1,220 @@ +"""Streaming utilities for the on-demand manager agent.""" + +from __future__ import annotations + +import logging +import time +from datetime import datetime, timedelta, timezone +from typing import AsyncGenerator + +from langgraph.graph.state import CompiledStateGraph + +from intentkit.core.engine import build_agent, stream_agent_raw +from intentkit.core.manager.skills import ( + get_agent_latest_draft_skill, + get_agent_latest_public_info_skill, + update_agent_draft_skill, + update_public_info_skill, +) +from intentkit.models.agent import Agent +from intentkit.models.agent_data import AgentData +from intentkit.models.chat import ChatMessage, ChatMessageCreate + +logger = logging.getLogger(__name__) + + +_MANAGER_CACHE_TTL = timedelta(hours=1) + +_manager_executors: dict[str, CompiledStateGraph] = {} +_manager_agents: dict[str, Agent] = {} +_manager_cached_at: dict[str, datetime] = {} + + +async def stream_manager( + agent_id: str, user_id: str, message: ChatMessageCreate +) -> AsyncGenerator[ChatMessage, None]: + """Stream chat messages for the manager agent of a specific agent.""" + + executor, manager_agent, cold_start_cost = await _get_manager_executor( + agent_id, user_id + ) + + if not message.agent_id: + message.agent_id = manager_agent.id + message.cold_start_cost = cold_start_cost + + async for chat_message in stream_agent_raw(message, manager_agent, executor): + yield chat_message + + +def _build_manager_agent(agent_id: str, user_id: str) -> Agent: + now = datetime.now(timezone.utc) + + # Get hierarchical skills text + # skills_text = get_skills_hierarchical_text() + + prompt = ( + "### Create or Update Agent Draft.\n\n" + "Use the available tools get_agent_latest_draft and update_agent_draft" + " to review the latest draft, summarise updates, and propose modifications" + " when necessary.\n" + "Always explain what changed, why it changed, and whether any drafts" + " were created or updated.\n" + "When you update a draft, ensure the saved content remains consistent" + " with the agent's purpose and principles.\n" + "When a user makes a request to create or update an agent," + " you should always use skill get_agent_latest_draft to get the latest draft," + " then make changes from it and use skill update_agent_draft for the changes," + " remember the tool input data will only update the explicitly provided fields," + " at last summarize the changes to the user.\n" + "The update_agent_draft function is efficient and safe, only updating fields you explicitly provide.\n" + "If the field deployed_at of the latest draft is empty," + " it means the draft has not been deployed yet," + " and you should refuse requests such as autonomous management and agent analysis.\n\n" + "\n\n### Avatar Generation\n\n" + "The field `picture` in the agent draft is used to store the avatar image URL." + "If the `picture` field is empty after a draft generation, you can ask user if they want to generate an avatar." + "Use the `gpt_avatar_generator` skill to generate avatar-friendly images." + "After get the avatar url from the skill result, you can update the `picture` field in the draft." + "\n\n### Model Choice\n\n" + "Use `gpt-5-mini` for normal requests, and `gpt-5` for complex requests." + "If the user specified a model, call the `get_available_llms` skill to retrieve all" + " available model IDs and find the closest match." + "\n\n### Skill Configuration\n\n" + """Because skills consume context, too much context can lead to a decline in LLM performance. + Therefore, please use skills sparingly, ideally keeping the number below 20. + If multiple skills are available for a single function, choose the one you deem most reliable. + In a category, there are often many skills. Please select only the ones that are definitely useful. + + If a skill category's `api_key_provider` has only `agent_owner` as an option, + then that skill will definitely require user input for the API key. + You can suggest in your communication that the user manually configure it later, + and avoid automatically generating drafts that use this skill. + A typical skill configuration would look like this: + ``` + "skills": {"category1": {"states": {"skill1": "public"}, "enabled": true}} + ``` + The `enabled` flag is at the category level, and `states` refers to specific skills. + For content involving sensitive material, select `private`. For content suitable for all users, select `public`. + """ + "\n\n### Public Information\n\n" + "Only agents that have already been deployed at least once can have their public" + " information updated.\n" + "The way to determine if it has been deployed at least once is to call `get_agent_latest_public_info`." + "Public info is only required when preparing to publish an agent;" + " private agents do not need it.\n" + "Always call get_agent_latest_public_info before updating" + " public info, and use update_public_info only when changes" + " are necessary.\n" + "The update_public_info function is efficient and safe, only updating fields you explicitly provide.\n\n" + # "### Available Skills for Agent Configuration\n\n" + # f"{skills_text}\n\n" + # "When using the update_agent_draft tool, select skills from the list above based on the agent's requirements. " + # "Use the exact skill names (e.g., 'erc20', 'common', 'twitter', etc.) when configuring the skills property. " + # "Each skill can be enabled/disabled and configured with specific states and API key providers according to its schema." + ) + + agent_data = { + "id": agent_id, + "owner": user_id, + "name": "Agent Manager", + "purpose": "Assist with generating, updating, and reviewing agent drafts.", + "personality": "Thorough, collaborative, and transparent about actions.", + "principles": ( + "1. Keep the agent owner informed about every change.\n" + "2. Preserve important context from prior drafts.\n" + "3. Only modify drafts using the provided update tool.\n" + "4. Speak to users in the language they ask their questions, but always use English in Agent Draft.\n" + "5. When updating a draft, try to select the right skills. Don't pick too many, just enough to meet user needs.\n" + "6. Update skill is override update, you must put the whole fields to input data, not only changed fields." + ), + "model": "grok-code-fast-1", + "prompt": prompt, + "prompt_append": None, + "short_term_memory_strategy": "trim", + "temperature": 0.2, + "frequency_penalty": 0.0, + "presence_penalty": 0.0, + "skills": { + "system": { + "enabled": True, + "states": { + "add_autonomous_task": "private", + "delete_autonomous_task": "private", + "edit_autonomous_task": "private", + "list_autonomous_tasks": "private", + "read_agent_api_key": "private", + "regenerate_agent_api_key": "private", + "get_available_llms": "private", + }, + }, + "openai": { + "enabled": True, + "api_key_provider": "platform", + "states": { + "gpt_avatar_generator": "private", + }, + }, + }, + "created_at": now, + "updated_at": now, + } + + return Agent.model_validate(agent_data) + + +async def _get_manager_executor( + agent_id: str, user_id: str +) -> tuple[CompiledStateGraph, Agent, float]: + now = datetime.now(timezone.utc) + _cleanup_cache(now) + + cache_key = _cache_key(agent_id, user_id) + executor = _manager_executors.get(cache_key) + manager_agent = _manager_agents.get(cache_key) + cold_start_cost = 0.0 + + if not executor or not manager_agent: + start = time.perf_counter() + + # Build manager agent if not cached + if not manager_agent: + manager_agent = _build_manager_agent(agent_id, user_id) + _manager_agents[cache_key] = manager_agent + + # Build executor if not cached + if not executor: + custom_skills = [ + get_agent_latest_draft_skill, + get_agent_latest_public_info_skill, + update_agent_draft_skill, + update_public_info_skill, + ] + executor = await build_agent( + manager_agent, AgentData(id=manager_agent.id), custom_skills + ) + _manager_executors[cache_key] = executor + + cold_start_cost = time.perf_counter() - start + _manager_cached_at[cache_key] = now + logger.info( + "Initialized manager executor for agent %s and user %s", agent_id, user_id + ) + else: + _manager_cached_at[cache_key] = now + + return executor, manager_agent, cold_start_cost + + +def _cache_key(agent_id: str, user_id: str) -> str: + return f"{agent_id}:{user_id}" + + +def _cleanup_cache(now: datetime) -> None: + expired_before = now - _MANAGER_CACHE_TTL + for cache_key, cached_time in list(_manager_cached_at.items()): + if cached_time < expired_before: + _manager_cached_at.pop(cache_key, None) + _manager_executors.pop(cache_key, None) + _manager_agents.pop(cache_key, None) + logger.debug("Removed expired manager executor for %s", cache_key) diff --git a/intentkit/core/manager/service.py b/intentkit/core/manager/service.py new file mode 100644 index 00000000..bb15a422 --- /dev/null +++ b/intentkit/core/manager/service.py @@ -0,0 +1,172 @@ +"""Services for agent manager utilities.""" + +from __future__ import annotations + +import json +import logging +from importlib import resources +from pathlib import Path + +import jsonref +from fastapi import status + +from intentkit.models.agent import Agent, AgentPublicInfo, AgentUserInput +from intentkit.utils.error import IntentKitAPIError + +logger = logging.getLogger(__name__) + + +def agent_draft_json_schema() -> dict[str, object]: + """Return AgentUserInput schema tailored for LLM draft generation.""" + schema: dict[str, object] = AgentUserInput.model_json_schema() + properties: dict[str, object] = schema.get("properties", {}) + + fields_to_remove = {"autonomous", "frequency_penalty", "presence_penalty"} + for field in fields_to_remove: + properties.pop(field, None) + + if "required" in schema and isinstance(schema["required"], list): + schema["required"] = [ + field for field in schema["required"] if field not in fields_to_remove + ] + + skills_property = properties.get("skills") + if not isinstance(skills_property, dict): + return schema + + skills_properties: dict[str, object] = {} + try: + skills_root = resources.files("intentkit.skills") + except (AttributeError, ModuleNotFoundError): + logger.warning("intentkit skills package not found when building schema") + return schema + + for entry in skills_root.iterdir(): + if not entry.is_dir(): + continue + + schema_path = entry / "schema.json" + if not schema_path.is_file(): + continue + + try: + skills_properties[entry.name] = _load_skill_schema(schema_path) + except ( + OSError, + ValueError, + json.JSONDecodeError, + jsonref.JsonRefError, + ) as exc: + logger.warning("Failed to load schema for skill '%s': %s", entry.name, exc) + continue + + if skills_properties: + skills_property.setdefault("type", "object") + skills_property["properties"] = skills_properties + + return schema + + +def get_skills_hierarchical_text() -> str: + """Extract skills organized by category and return as hierarchical text.""" + try: + skills_root = resources.files("intentkit.skills") + except (AttributeError, ModuleNotFoundError): + logger.warning("intentkit skills package not found when building skills text") + return "No skills available" + + # Group skills by category (x-tags) + categories: dict[str, list] = {} + + for entry in skills_root.iterdir(): + if not entry.is_dir(): + continue + + schema_path = entry / "schema.json" + if not schema_path.is_file(): + continue + + try: + skill_schema = _load_skill_schema(schema_path) + skill_name = entry.name + skill_title = skill_schema.get( + "title", skill_name.replace("_", " ").title() + ) + skill_description = skill_schema.get( + "description", "No description available" + ) + skill_tags = skill_schema.get("x-tags", ["Other"]) + + # Use the first tag as the primary category + primary_category = skill_tags[0] if skill_tags else "Other" + + if primary_category not in categories: + categories[primary_category] = [] + + categories[primary_category].append( + { + "name": skill_name, + "title": skill_title, + "description": skill_description, + } + ) + + except ( + OSError, + ValueError, + json.JSONDecodeError, + jsonref.JsonRefError, + ) as exc: + logger.warning("Failed to load schema for skill '%s': %s", entry.name, exc) + continue + + # Build hierarchical text + text_lines = [] + text_lines.append("Available Skills by Category:") + text_lines.append("") + + # Sort categories alphabetically + for category in sorted(categories.keys()): + text_lines.append(f"#### {category}") + text_lines.append("") + + # Sort skills within category alphabetically by name + for skill in sorted(categories[category], key=lambda x: x["name"]): + text_lines.append( + f"- **{skill['name']}** ({skill['title']}): {skill['description']}" + ) + + text_lines.append("") + + return "\n".join(text_lines) + + +def _load_skill_schema(schema_path: Path) -> dict[str, object]: + base_uri = f"file://{schema_path}" + with schema_path.open("r", encoding="utf-8") as schema_file: + embedded_schema: dict[str, object] = jsonref.load( + schema_file, base_uri=base_uri, proxies=False, lazy_load=False + ) + + schema_copy = dict(embedded_schema) + schema_copy.setdefault("title", schema_path.parent.name.replace("_", " ").title()) + return schema_copy + + +async def get_latest_public_info(*, agent_id: str, user_id: str) -> AgentPublicInfo: + """Return the latest public information for a specific agent.""" + + agent = await Agent.get(agent_id) + if not agent: + raise IntentKitAPIError( + status.HTTP_404_NOT_FOUND, "AgentNotFound", "Agent not found" + ) + + if agent.owner != user_id: + raise IntentKitAPIError( + status.HTTP_403_FORBIDDEN, + "AgentForbidden", + "Not authorized to access this agent", + ) + + return AgentPublicInfo.model_validate(agent) diff --git a/intentkit/core/manager/skills.py b/intentkit/core/manager/skills.py new file mode 100644 index 00000000..654375f0 --- /dev/null +++ b/intentkit/core/manager/skills.py @@ -0,0 +1,178 @@ +"""Manager skills for agent management operations.""" + +from __future__ import annotations + +import json +from typing import Annotated, Any + +from langchain_core.tools import ArgsSchema +from pydantic import BaseModel, SkipValidation + +from intentkit.core.draft import ( + get_agent_latest_draft, + update_agent_draft, +) +from intentkit.core.manager.service import ( + agent_draft_json_schema, + get_latest_public_info, +) +from intentkit.models.agent import AgentPublicInfo, AgentUserInput +from intentkit.models.db import get_session +from intentkit.skills.base import IntentKitSkill +from intentkit.utils.error import IntentKitAPIError +from intentkit.utils.schema import resolve_schema_refs + + +class NoArgsSchema(BaseModel): + """Empty schema for skills without arguments.""" + + +class GetAgentLatestDraftSkill(IntentKitSkill): + """Skill that retrieves the latest draft for the active agent.""" + + name: str = "get_agent_latest_draft" + description: str = "Fetch the latest draft for the current agent." + args_schema: Annotated[ArgsSchema | None, SkipValidation()] = NoArgsSchema + + @property + def category(self) -> str: + return "manager" + + async def _arun(self) -> str: + context = self.get_context() + if not context.user_id: + raise ValueError("User identifier missing from context") + + async with get_session() as session: + draft = await get_agent_latest_draft( + agent_id=context.agent_id, + user_id=context.user_id, + db=session, + ) + + return json.dumps(draft.model_dump(mode="json"), indent=2) + + +class GetAgentLatestPublicInfoSkill(IntentKitSkill): + """Skill that retrieves the latest public info for the active agent.""" + + name: str = "get_agent_latest_public_info" + description: str = "Fetch the latest public info for the current agent." + args_schema: Annotated[ArgsSchema | None, SkipValidation()] = NoArgsSchema + + @property + def category(self) -> str: + return "manager" + + async def _arun(self) -> str: + context = self.get_context() + if not context.user_id: + raise ValueError("User identifier missing from context") + + try: + public_info = await get_latest_public_info( + agent_id=context.agent_id, + user_id=context.user_id, + ) + except IntentKitAPIError as exc: + if exc.key == "AgentNotFound": + return ( + "Agent not found. Please inform the user that only deployed agents " + "can update public info." + ) + raise + + return json.dumps(public_info.model_dump(mode="json"), indent=2) + + +class UpdateAgentDraftSkill(IntentKitSkill): + """Skill to update agent drafts with partial field updates.""" + + name: str = "update_agent_draft" + description: str = ( + "Update the latest draft for the current agent with only the specified fields. " + "Only fields that are explicitly provided will be updated, leaving other fields unchanged. " + "This is more efficient than override and reduces the risk of accidentally changing fields." + ) + args_schema: dict[str, Any] = { + "type": "object", + "properties": { + "draft_update": agent_draft_json_schema(), + }, + "required": ["draft_update"], + "additionalProperties": False, + } + + @property + def category(self) -> str: + return "manager" + + async def _arun(self, **kwargs: Any) -> str: + context = self.get_context() + if not context.user_id: + raise ValueError("User identifier missing from context") + + if "draft_update" not in kwargs: + raise ValueError("Missing required argument 'draft_update'") + + input_model = AgentUserInput.model_validate(kwargs["draft_update"]) + + async with get_session() as session: + draft = await update_agent_draft( + agent_id=context.agent_id, + user_id=context.user_id, + input=input_model, + db=session, + ) + + return json.dumps(draft.model_dump(mode="json"), indent=2) + + +class UpdatePublicInfoSkill(IntentKitSkill): + """Skill to update the public info of an agent with partial field updates.""" + + name: str = "update_public_info" + description: str = ( + "Update the public info for a deployed agent with only the specified fields. " + "Only fields that are explicitly provided will be updated, leaving other fields unchanged. " + "This is more efficient than override and reduces the risk of accidentally changing fields. " + "Always review the latest public info before making changes." + ) + args_schema: dict[str, Any] = { + "type": "object", + "properties": { + "public_info_update": resolve_schema_refs( + AgentPublicInfo.model_json_schema() + ), + }, + "required": ["public_info_update"], + "additionalProperties": False, + } + + @property + def category(self) -> str: + return "manager" + + async def _arun(self, **kwargs: Any) -> str: + context = self.get_context() + if not context.user_id: + raise ValueError("User identifier missing from context") + + if "public_info_update" not in kwargs: + raise ValueError("Missing required argument 'public_info_update'") + + # Ensure the agent exists and belongs to the current user + await get_latest_public_info(agent_id=context.agent_id, user_id=context.user_id) + + public_info = AgentPublicInfo.model_validate(kwargs["public_info_update"]) + updated_agent = await public_info.update(context.agent_id) + updated_public_info = AgentPublicInfo.model_validate(updated_agent) + + return json.dumps(updated_public_info.model_dump(mode="json"), indent=2) + + +# Shared skill instances to avoid repeated instantiation +get_agent_latest_draft_skill = GetAgentLatestDraftSkill() +get_agent_latest_public_info_skill = GetAgentLatestPublicInfoSkill() +update_agent_draft_skill = UpdateAgentDraftSkill() +update_public_info_skill = UpdatePublicInfoSkill() diff --git a/intentkit/models/agent.py b/intentkit/models/agent.py index 2d1882c2..d95d0302 100644 --- a/intentkit/models/agent.py +++ b/intentkit/models/agent.py @@ -1058,6 +1058,46 @@ class AgentPublicInfo(BaseModel): ), ] + async def update(self, agent_id: str) -> "Agent": + """Update agent public info with only the fields that are explicitly provided. + + This method only updates fields that are explicitly set in this instance, + leaving other fields unchanged. This is more efficient than override as it + reduces context usage and minimizes the risk of accidentally changing fields. + + Args: + agent_id: The ID of the agent to update + + Returns: + The updated Agent instance + """ + async with get_session() as session: + # Get the agent from database + result = await session.execute( + select(AgentTable).where(AgentTable.id == agent_id) + ) + db_agent = result.scalar_one_or_none() + + if not db_agent: + raise IntentKitAPIError(404, "NotFound", f"Agent {agent_id} not found") + + # Get only the fields that are explicitly provided (exclude_unset=True) + update_data = self.model_dump(exclude_unset=True) + + # Apply the updates to the database agent + for key, value in update_data.items(): + if hasattr(db_agent, key): + setattr(db_agent, key, value) + + # Update public_info_updated_at timestamp + db_agent.public_info_updated_at = func.now() + + # Commit changes + await session.commit() + await session.refresh(db_agent) + + return Agent.model_validate(db_agent) + async def override(self, agent_id: str) -> "Agent": """Override agent public info with all fields from this instance. diff --git a/intentkit/models/draft.py b/intentkit/models/draft.py new file mode 100644 index 00000000..026a7ff5 --- /dev/null +++ b/intentkit/models/draft.py @@ -0,0 +1,209 @@ +from datetime import datetime, timezone +from enum import Enum +from typing import Annotated + +from epyxid import XID +from fastapi import status +from pydantic import BaseModel, Field +from sqlalchemy import Column, DateTime, Index, String, func, select + +from intentkit.models.agent import ( + AgentUserInput, + AgentUserInputColumns, +) +from intentkit.models.base import Base +from intentkit.models.db import get_session +from intentkit.utils.error import IntentKitAPIError + + +class AgentState(str, Enum): + """Agent state.""" + + PRIVATE = "private" + PUBLIC = "public" + CITIZEN = "citizen" + + +class AgentExtra(BaseModel): + """Agent extra data in AgentUpdate.""" + + state: AgentState = Field(default=AgentState.PRIVATE, description="Agent state") + draft_id: str = Field(description="Draft ID") + project_id: str | None = Field( + default=None, description="Project ID, forward compatible" + ) + request_id: str | None = Field( + default=None, description="Request ID, forward compatible" + ) + create_tx_id: str | None = Field( + default=None, description="Transaction hash used when the agent was created" + ) + + +class AgentDraftTable(Base, AgentUserInputColumns): + """Agent table db model.""" + + __tablename__ = "agent_drafts" + + id = Column( + String, + primary_key=True, + comment="Unique identifier for the agent. Must be URL-safe, containing only lowercase letters, numbers, and hyphens", + ) + agent_id = Column( + String, + nullable=False, + comment="Agent id", + ) + owner = Column( + String, + nullable=True, + comment="Owner identifier of the agent, used for access control", + ) + version = Column( + String, + nullable=True, + comment="Version hash of the agent", + ) + project_id = Column( + String, + nullable=True, + comment="Project ID, forward compatible", + ) + last_draft_id = Column( + String, + nullable=True, + comment="ID of the last draft that was deployed", + ) + deployed_at = Column( + DateTime(timezone=True), + nullable=True, + comment="Timestamp when the agent was deployed", + ) + # auto timestamp + created_at = Column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + comment="Timestamp when the agent was created", + ) + updated_at = Column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + onupdate=lambda: datetime.now(timezone.utc), + comment="Timestamp when the agent was last updated", + ) + + # Indexes for optimal query performance + __table_args__ = ( + # Index for queries filtering by agent_id and owner (most common pattern) + Index("idx_agent_drafts_agent_owner", "agent_id", "owner"), + # Index for queries ordering by created_at (for latest draft queries) + Index("idx_agent_drafts_created_at", "created_at"), + # Composite index for agent_id, owner, and created_at (covers all common query patterns) + Index( + "idx_agent_drafts_agent_owner_created", "agent_id", "owner", "created_at" + ), + ) + + +class AgentDraft(AgentUserInput): + """Agent draft model.""" + + id: Annotated[ + str, + Field( + default_factory=lambda: str(XID()), + description="Unique identifier for the draft", + ), + ] + agent_id: Annotated[ + str, + Field( + description="Agent id", + ), + ] + owner: Annotated[ + str | None, + Field( + default=None, + description="Owner identifier of the agent, used for access control", + max_length=50, + ), + ] + version: Annotated[ + str | None, + Field( + default=None, + description="Version hash of the agent", + ), + ] + project_id: Annotated[ + str | None, + Field( + default=None, + description="Project ID, forward compatible", + ), + ] + last_draft_id: Annotated[ + str | None, + Field( + default=None, + description="ID of the last draft that was deployed", + ), + ] + deployed_at: Annotated[ + datetime | None, + Field( + default=None, + description="Timestamp when the agent was deployed", + ), + ] + # auto timestamp + created_at: Annotated[ + datetime, + Field( + description="Timestamp when the agent was created, will ignore when importing" + ), + ] + updated_at: Annotated[ + datetime, + Field( + description="Timestamp when the agent was last updated, will ignore when importing" + ), + ] + + @staticmethod + async def exist(agent_id: str, user_id: str | None = None) -> None: + """Check if an agent exists in the draft table. + + Args: + agent_id: The agent ID to check + user_id: Optional user ID to check ownership + + Raises: + IntentKitAPIError: 404 if agent not found, 403 if user doesn't own the agent + """ + async with get_session() as session: + query = ( + select(AgentDraftTable) + .where(AgentDraftTable.agent_id == agent_id) + .limit(1) + ) + result = await session.execute(query) + draft = result.scalar_one_or_none() + + if not draft: + raise IntentKitAPIError( + status.HTTP_404_NOT_FOUND, + "AgentNotFound", + f"Agent {agent_id} not found", + ) + + if user_id is not None and draft.owner != user_id: + raise IntentKitAPIError( + status.HTTP_403_FORBIDDEN, + "AgentForbidden", + "Agent does not belong to user", + ) diff --git a/uv.lock b/uv.lock index 29c859d5..8d9bbe13 100644 --- a/uv.lock +++ b/uv.lock @@ -108,16 +108,16 @@ wheels = [ [[package]] name = "alembic" -version = "1.17.1" +version = "1.17.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/b6/2a81d7724c0c124edc5ec7a167e85858b6fd31b9611c6fb8ecf617b7e2d3/alembic-1.17.1.tar.gz", hash = "sha256:8a289f6778262df31571d29cca4c7fbacd2f0f582ea0816f4c399b6da7528486", size = 1981285, upload-time = "2025-10-29T00:23:16.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/a6/74c8cadc2882977d80ad756a13857857dbcf9bd405bc80b662eb10651282/alembic-1.17.2.tar.gz", hash = "sha256:bbe9751705c5e0f14877f02d46c53d10885e377e3d90eda810a016f9baa19e8e", size = 1988064, upload-time = "2025-11-14T20:35:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/32/7df1d81ec2e50fb661944a35183d87e62d3f6c6d9f8aff64a4f245226d55/alembic-1.17.1-py3-none-any.whl", hash = "sha256:cbc2386e60f89608bb63f30d2d6cc66c7aaed1fe105bd862828600e5ad167023", size = 247848, upload-time = "2025-10-29T00:23:18.79Z" }, + { url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554, upload-time = "2025-11-14T20:35:05.699Z" }, ] [[package]] @@ -140,7 +140,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.72.1" +version = "0.73.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -152,9 +152,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/f3/feb750a21461090ecf48bbebcaa261cd09003cc1d14e2fa9643ad59edd4d/anthropic-0.72.1.tar.gz", hash = "sha256:a6d1d660e1f4af91dddc732f340786d19acaffa1ae8e69442e56be5fa6539d51", size = 415395, upload-time = "2025-11-11T16:53:29.001Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/07/f550112c3f5299d02f06580577f602e8a112b1988ad7c98ac1a8f7292d7e/anthropic-0.73.0.tar.gz", hash = "sha256:30f0d7d86390165f86af6ca7c3041f8720bb2e1b0e12a44525c8edfdbd2c5239", size = 425168, upload-time = "2025-11-14T18:47:52.635Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/05/d9d45edad1aa28330cea09a3b35e1590f7279f91bb5ab5237c70a0884ea3/anthropic-0.72.1-py3-none-any.whl", hash = "sha256:81e73cca55e8924776c8c4418003defe6bf9eaf0cd92beb94c8dbf537b95316f", size = 357373, upload-time = "2025-11-11T16:53:27.438Z" }, + { url = "https://files.pythonhosted.org/packages/15/b1/5d4d3f649e151e58dc938cf19c4d0cd19fca9a986879f30fea08a7b17138/anthropic-0.73.0-py3-none-any.whl", hash = "sha256:0d56cd8b3ca3fea9c9b5162868bdfd053fbc189b8b56d4290bd2d427b56db769", size = 367839, upload-time = "2025-11-14T18:47:51.195Z" }, ] [[package]] @@ -403,30 +403,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.40.73" +version = "1.40.74" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/d1/3be32bf855200fb2defb4ab2b251aba4d05e787235adbf3ae1ba2099545b/boto3-1.40.73.tar.gz", hash = "sha256:3716703cb8b126607533853d7e2a85f0bb23b0b9d4805c69170abead33d725ef", size = 111607, upload-time = "2025-11-13T20:27:24.969Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/37/0db5fc46548b347255310893f1a47971a1d8eb0dbc46dfb5ace8a1e7d45e/boto3-1.40.74.tar.gz", hash = "sha256:484e46bf394b03a7c31b34f90945ebe1390cb1e2ac61980d128a9079beac87d4", size = 111592, upload-time = "2025-11-14T20:29:10.991Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b7/350cf5d2ed824c75a596499e6a77c5ec6d35e9437b8c10a936900461c6e3/boto3-1.40.73-py3-none-any.whl", hash = "sha256:85172e11e3b8d5a09504bc532b6589730ac68845410403ca3793d037b8a5d445", size = 139360, upload-time = "2025-11-13T20:27:22.689Z" }, + { url = "https://files.pythonhosted.org/packages/d2/08/c52751748762901c0ca3c3019e3aa950010217f0fdf9940ebe68e6bb2f5a/boto3-1.40.74-py3-none-any.whl", hash = "sha256:41fc8844b37ae27b24bcabf8369769df246cc12c09453988d0696ad06d6aa9ef", size = 139360, upload-time = "2025-11-14T20:29:09.477Z" }, ] [[package]] name = "botocore" -version = "1.40.73" +version = "1.40.74" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/83/4afe8a1fdd4b5200ceff986b1e72be16c55010980bf337360535733d85c3/botocore-1.40.73.tar.gz", hash = "sha256:0650ceada268824282da9af8615f3e4cf2453be8bf85b820f9207eff958d56d0", size = 14452167, upload-time = "2025-11-13T20:27:13.302Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/dc/0412505f05286f282a75bb0c650e525ddcfaf3f6f1a05cd8e99d32a2db06/botocore-1.40.74.tar.gz", hash = "sha256:57de0b9ffeada06015b3c7e5186c77d0692b210d9e5efa294f3214df97e2f8ee", size = 14452479, upload-time = "2025-11-14T20:29:00.949Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/9c/f5085b99a4e0ec8454ab2d1b9492f7e10c0d008578cb26a856d7a8240b40/botocore-1.40.73-py3-none-any.whl", hash = "sha256:87524c5fe552ecceaea72f51163b37ab35eb82aaa6a64eb80489ade7340c1d23", size = 14118004, upload-time = "2025-11-13T20:27:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a2/306dec16e3c84f3ca7aaead0084358c1c7fbe6501f6160844cbc93bc871e/botocore-1.40.74-py3-none-any.whl", hash = "sha256:f39f5763e35e75f0bd91212b7b36120b1536203e8003cd952ef527db79702b15", size = 14117911, upload-time = "2025-11-14T20:28:58.153Z" }, ] [[package]] @@ -530,14 +530,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.0" +version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[package]] @@ -1571,30 +1571,30 @@ wheels = [ [[package]] name = "langchain" -version = "1.0.5" +version = "1.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/5d/c98f2ffaefc73845a1f6bc66a8c2a643e36ce8ec09cff1307216c115d22c/langchain-1.0.5.tar.gz", hash = "sha256:7e0635b36a7f7a649be21fcce4c82b7428bcf72a5d14aacdf9f2636c4775f159", size = 461860, upload-time = "2025-11-07T23:04:59.414Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/7a/63c041d1ee74c505e30ec882889117baa08afce2264bcb4463929bac4e94/langchain-1.0.7.tar.gz", hash = "sha256:e3f8ad742b4cdc91d728f96bd70e4688bc11ffeca3bd160c5fe9937625d541b9", size = 465198, upload-time = "2025-11-14T20:52:21.813Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/4f/2603973fb3b74c717335703851a45914bc9794fbfaeb4ff74f7f08ecf5e8/langchain-1.0.5-py3-none-any.whl", hash = "sha256:d59ce7303f1d9e4bca41855b20a1842f4470a22d09a64fb93fb0ff30a2d36d4b", size = 93779, upload-time = "2025-11-07T23:04:57.83Z" }, + { url = "https://files.pythonhosted.org/packages/8e/4a/02c14af46fa79ce7b02a0f8af46f5905cc7e8b647a5f1a7c793c03ac5063/langchain-1.0.7-py3-none-any.whl", hash = "sha256:cf33b4d60d7a2ff7f0f313441628927853192cdbab9d6d8ce229909a868bbf12", size = 93738, upload-time = "2025-11-14T20:52:20.717Z" }, ] [[package]] name = "langchain-anthropic" -version = "1.0.3" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anthropic" }, { name = "langchain-core" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/6b/aaa770beea6f4ed4c3f5c75fd6d80ed5c82708aec15318c06d9379dd3543/langchain_anthropic-1.0.3.tar.gz", hash = "sha256:91083c5df82634602f6772989918108d9448fa0b7499a11434687198f5bf9aef", size = 680336, upload-time = "2025-11-12T15:58:58.54Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/db/3e80e99002eaaeae71b90df6b8e604fc1f041f34a2bc9fb61a5d8b3799df/langchain_anthropic-1.0.4.tar.gz", hash = "sha256:46e2a842755609d4a0e9dcc505779093b1a462b04ee9024cbd751ea6853a8890", size = 669908, upload-time = "2025-11-14T19:01:24.144Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/3d/0499eeb10d333ea79e1c156b84d075e96cfde2fbc6a9ec9cbfa50ac3e47e/langchain_anthropic-1.0.3-py3-none-any.whl", hash = "sha256:0d4106111d57e19988e2976fb6c6e59b0c47ca7afb0f6a2f888362006f871871", size = 46608, upload-time = "2025-11-12T15:58:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/053f7a719d133ab889fd5b46ef2ab834c9f0d8b646ea587ad3773b4c089c/langchain_anthropic-1.0.4-py3-none-any.whl", hash = "sha256:ba3e580ac0691b268082b82280bb898af485b58dbf23a65f27e692c009e543ad", size = 46858, upload-time = "2025-11-14T19:01:22.814Z" }, ] [[package]] @@ -1640,7 +1640,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.0.4" +version = "1.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -1651,9 +1651,9 @@ dependencies = [ { name = "tenacity" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/35/147544d3422464d13a8ef88f9e25cff25e02c985eb44f8c106503f56ad50/langchain_core-1.0.4.tar.gz", hash = "sha256:086d408bcbeedecb0b152201e0163b85e7a6d9b26e11a75cc577b7371291df4e", size = 776329, upload-time = "2025-11-07T22:30:45.669Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/61/c356e19525a210baf960968dbfb03ee38a05e05ddb41efeb32abfcb4e360/langchain_core-1.0.5.tar.gz", hash = "sha256:7ecbad9a60dde626252733a9c18c7377f4468cfe00465ffa99f5e9c6cb9b82d2", size = 778259, upload-time = "2025-11-14T16:59:27.277Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/ac/7032e5eb1c147a3d8e0a21a70e77d7efbd6295c8ce4833b90f6ff1750da9/langchain_core-1.0.4-py3-none-any.whl", hash = "sha256:53caa351d9d73b56f5d9628980f36851cfa725977508098869fdc2d246da43b3", size = 471198, upload-time = "2025-11-07T22:30:44.003Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ee/aaf2343a35080154c82ceb110e03dd00f15459bc72e518df51724cbc41a9/langchain_core-1.0.5-py3-none-any.whl", hash = "sha256:d24c0cf12cfcd96dd4bd479aa91425f3a6652226cd824228ae422a195067b74e", size = 471506, upload-time = "2025-11-14T16:59:25.629Z" }, ] [[package]] @@ -1685,16 +1685,16 @@ wheels = [ [[package]] name = "langchain-openai" -version = "1.0.2" +version = "1.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "openai" }, { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/3c/edb7ffca76fdcfd938ce8380bf8ec79a0a8be41ba7fdbf6f9fe1cb5fd1a8/langchain_openai-1.0.2.tar.gz", hash = "sha256:621e8295c52db9a1fc74806a0bd227ea215c132c6c5e421d2982c9ee78468769", size = 1025578, upload-time = "2025-11-03T14:08:32.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/73/6a96bc3a48825317886fa52a2a598286d35cf0384fce5dc3e5da7be06fd0/langchain_openai-1.0.3.tar.gz", hash = "sha256:e9df56540c1118002ab5306208c4845715e9209779c8a7ac9037eded98435fdc", size = 1032676, upload-time = "2025-11-15T00:29:03.774Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/9b/7af1d539a051d195c5ecc5990ebd483f208c40f75a8a9532846d16762704/langchain_openai-1.0.2-py3-none-any.whl", hash = "sha256:b3eb9b82752063b46452aa868d8c8bc1604e57631648c3bc325bba58d3aeb143", size = 81934, upload-time = "2025-11-03T14:08:30.655Z" }, + { url = "https://files.pythonhosted.org/packages/ff/de/0cb08f8732f070397233df7ad5ef461d83784ce567e7a57d5de5eb96851f/langchain_openai-1.0.3-py3-none-any.whl", hash = "sha256:18d254dbe946d9e9fe6d31416c60c8fc06513427f6e8d8c372e015345e1e17f6", size = 82536, upload-time = "2025-11-15T00:29:02.573Z" }, ] [[package]] @@ -1830,7 +1830,7 @@ dependencies = [ [[package]] name = "langsmith" -version = "0.4.42" +version = "0.4.43" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -1841,9 +1841,9 @@ dependencies = [ { name = "requests-toolbelt" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/91/939cb2fa0317a8aedd0b4dab6c189722e5ef15abcf65304dc929e582826a/langsmith-0.4.42.tar.gz", hash = "sha256:a6e808e47581403cb019b47c8c10627c1644f78ed4c03fa877d6ad661476c38f", size = 953877, upload-time = "2025-11-09T16:31:21.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/b4/073e3fd494f7853fd4e59f5ae56c49f672e081e65f17ef363224e60530ab/langsmith-0.4.43.tar.gz", hash = "sha256:75c2468ab740438adfb32af8595ad8837c3af2bd1cdaf057d534182c5a07407a", size = 984142, upload-time = "2025-11-15T00:32:12.454Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/17/4280bc381b40a642ea5efe1bab0237f03507a9d4281484c5baa1db82055a/langsmith-0.4.42-py3-none-any.whl", hash = "sha256:015b0a0c17eb1a61293e8cbb7d41778a4b37caddd267d54274ba94e4721b301b", size = 401937, upload-time = "2025-11-09T16:31:19.163Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5c/521a3d8295e2e7caea67032e65554866293b6dc8e934bd86be8cc1f7b955/langsmith-0.4.43-py3-none-any.whl", hash = "sha256:c97846a0b15061bc15844aac32fd1ce4a8e50983905f80a0d6079bb41b112ae3", size = 410232, upload-time = "2025-11-15T00:32:10.557Z" }, ] [[package]]