[PAI-1055] Chore: Upgrade Plane Python SDK Version in Plane AI (#5195)
This commit is contained in:
@@ -43,17 +43,17 @@ from pi.services.llm.error_handling import llm_error_handler
|
||||
from pi.services.llm.llms import get_sql_agent_llm
|
||||
from pi.services.schemas.chat import QueryFlowStore
|
||||
|
||||
from .helpers import _fix_group_by_order_by_mismatch_parsed
|
||||
from .helpers import _get_available_tables
|
||||
from .helpers import execute_sql_query
|
||||
from .helpers import fix_group_by_order_by_mismatch
|
||||
from .helpers import format_column_context
|
||||
from .helpers import generate_cte_query
|
||||
from .helpers import get_column_details
|
||||
from .helpers import get_table_schemas
|
||||
from .prompts import TABLE_SELECTION
|
||||
from .prompts import get_sql_generator
|
||||
from .schemas import TableSelectionResponse
|
||||
from .tools import _fix_group_by_order_by_mismatch_parsed
|
||||
from .tools import _get_available_tables
|
||||
from .tools import execute_sql_query
|
||||
from .tools import fix_group_by_order_by_mismatch
|
||||
from .tools import format_column_context
|
||||
from .tools import generate_cte_query
|
||||
from .tools import get_column_details
|
||||
from .tools import get_table_schemas
|
||||
|
||||
log = logger.getChild(__name__)
|
||||
console = Console()
|
||||
|
||||
@@ -161,6 +161,14 @@ def format_column_context(column_context: Dict[str, Any]) -> str:
|
||||
distinct_values = column["distinct_values_in_column"]
|
||||
distinct_formatted = ", ".join(f"`{value}`" for value in distinct_values)
|
||||
formatted_lines.append(f" - **Distinct Values:** {distinct_formatted}")
|
||||
elif "computed_status_conditions" in column:
|
||||
conditions = column["computed_status_conditions"]
|
||||
formatted_lines.append(f" - **Computed Status Conditions:**")
|
||||
for cond in conditions:
|
||||
status = cond.get("status", "N/A")
|
||||
cond_desc = cond.get("description", "No description")
|
||||
sql_condition = cond.get("sql_condition", "N/A")
|
||||
formatted_lines.append(f" - **`{status}`** ({cond_desc}): `{sql_condition}`")
|
||||
formatted_lines.append("")
|
||||
return "\n".join(formatted_lines)
|
||||
|
||||
@@ -1097,6 +1105,31 @@ async def construct_action_entity_url(
|
||||
else:
|
||||
return None
|
||||
|
||||
elif entity_type == "workspace":
|
||||
# For workspaces: /workspace_slug/
|
||||
url = f"{api_base_url}/{workspace_slug}/"
|
||||
return {"entity_url": url, "entity_name": entity_name, "entity_type": entity_type, "entity_id": entity_id}
|
||||
|
||||
elif entity_type == "sticky":
|
||||
# For stickies: /workspace_slug/stickies/
|
||||
url = f"{api_base_url}/{workspace_slug}/stickies/"
|
||||
return {"entity_url": url, "entity_name": entity_name, "entity_type": entity_type, "entity_id": entity_id}
|
||||
|
||||
elif entity_type == "initiative":
|
||||
# For initiatives: /workspace_slug/initiatives/
|
||||
url = f"{api_base_url}/{workspace_slug}/initiatives/"
|
||||
return {"entity_url": url, "entity_name": entity_name, "entity_type": entity_type, "entity_id": entity_id}
|
||||
|
||||
elif entity_type == "teamspace":
|
||||
# For teamspaces: /workspace_slug/teamspaces/
|
||||
url = f"{api_base_url}/{workspace_slug}/teamspaces/"
|
||||
return {"entity_url": url, "entity_name": entity_name, "entity_type": entity_type, "entity_id": entity_id}
|
||||
|
||||
elif entity_type == "customer":
|
||||
# For customers: /workspace_slug/customers/
|
||||
url = f"{api_base_url}/{workspace_slug}/customers/"
|
||||
return {"entity_url": url, "entity_name": entity_name, "entity_type": entity_type, "entity_id": entity_id}
|
||||
|
||||
else:
|
||||
# Unknown entity type
|
||||
return None
|
||||
@@ -16,7 +16,7 @@ from typing import Any
|
||||
|
||||
# Lazy import to avoid circular dependency
|
||||
# from pi.services.chat.prompts import plane_context
|
||||
from pi.agents.sql_agent.tools import format_table_details
|
||||
from pi.agents.sql_agent.helpers import format_table_details
|
||||
|
||||
table_description_content: dict[str, dict[str, Any]] = json.loads(read_text("pi.agents.sql_agent.store", "table-descriptions.json"))
|
||||
table_descriptions = format_table_details(table_description_content)
|
||||
|
||||
@@ -1,245 +1,273 @@
|
||||
[
|
||||
{
|
||||
"table": "intake_issues",
|
||||
"columns": [
|
||||
{
|
||||
"name": "status",
|
||||
"description": "Enumerated states representing the status of an issue in intake",
|
||||
"enumerations": [
|
||||
{
|
||||
"identifier": -2,
|
||||
"description": "Pending"
|
||||
},
|
||||
{
|
||||
"identifier": -1,
|
||||
"description": "Rejected"
|
||||
},
|
||||
{
|
||||
"identifier": 0,
|
||||
"description": "Snoozed"
|
||||
},
|
||||
{
|
||||
"identifier": 1,
|
||||
"description": "Accepted"
|
||||
},
|
||||
{
|
||||
"identifier": 2,
|
||||
"description": "Duplicate"
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"table": "intake_issues",
|
||||
"columns": [
|
||||
{
|
||||
"name": "status",
|
||||
"description": "Enumerated states representing the status of an issue in intake",
|
||||
"enumerations": [
|
||||
{
|
||||
"identifier": -2,
|
||||
"description": "Pending"
|
||||
},
|
||||
{
|
||||
"identifier": -1,
|
||||
"description": "Rejected"
|
||||
},
|
||||
{
|
||||
"identifier": 0,
|
||||
"description": "Snoozed"
|
||||
},
|
||||
{
|
||||
"identifier": 1,
|
||||
"description": "Accepted"
|
||||
},
|
||||
{
|
||||
"identifier": 2,
|
||||
"description": "Duplicate"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "project_members",
|
||||
"columns": [
|
||||
{
|
||||
"name": "role",
|
||||
"description": "Enumerated role representing the role of a memebr in project",
|
||||
"enumerations": [
|
||||
{
|
||||
"identifier": 5,
|
||||
"description": "Guest"
|
||||
},
|
||||
{
|
||||
"identifier": 15,
|
||||
"description": "Member"
|
||||
},
|
||||
{
|
||||
"identifier": 20,
|
||||
"description": "Admin"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "project_members",
|
||||
"columns": [
|
||||
{
|
||||
"name": "role",
|
||||
"description": "Enumerated role representing the role of a memebr in project",
|
||||
"enumerations": [
|
||||
{
|
||||
"identifier": 5,
|
||||
"description": "Guest"
|
||||
},
|
||||
{
|
||||
"identifier": 15,
|
||||
"description": "Member"
|
||||
},
|
||||
{
|
||||
"identifier": 20,
|
||||
"description": "Admin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "workspace_members",
|
||||
"columns": [
|
||||
{
|
||||
"name": "role",
|
||||
"description": "Enumerated role representing the role of a memebr in workspace",
|
||||
"enumerations": [
|
||||
{
|
||||
"identifier": 5,
|
||||
"description": "Guest"
|
||||
},
|
||||
{
|
||||
"identifier": 15,
|
||||
"description": "Member"
|
||||
},
|
||||
{
|
||||
"identifier": 20,
|
||||
"description": "Admin"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "workspace_members",
|
||||
"columns": [
|
||||
{
|
||||
"name": "role",
|
||||
"description": "Enumerated role representing the role of a memebr in workspace",
|
||||
"enumerations": [
|
||||
{
|
||||
"identifier": 5,
|
||||
"description": "Guest"
|
||||
},
|
||||
{
|
||||
"identifier": 15,
|
||||
"description": "Member"
|
||||
},
|
||||
{
|
||||
"identifier": 20,
|
||||
"description": "Admin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "issue_relations",
|
||||
"columns": [
|
||||
{
|
||||
"name": "relation_type",
|
||||
"description": "The type of relationship between two issues.",
|
||||
"distinct_values_in_column": [
|
||||
"duplicate",
|
||||
"finish_before",
|
||||
"blocked_by",
|
||||
"start_before",
|
||||
"relates_to"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "issue_relations",
|
||||
"columns": [
|
||||
{
|
||||
"name": "relation_type",
|
||||
"description": "The type of relationship between two issues.",
|
||||
"distinct_values_in_column": ["duplicate", "finish_before", "blocked_by", "start_before", "relates_to"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "issue_activities",
|
||||
"columns": [
|
||||
{
|
||||
"name": "verb",
|
||||
"description": "The action that was performed on field present in 'field' column for the given issue.",
|
||||
"distinct_values_in_column": ["created", "deleted", "removed", "updated"]
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"description": "The name of the field on which activity is performed.",
|
||||
"distinct_values_in_column": [
|
||||
"archived_at",
|
||||
"assignees",
|
||||
"attachment",
|
||||
"blocked_by",
|
||||
"blocking",
|
||||
"comment",
|
||||
"cycles",
|
||||
"description",
|
||||
"draft",
|
||||
"duplicate",
|
||||
"estimate_point",
|
||||
"finish_before",
|
||||
"finish_after",
|
||||
"intake",
|
||||
"issue",
|
||||
"labels",
|
||||
"link",
|
||||
"modules",
|
||||
"name",
|
||||
"parent",
|
||||
"priority",
|
||||
"project",
|
||||
"reaction",
|
||||
"relates_to",
|
||||
"sequence_id",
|
||||
"start_before",
|
||||
"start_after",
|
||||
"start_date",
|
||||
"state",
|
||||
"target_date",
|
||||
"type",
|
||||
"vote"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "issue_activities",
|
||||
"columns": [
|
||||
{
|
||||
"name": "verb",
|
||||
"description": "The action that was performed on field present in 'field' column for the given issue.",
|
||||
"distinct_values_in_column": [
|
||||
"created",
|
||||
"deleted",
|
||||
"removed",
|
||||
"updated"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "field",
|
||||
"description": "The name of the field on which activity is performed.",
|
||||
"distinct_values_in_column": [
|
||||
"archived_at",
|
||||
"assignees",
|
||||
"attachment",
|
||||
"blocked_by",
|
||||
"blocking",
|
||||
"comment",
|
||||
"cycles",
|
||||
"description",
|
||||
"draft",
|
||||
"duplicate",
|
||||
"estimate_point",
|
||||
"finish_before",
|
||||
"finish_after",
|
||||
"intake",
|
||||
"issue",
|
||||
"labels",
|
||||
"link",
|
||||
"modules",
|
||||
"name",
|
||||
"parent",
|
||||
"priority",
|
||||
"project",
|
||||
"reaction",
|
||||
"relates_to",
|
||||
"sequence_id",
|
||||
"start_before",
|
||||
"start_after",
|
||||
"start_date",
|
||||
"state",
|
||||
"target_date",
|
||||
"type",
|
||||
"vote"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "new_value",
|
||||
"description": "The new value (string) of the field after the update."
|
||||
},
|
||||
{
|
||||
"name": "old_value",
|
||||
"description": "The old value (string) of the field before the update."
|
||||
},
|
||||
{
|
||||
"name": "new_identifier",
|
||||
"description": "The identifier (UUID) of the new value of the field after the update."
|
||||
},
|
||||
{
|
||||
"name": "old_identifier",
|
||||
"description": "The identifier (UUID) of the old value of the field before the update."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "new_value",
|
||||
"description": "The new value (string) of the field after the update."
|
||||
},
|
||||
{
|
||||
"name": "old_value",
|
||||
"description": "The old value (string) of the field before the update."
|
||||
},
|
||||
{
|
||||
"name": "new_identifier",
|
||||
"description": "The identifier (UUID) of the new value of the field after the update."
|
||||
},
|
||||
{
|
||||
"name": "old_identifier",
|
||||
"description": "The identifier (UUID) of the old value of the field before the update."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "states",
|
||||
"columns": [
|
||||
{
|
||||
"name": "group",
|
||||
"description": "The name of the group to which the issue state belongs.",
|
||||
"distinct_values_in_column": ["started", "backlog", "completed", "cancelled", "unstarted"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "issue_attachments",
|
||||
"columns": [
|
||||
{
|
||||
"name": "attributes",
|
||||
"description": "Attributes of the attachment (size, name).",
|
||||
"fields_in_jsonb_object": ["name", "size"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "project_states",
|
||||
"columns": [
|
||||
{
|
||||
"name": "group",
|
||||
"description": "The name of the group to which the project state belongs.",
|
||||
"distinct_values_in_column": ["monitoring", "draft", "completed", "cancelled", "planning", "execution"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "projects",
|
||||
"columns": [
|
||||
{
|
||||
"name": "network",
|
||||
"description": "Enumerated numbers representing the visibility of a project (public/ private).",
|
||||
"enumerations": [
|
||||
{
|
||||
"identifier": 0,
|
||||
"description": "Private"
|
||||
},
|
||||
{
|
||||
"identifier": 2,
|
||||
"description": "Public"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "states",
|
||||
"columns": [
|
||||
{
|
||||
"name": "group",
|
||||
"description": "The name of the group to which the issue state belongs.",
|
||||
"distinct_values_in_column": [
|
||||
"started",
|
||||
"backlog",
|
||||
"completed",
|
||||
"cancelled",
|
||||
"unstarted"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "issues",
|
||||
"columns": [
|
||||
{
|
||||
"name": "priority",
|
||||
"description": "The priority level assigned to the issue, stored as lowercase text",
|
||||
"distinct_values_in_column": ["urgent", "high", "medium", "low", "none"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "cycles",
|
||||
"columns": [
|
||||
{
|
||||
"name": "status",
|
||||
"description": "IMPORTANT: The 'cycles' table does NOT have a 'status' column. Status is computed dynamically based on 'start_date' and 'end_date'. Use the following conditions to filter by status",
|
||||
"computed_status_conditions": [
|
||||
{
|
||||
"status": "current",
|
||||
"description": "Currently active cycle",
|
||||
"sql_condition": "start_date <= NOW() AND end_date >= NOW()"
|
||||
},
|
||||
{
|
||||
"status": "active",
|
||||
"description": "Alias for 'current' - currently active cycle",
|
||||
"sql_condition": "start_date <= NOW() AND end_date >= NOW()"
|
||||
},
|
||||
{
|
||||
"status": "ongoing",
|
||||
"description": "Alias for 'current' - currently active cycle",
|
||||
"sql_condition": "start_date <= NOW() AND end_date >= NOW()"
|
||||
},
|
||||
{
|
||||
"status": "in-progress",
|
||||
"description": "Alias for 'current' - currently active cycle",
|
||||
"sql_condition": "start_date <= NOW() AND end_date >= NOW()"
|
||||
},
|
||||
{
|
||||
"status": "upcoming",
|
||||
"description": "Cycle that hasn't started yet",
|
||||
"sql_condition": "start_date > NOW()"
|
||||
},
|
||||
{
|
||||
"status": "completed",
|
||||
"description": "Cycle that has ended",
|
||||
"sql_condition": "end_date < NOW()"
|
||||
},
|
||||
{
|
||||
"status": "draft",
|
||||
"description": "Cycle without start or end dates",
|
||||
"sql_condition": "start_date IS NULL AND end_date IS NULL"
|
||||
},
|
||||
{
|
||||
"status": "incomplete",
|
||||
"description": "Cycle that is not yet completed (current, upcoming, or draft)",
|
||||
"sql_condition": "end_date >= NOW() OR end_date IS NULL"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "issue_attachments",
|
||||
"columns": [
|
||||
{
|
||||
"name": "attributes",
|
||||
"description": "Attributes of the attachment (size, name).",
|
||||
"fields_in_jsonb_object": [
|
||||
"name",
|
||||
"size"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "project_states",
|
||||
"columns": [
|
||||
{
|
||||
"name": "group",
|
||||
"description": "The name of the group to which the project state belongs.",
|
||||
"distinct_values_in_column": [
|
||||
"monitoring",
|
||||
"draft",
|
||||
"completed",
|
||||
"cancelled",
|
||||
"planning",
|
||||
"execution"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "projects",
|
||||
"columns": [
|
||||
{
|
||||
"name": "network",
|
||||
"description": "Enumerated numbers representing the visibility of a project (public/ private).",
|
||||
"enumerations": [
|
||||
{
|
||||
"identifier": 0,
|
||||
"description": "Private"
|
||||
},
|
||||
{
|
||||
"identifier": 2,
|
||||
"description": "Public"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "issues",
|
||||
"columns": [
|
||||
{
|
||||
"name": "priority",
|
||||
"description": "The priority level assigned to the issue, stored as lowercase text",
|
||||
"distinct_values_in_column": [
|
||||
"urgent",
|
||||
"high",
|
||||
"medium",
|
||||
"low",
|
||||
"none"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "modules",
|
||||
"columns": [
|
||||
{
|
||||
"name": "status",
|
||||
"description": "The status of a module, stored as a string",
|
||||
"distinct_values_in_column": ["backlog", "planned", "in-progress", "paused", "completed", "cancelled"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -381,6 +381,11 @@ async def get_answer_for_silo_app(data: ChatRequest, request: Request, db: Async
|
||||
# Add this action's artifact to the list
|
||||
artifact_data_objects.append(ArtifactData(artifact_id=artifact_id, is_edited=False, action_data=actions_data))
|
||||
|
||||
# Type assertions for mypy (validated above)
|
||||
assert workspace_id is not None
|
||||
assert chat_id is not None
|
||||
assert message_id is not None
|
||||
|
||||
# Execute batch actions using the service with ALL artifacts
|
||||
service = BuildModeToolExecutor(chatbot=PlaneChatBot(plane_apps_llm), db=db)
|
||||
result = await service.execute(
|
||||
@@ -411,7 +416,7 @@ async def get_answer_for_silo_app(data: ChatRequest, request: Request, db: Async
|
||||
formatted_context = result
|
||||
|
||||
# Parse JSON response for proper API format (avoid double-encoded JSON string)
|
||||
parsed_response: Union[str, Dict[str, Any]] = final_response
|
||||
parsed_response: Union[str, Dict[str, Any], Any] = final_response
|
||||
if response_type == "response" and final_response:
|
||||
try:
|
||||
# Try to parse as JSON for structured app responses
|
||||
@@ -516,6 +521,11 @@ async def get_answer_for_slack(data: ChatRequest, request: Request, db: AsyncSes
|
||||
# Add this action's artifact to the list
|
||||
artifact_data_objects.append(ArtifactData(artifact_id=artifact_id, is_edited=False, action_data=actions_data))
|
||||
|
||||
# Type assertions for mypy (validated above)
|
||||
assert workspace_id is not None
|
||||
assert chat_id is not None
|
||||
assert message_id is not None
|
||||
|
||||
# Execute batch actions using the service with ALL artifacts
|
||||
service = BuildModeToolExecutor(chatbot=PlaneChatBot(slack_ai_llm), db=db)
|
||||
result = await service.execute(
|
||||
|
||||
@@ -178,6 +178,11 @@ async def create_response_slack(data: ChatRequest, request: Request, db: AsyncSe
|
||||
|
||||
artifact_data_objects.append(ArtifactData(artifact_id=artifact_id, is_edited=False, action_data=actions_data))
|
||||
|
||||
# Type assertions for mypy (validated above)
|
||||
assert workspace_id is not None
|
||||
assert chat_id is not None
|
||||
assert message_id is not None
|
||||
|
||||
# Execute batch actions using the service
|
||||
service = BuildModeToolExecutor(chatbot=PlaneChatBot("gpt-4.1"), db=db)
|
||||
|
||||
|
||||
+29
-6
@@ -428,6 +428,10 @@ class Settings:
|
||||
colorlog.getLogger("httpx").setLevel(colorlog.WARNING)
|
||||
colorlog.getLogger("httpcore").setLevel(colorlog.WARNING)
|
||||
|
||||
# Suppress Anthropic client debug logs
|
||||
colorlog.getLogger("anthropic").setLevel(colorlog.WARNING)
|
||||
colorlog.getLogger("anthropic._base_client").setLevel(colorlog.WARNING)
|
||||
|
||||
# Suppress boto3/botocore debug logs
|
||||
colorlog.getLogger("boto3").setLevel(colorlog.INFO)
|
||||
colorlog.getLogger("botocore").setLevel(colorlog.INFO)
|
||||
@@ -453,13 +457,32 @@ class Settings:
|
||||
colorlog.getLogger("datadog").setLevel(colorlog.INFO)
|
||||
colorlog.getLogger("datadog.dogstatsd").setLevel(colorlog.INFO)
|
||||
|
||||
colorlog.getLogger("ddtrace.internal.telemetry").setLevel(colorlog.INFO)
|
||||
colorlog.getLogger("ddtrace.internal.telemetry.writer").setLevel(colorlog.INFO)
|
||||
# Conditional logging format based on DD_ENABLED (Datadog)
|
||||
# When Datadog is enabled (production), use JSON logging for structured logs
|
||||
# When Datadog is disabled (local dev), use colored logs for readability
|
||||
dd_enabled = get_env_bool("DD_ENABLED", "0")
|
||||
if dd_enabled:
|
||||
# Production: Use JSON formatter for structured logging (Datadog)
|
||||
from pythonjsonlogger.json import JsonFormatter
|
||||
|
||||
from pythonjsonlogger.json import JsonFormatter
|
||||
|
||||
json_formatter = JsonFormatter(fmt="%(asctime)s %(name)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||
handler.setFormatter(json_formatter)
|
||||
colorlog.getLogger("ddtrace.internal.telemetry").setLevel(colorlog.INFO)
|
||||
colorlog.getLogger("ddtrace.internal.telemetry.writer").setLevel(colorlog.INFO)
|
||||
json_formatter = JsonFormatter(fmt="%(asctime)s %(name)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||
handler.setFormatter(json_formatter)
|
||||
else:
|
||||
# Local development: Use colorlog for readable output
|
||||
color_formatter = colorlog.ColoredFormatter(
|
||||
"%(log_color)s%(asctime)s %(name)-20s %(levelname)-8s%(reset)s %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
log_colors={
|
||||
"DEBUG": "cyan",
|
||||
"INFO": "green",
|
||||
"WARNING": "yellow",
|
||||
"ERROR": "red",
|
||||
"CRITICAL": "red,bg_white",
|
||||
},
|
||||
)
|
||||
handler.setFormatter(color_formatter)
|
||||
|
||||
# Get the root logger and configure it
|
||||
root_logger = colorlog.getLogger()
|
||||
|
||||
@@ -17,8 +17,9 @@ from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Field list for projects (matching edit modal fields)
|
||||
# Edit modal shows: name, description, logo_props (emoji/icon), cover_image, network, project_lead
|
||||
PROJECT_FIELDS = ["name", "description", "logo_props", "cover_image", "network", "project_lead"]
|
||||
# Edit modal shows: name, description, network, project_lead
|
||||
# NOTE: logo_props, icon_prop, emoji, cover_image are auto-generated by SDK and should not be included
|
||||
PROJECT_FIELDS = ["name", "description", "network", "project_lead"]
|
||||
|
||||
|
||||
class ProjectProperties(BaseModel):
|
||||
|
||||
@@ -23,6 +23,8 @@ from typing import List
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
from plane.errors import HttpError # type: ignore[attr-defined]
|
||||
|
||||
from .plane_sdk_adapter import PlaneSDKAdapter
|
||||
from .registry import get_available_categories
|
||||
from .registry import get_category_methods
|
||||
@@ -62,6 +64,11 @@ class PlaneActionsExecutor:
|
||||
self.states = self.sdk_adapter
|
||||
self.users = self.sdk_adapter
|
||||
self.workitems = self.sdk_adapter
|
||||
self.workspaces = self.sdk_adapter
|
||||
self.initiatives = self.sdk_adapter
|
||||
self.teamspaces = self.sdk_adapter
|
||||
self.stickies = self.sdk_adapter
|
||||
self.customers = self.sdk_adapter
|
||||
|
||||
def get_api_categories(self) -> Dict[str, str]:
|
||||
"""
|
||||
@@ -105,6 +112,31 @@ class PlaneActionsExecutor:
|
||||
|
||||
return {"success": True, "category": category, "method": method, "data": result}
|
||||
|
||||
except HttpError as e:
|
||||
# Centralized handling for auth errors (401/403)
|
||||
status_code = getattr(e, "status_code", None)
|
||||
if status_code in (401, 403):
|
||||
log.warning(f"Auth error ({status_code}) in {category}.{method}: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication or authorization failed. Please check your credentials and try again.",
|
||||
"status_code": status_code,
|
||||
"category": category,
|
||||
"method": method,
|
||||
}
|
||||
# Non-auth HTTP errors
|
||||
error_msg = f"HTTP error executing {category}.{method}: {str(e)}"
|
||||
log.error(error_msg)
|
||||
return {
|
||||
"success": False,
|
||||
"category": category,
|
||||
"method": method,
|
||||
"error": error_msg,
|
||||
"error_type": "HttpError",
|
||||
"status_code": status_code,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error executing {category}.{method}: {str(e)}"
|
||||
log.error(error_msg)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,10 @@ API_CATEGORIES: Dict[str, str] = {
|
||||
"users": "Manage user information and profiles",
|
||||
"workitems": "Work items/issues - create, update, manage work items",
|
||||
"intake": "Submit and manage work items in intake queue for triage",
|
||||
"initiatives": "Manage strategic initiatives with epics, labels, and projects",
|
||||
"teamspaces": "Manage team collaboration spaces with members and projects",
|
||||
"stickies": "Create and manage quick sticky notes and annotations",
|
||||
"customers": "Manage customer relationships and information",
|
||||
"members": "Manage workspace and project members",
|
||||
"activity": "Track and manage work item activities",
|
||||
"attachments": "Manage file attachments on work items",
|
||||
@@ -38,6 +42,7 @@ API_CATEGORIES: Dict[str, str] = {
|
||||
"properties": "Manage custom properties and their values",
|
||||
"types": "Manage work item types",
|
||||
"worklogs": "Track and manage time entries",
|
||||
"workspaces": "Manage workspace settings and features",
|
||||
}
|
||||
|
||||
# Centralized registry of methods for each category
|
||||
@@ -45,12 +50,11 @@ API_METHODS: Dict[str, Dict[str, str]] = {
|
||||
"workitems": {
|
||||
"create": "Create a new work item/issue",
|
||||
"update": "Update an existing work item/issue",
|
||||
"create_relation": "Create relationships between work items (blocking, blocked_by, duplicate, etc.)",
|
||||
# "list": "List work items with filtering",
|
||||
# "retrieve": "Get a single work item by ID",
|
||||
"retrieve": "Get details of a single work item",
|
||||
"list": "List work items in a project",
|
||||
"delete": "Delete a work item",
|
||||
# "search": "Search work items by criteria",
|
||||
# "get_workspace": "Get work item across workspace",
|
||||
"create_relation": "Create a relationship between work items (blocks, relates_to, etc.)",
|
||||
"search": "Search work items across workspace by query string",
|
||||
},
|
||||
"projects": {
|
||||
"create": "Create a new project",
|
||||
@@ -58,8 +62,8 @@ API_METHODS: Dict[str, Dict[str, str]] = {
|
||||
"retrieve": "Get details of a single project",
|
||||
"update": "Update project details",
|
||||
"delete": "Delete a project",
|
||||
"archive": "Archive a project",
|
||||
"unarchive": "Restore an archived project",
|
||||
"get_features": "Get enabled project features (epics, cycles, modules, etc.)",
|
||||
"update_features": "Enable or disable project features",
|
||||
},
|
||||
"cycles": {
|
||||
"create": "Create a new cycle",
|
||||
@@ -117,6 +121,53 @@ API_METHODS: Dict[str, Dict[str, str]] = {
|
||||
"update": "Update intake work item details",
|
||||
"delete": "Remove intake work item",
|
||||
},
|
||||
"initiatives": {
|
||||
"create": "Create a new initiative",
|
||||
"list": "List initiatives in workspace",
|
||||
"retrieve": "Get single initiative by ID",
|
||||
"update": "Update initiative details",
|
||||
"delete": "Delete an initiative",
|
||||
"create_label": "Create initiative label",
|
||||
"list_labels": "List initiative labels",
|
||||
"retrieve_label": "Get initiative label by ID",
|
||||
"update_label": "Update initiative label",
|
||||
"delete_label": "Delete initiative label",
|
||||
"add_labels": "Add labels to initiative",
|
||||
"remove_labels": "Remove labels from initiative",
|
||||
"add_projects": "Link projects to initiative",
|
||||
"list_projects": "List initiative's projects",
|
||||
"remove_projects": "Unlink projects from initiative",
|
||||
"add_epics": "Link epics to initiative",
|
||||
"list_epics": "List initiative's epics",
|
||||
"remove_epics": "Unlink epics from initiative",
|
||||
},
|
||||
"teamspaces": {
|
||||
"create": "Create a new teamspace",
|
||||
"list": "List teamspaces in workspace",
|
||||
"retrieve": "Get single teamspace by ID",
|
||||
"update": "Update teamspace details",
|
||||
"delete": "Delete a teamspace",
|
||||
"add_members": "Add members to teamspace",
|
||||
"list_members": "List teamspace members",
|
||||
"remove_members": "Remove members from teamspace",
|
||||
"add_projects": "Add projects to teamspace",
|
||||
"list_projects": "List teamspace projects",
|
||||
"remove_projects": "Remove projects from teamspace",
|
||||
},
|
||||
"stickies": {
|
||||
"create": "Create a new sticky note",
|
||||
"list": "List stickies in workspace",
|
||||
"retrieve": "Get single sticky by ID",
|
||||
"update": "Update sticky details",
|
||||
"delete": "Delete a sticky",
|
||||
},
|
||||
"customers": {
|
||||
"create": "Create a new customer",
|
||||
"list": "List customers in workspace",
|
||||
"retrieve": "Get single customer by ID",
|
||||
"update": "Update customer details",
|
||||
"delete": "Delete a customer",
|
||||
},
|
||||
"members": {
|
||||
"get_workspace_members": "List all workspace members",
|
||||
"get_project_members": "List all project members",
|
||||
@@ -173,6 +224,10 @@ API_METHODS: Dict[str, Dict[str, str]] = {
|
||||
"update": "Update time entry",
|
||||
"delete": "Delete time entry",
|
||||
},
|
||||
"workspaces": {
|
||||
"get_features": "Get enabled workspace features (initiatives, teams, customers, etc.)",
|
||||
"update_features": "Enable or disable workspace features",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -201,18 +256,19 @@ METHOD_NAME_MAP: Dict[str, Dict[str, str]] = {
|
||||
"create_relation": "create_work_item_relation",
|
||||
"list": "list_work_items",
|
||||
"retrieve": "retrieve_work_item",
|
||||
"search": "search_work_items",
|
||||
"get_workspace": "get_workspace_work_item",
|
||||
"delete": "delete_work_item",
|
||||
"search": "search_work_items",
|
||||
"create_epic": "create_work_item", # Epic is a workitem with specific type_id
|
||||
"update_epic": "update_work_item", # Epic is a workitem with specific type_id
|
||||
},
|
||||
"projects": {
|
||||
"create": "create_project",
|
||||
"list": "list_projects",
|
||||
"retrieve": "retrieve_project",
|
||||
"update": "update_project",
|
||||
"archive": "archive_project",
|
||||
"unarchive": "unarchive_project",
|
||||
"delete": "delete_project",
|
||||
"get_features": "get_project_features",
|
||||
"update_features": "update_project_features",
|
||||
},
|
||||
"cycles": {
|
||||
"create": "create_cycle",
|
||||
@@ -224,7 +280,6 @@ METHOD_NAME_MAP: Dict[str, Dict[str, str]] = {
|
||||
"list_archived": "list_archived_cycles",
|
||||
"add_work_items": "add_cycle_work_items",
|
||||
"list_work_items": "list_cycle_work_items",
|
||||
"retrieve_work_item": "retrieve_cycle_work_item",
|
||||
"remove_work_item": "remove_cycle_work_item",
|
||||
"transfer_work_items": "transfer_cycle_work_items",
|
||||
"delete": "delete_cycle",
|
||||
@@ -232,7 +287,7 @@ METHOD_NAME_MAP: Dict[str, Dict[str, str]] = {
|
||||
"labels": {
|
||||
"create": "create_label",
|
||||
"list": "list_labels",
|
||||
"retrieve": "get_labels",
|
||||
"retrieve": "retrieve_label",
|
||||
"update": "update_label",
|
||||
"delete": "delete_label",
|
||||
},
|
||||
@@ -259,6 +314,8 @@ METHOD_NAME_MAP: Dict[str, Dict[str, str]] = {
|
||||
"pages": {
|
||||
"create_project_page": "create_project_page",
|
||||
"create_workspace_page": "create_workspace_page",
|
||||
"retrieve_project": "retrieve_project_page",
|
||||
"retrieve_workspace": "retrieve_workspace_page",
|
||||
},
|
||||
"assets": {
|
||||
"create": "create_generic_asset_upload",
|
||||
@@ -279,6 +336,53 @@ METHOD_NAME_MAP: Dict[str, Dict[str, str]] = {
|
||||
"update": "update_intake_work_item",
|
||||
"delete": "delete_intake_work_item",
|
||||
},
|
||||
"initiatives": {
|
||||
"create": "create_initiative",
|
||||
"list": "list_initiatives",
|
||||
"retrieve": "retrieve_initiative",
|
||||
"update": "update_initiative",
|
||||
"delete": "delete_initiative",
|
||||
"create_label": "create_initiative_label",
|
||||
"list_labels": "list_initiative_labels",
|
||||
"retrieve_label": "retrieve_initiative_label",
|
||||
"update_label": "update_initiative_label",
|
||||
"delete_label": "delete_initiative_label",
|
||||
"add_labels": "add_initiative_labels",
|
||||
"remove_labels": "remove_initiative_labels",
|
||||
"add_projects": "add_initiative_projects",
|
||||
"list_projects": "list_initiative_projects",
|
||||
"remove_projects": "remove_initiative_projects",
|
||||
"add_epics": "add_initiative_epics",
|
||||
"list_epics": "list_initiative_epics",
|
||||
"remove_epics": "remove_initiative_epics",
|
||||
},
|
||||
"teamspaces": {
|
||||
"create": "create_teamspace",
|
||||
"list": "list_teamspaces",
|
||||
"retrieve": "retrieve_teamspace",
|
||||
"update": "update_teamspace",
|
||||
"delete": "delete_teamspace",
|
||||
"add_members": "add_teamspace_members",
|
||||
"list_members": "list_teamspace_members",
|
||||
"remove_members": "remove_teamspace_members",
|
||||
"add_projects": "add_teamspace_projects",
|
||||
"list_projects": "list_teamspace_projects",
|
||||
"remove_projects": "remove_teamspace_projects",
|
||||
},
|
||||
"stickies": {
|
||||
"create": "create_sticky",
|
||||
"list": "list_stickies",
|
||||
"retrieve": "retrieve_sticky",
|
||||
"update": "update_sticky",
|
||||
"delete": "delete_sticky",
|
||||
},
|
||||
"customers": {
|
||||
"create": "create_customer",
|
||||
"list": "list_customers",
|
||||
"retrieve": "retrieve_customer",
|
||||
"update": "update_customer",
|
||||
"delete": "delete_customer",
|
||||
},
|
||||
"members": {
|
||||
"get_workspace_members": "get_workspace_members",
|
||||
"get_project_members": "get_project_members",
|
||||
@@ -291,6 +395,7 @@ METHOD_NAME_MAP: Dict[str, Dict[str, str]] = {
|
||||
"create": "create_work_item_attachment",
|
||||
"list": "list_work_item_attachments",
|
||||
"retrieve": "retrieve_work_item_attachment",
|
||||
"update": "update_work_item_attachment",
|
||||
"delete": "delete_work_item_attachment",
|
||||
},
|
||||
"comments": {
|
||||
@@ -335,6 +440,10 @@ METHOD_NAME_MAP: Dict[str, Dict[str, str]] = {
|
||||
"update": "update_issue_worklog",
|
||||
"delete": "delete_issue_worklog",
|
||||
},
|
||||
"workspaces": {
|
||||
"get_features": "get_workspace_features",
|
||||
"update_features": "update_workspace_features",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -371,3 +480,80 @@ def resolve_actual_method_name(category: str, method: str) -> Optional[str]:
|
||||
|
||||
# If not a simplified name, assume it could already be actual; allow pass-through
|
||||
return method
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CENTRALIZED TOOL DEFINITIONS (Phase 1: Foundation)
|
||||
# ============================================================================
|
||||
# This is the single source of truth for all tool metadata.
|
||||
# Auto-generation in progress - currently only labels category defined.
|
||||
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
|
||||
# Tool definitions - now imported from individual tool modules for modularity
|
||||
# Each tool file defines its own metadata, special handlers, and business logic
|
||||
from pi.services.actions.tools.activity import ACTIVITY_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.assets import ASSET_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.comments import COMMENTS_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.customers import CUSTOMER_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.cycles import CYCLES_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.initiatives import INITIATIVE_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.intake import INTAKE_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.labels import LABEL_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.links import LINKS_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.members import MEMBER_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.modules import MODULES_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.pages import PAGE_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.projects import PROJECT_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.properties import PROPERTIES_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.states import STATE_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.stickies import STICKY_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.teamspaces import TEAMSPACE_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.types import TYPE_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.users import USER_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.workitems import WORKITEMS_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.worklogs import WORKLOGS_TOOL_DEFINITIONS
|
||||
from pi.services.actions.tools.workspaces import WORKSPACE_TOOL_DEFINITIONS
|
||||
|
||||
TOOL_DEFINITIONS: Dict[str, Dict[str, ToolMetadata]] = {
|
||||
"activity": ACTIVITY_TOOL_DEFINITIONS,
|
||||
"assets": ASSET_TOOL_DEFINITIONS,
|
||||
"comments": COMMENTS_TOOL_DEFINITIONS,
|
||||
"customers": CUSTOMER_TOOL_DEFINITIONS,
|
||||
"cycles": CYCLES_TOOL_DEFINITIONS,
|
||||
"initiatives": INITIATIVE_TOOL_DEFINITIONS,
|
||||
"intake": INTAKE_TOOL_DEFINITIONS,
|
||||
"labels": LABEL_TOOL_DEFINITIONS,
|
||||
"links": LINKS_TOOL_DEFINITIONS,
|
||||
"members": MEMBER_TOOL_DEFINITIONS,
|
||||
"modules": MODULES_TOOL_DEFINITIONS,
|
||||
"pages": PAGE_TOOL_DEFINITIONS,
|
||||
"projects": PROJECT_TOOL_DEFINITIONS,
|
||||
"properties": PROPERTIES_TOOL_DEFINITIONS,
|
||||
"states": STATE_TOOL_DEFINITIONS,
|
||||
"stickies": STICKY_TOOL_DEFINITIONS,
|
||||
"teamspaces": TEAMSPACE_TOOL_DEFINITIONS,
|
||||
"types": TYPE_TOOL_DEFINITIONS,
|
||||
"users": USER_TOOL_DEFINITIONS,
|
||||
"workitems": WORKITEMS_TOOL_DEFINITIONS,
|
||||
"worklogs": WORKLOGS_TOOL_DEFINITIONS,
|
||||
"workspaces": WORKSPACE_TOOL_DEFINITIONS,
|
||||
}
|
||||
|
||||
|
||||
def get_tool_metadata(category: str, method: str) -> Optional[ToolMetadata]:
|
||||
"""Get tool metadata for a specific category and method.
|
||||
|
||||
Args:
|
||||
category: API category (e.g., "labels", "workitems")
|
||||
method: Simplified method name (e.g., "create", "list")
|
||||
|
||||
Returns:
|
||||
ToolMetadata if found, None otherwise
|
||||
"""
|
||||
return TOOL_DEFINITIONS.get(category, {}).get(method)
|
||||
|
||||
|
||||
def get_all_tool_metadata() -> Dict[str, Dict[str, ToolMetadata]]:
|
||||
"""Get all tool metadata across all categories."""
|
||||
return TOOL_DEFINITIONS.copy()
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
# SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
|
||||
# SPDX-License-Identifier: LicenseRef-Plane-Commercial
|
||||
#
|
||||
# Licensed under the Plane Commercial License (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# https://plane.so/legals/eula
|
||||
#
|
||||
# DO NOT remove or modify this notice.
|
||||
# NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
|
||||
"""
|
||||
Tool generator for auto-generating LangChain tools from centralized metadata.
|
||||
|
||||
This module provides functionality to dynamically create LangChain tools
|
||||
from ToolMetadata definitions, eliminating manual tool duplication.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from pi import logger
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tools.base import PlaneToolBase
|
||||
|
||||
log = logger.getChild(__name__)
|
||||
|
||||
|
||||
# Complete type mapping - no string parsing needed
|
||||
from typing import Optional
|
||||
|
||||
_TYPE_MAP = {
|
||||
# Basic types
|
||||
"str": str,
|
||||
"int": int,
|
||||
"float": float,
|
||||
"bool": bool,
|
||||
"dict": dict,
|
||||
"list": list,
|
||||
# Optional types
|
||||
"Optional[str]": Optional[str],
|
||||
"Optional[int]": Optional[int],
|
||||
"Optional[float]": Optional[float],
|
||||
"Optional[bool]": Optional[bool],
|
||||
"Optional[dict]": Optional[dict],
|
||||
"Optional[list]": Optional[list],
|
||||
# Container types
|
||||
"List[str]": list,
|
||||
"List[int]": list,
|
||||
"List[dict]": list,
|
||||
"Dict[str, Any]": dict,
|
||||
"Dict[str, str]": dict,
|
||||
}
|
||||
|
||||
|
||||
def _parse_type_annotation(type_str: str) -> Any:
|
||||
"""Parse type annotation string into actual Python type.
|
||||
|
||||
Args:
|
||||
type_str: Type as string (e.g., "str", "Optional[str]", "List[str]")
|
||||
|
||||
Returns:
|
||||
Python type object or typing generic
|
||||
"""
|
||||
return _TYPE_MAP.get(type_str, str)
|
||||
|
||||
|
||||
def _build_function_signature(metadata: ToolMetadata) -> Dict[str, inspect.Parameter]:
|
||||
"""Build function signature from metadata parameters.
|
||||
|
||||
Args:
|
||||
metadata: Tool metadata
|
||||
|
||||
Returns:
|
||||
Dictionary of parameter name to inspect.Parameter objects
|
||||
"""
|
||||
sig_params = {}
|
||||
|
||||
for param in metadata.parameters:
|
||||
# Determine default value
|
||||
if param.required:
|
||||
default = inspect.Parameter.empty
|
||||
else:
|
||||
default = param.default # type: ignore[assignment]
|
||||
|
||||
# Create parameter
|
||||
sig_params[param.name] = inspect.Parameter(
|
||||
name=param.name,
|
||||
kind=inspect.Parameter.KEYWORD_ONLY,
|
||||
default=default,
|
||||
annotation=_parse_type_annotation(param.type),
|
||||
)
|
||||
|
||||
return sig_params
|
||||
|
||||
|
||||
def _build_type_annotations(metadata: ToolMetadata) -> Dict[str, Any]:
|
||||
"""Build type annotations dictionary for function.
|
||||
|
||||
Args:
|
||||
metadata: Tool metadata
|
||||
|
||||
Returns:
|
||||
Dictionary of parameter names to types
|
||||
"""
|
||||
annotations = {}
|
||||
|
||||
for param in metadata.parameters:
|
||||
annotations[param.name] = _parse_type_annotation(param.type)
|
||||
|
||||
# Add return type
|
||||
annotations["return"] = Dict[str, Any]
|
||||
|
||||
return annotations
|
||||
|
||||
|
||||
def _build_docstring(metadata: ToolMetadata) -> str:
|
||||
"""Generate Google-style docstring from metadata.
|
||||
|
||||
Args:
|
||||
metadata: Tool metadata
|
||||
|
||||
Returns:
|
||||
Formatted docstring
|
||||
"""
|
||||
lines = [metadata.description, ""]
|
||||
|
||||
if metadata.parameters:
|
||||
lines.append("Args:")
|
||||
for param in metadata.parameters:
|
||||
# Format: param_name: Description
|
||||
required_marker = "(required)" if param.required else ""
|
||||
lines.append(f" {param.name}: {param.description} {required_marker}".strip())
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def _apply_pre_processing(
|
||||
metadata: ToolMetadata,
|
||||
kwargs: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
category: str,
|
||||
method_key: str,
|
||||
method_executor: Any = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Apply pre-processing via custom handler if provided.
|
||||
|
||||
Args:
|
||||
metadata: Tool metadata
|
||||
kwargs: Tool arguments
|
||||
context: Execution context
|
||||
category: API category
|
||||
method_key: Method key
|
||||
method_executor: Method executor instance (for making additional API calls)
|
||||
|
||||
Returns:
|
||||
Modified kwargs
|
||||
"""
|
||||
if metadata.pre_handler:
|
||||
return await metadata.pre_handler(metadata, kwargs, context, category, method_key, method_executor)
|
||||
return kwargs
|
||||
|
||||
|
||||
async def _apply_post_processing(
|
||||
metadata: ToolMetadata,
|
||||
result: Dict[str, Any],
|
||||
kwargs: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
method_executor: Any,
|
||||
category: str,
|
||||
method_key: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Apply post-processing via custom handler if provided.
|
||||
|
||||
Args:
|
||||
metadata: Tool metadata
|
||||
result: Execution result
|
||||
kwargs: Original tool arguments
|
||||
context: Execution context
|
||||
method_executor: Method executor instance
|
||||
category: API category
|
||||
method_key: Method key
|
||||
|
||||
Returns:
|
||||
Modified result
|
||||
"""
|
||||
if metadata.post_handler:
|
||||
return await metadata.post_handler(metadata, result, kwargs, context, method_executor, category, method_key)
|
||||
return result
|
||||
|
||||
|
||||
def generate_tool_from_metadata(
|
||||
category: str,
|
||||
method_key: str,
|
||||
metadata: ToolMetadata,
|
||||
method_executor: Any,
|
||||
context: Dict[str, Any],
|
||||
) -> Callable:
|
||||
"""Generate a LangChain tool function from metadata.
|
||||
|
||||
This function creates a fully functional LangChain tool that:
|
||||
- Has the correct signature and type annotations
|
||||
- Auto-fills context parameters (workspace_slug, project_id)
|
||||
- Calls method_executor.execute() with the right category/method
|
||||
- Formats responses using PlaneToolBase helpers
|
||||
|
||||
Args:
|
||||
category: API category (e.g., "labels")
|
||||
method_key: Simplified method name (e.g., "create")
|
||||
metadata: Complete tool metadata
|
||||
method_executor: MethodExecutor instance for calling SDK adapter
|
||||
context: Execution context with workspace_slug, project_id, etc.
|
||||
|
||||
Returns:
|
||||
Decorated LangChain tool function
|
||||
"""
|
||||
|
||||
# Create async function dynamically
|
||||
async def tool_func(**kwargs):
|
||||
"""Dynamically generated tool function."""
|
||||
|
||||
# Auto-fill context values for parameters marked as auto_fill_from_context
|
||||
for param in metadata.parameters:
|
||||
if param.auto_fill_from_context and param.name in context:
|
||||
if param.name not in kwargs or kwargs[param.name] is None:
|
||||
kwargs[param.name] = context[param.name]
|
||||
log.debug(f"Auto-filled {param.name} = {context[param.name]} for {metadata.name}")
|
||||
|
||||
# PRE-PROCESSING: Apply custom handler if provided
|
||||
if metadata.pre_handler:
|
||||
try:
|
||||
kwargs = await _apply_pre_processing(metadata, kwargs, context, category, method_key, method_executor)
|
||||
except ValueError as e:
|
||||
# Pre-processing validation failed
|
||||
return PlaneToolBase.format_error_payload(str(e), "")
|
||||
|
||||
# Execute via method_executor
|
||||
result = await method_executor.execute(category, method_key, **kwargs)
|
||||
|
||||
# POST-PROCESSING: Apply custom handler if provided
|
||||
if metadata.post_handler:
|
||||
result = await _apply_post_processing(metadata, result, kwargs, context, method_executor, category, method_key)
|
||||
|
||||
# Format response based on whether it returns an entity
|
||||
if result["success"]:
|
||||
if metadata.returns_entity_type:
|
||||
# Format with entity URL
|
||||
return await PlaneToolBase.format_success_payload_with_url(
|
||||
f"Successfully executed {metadata.name}", result.get("data"), metadata.returns_entity_type, context
|
||||
)
|
||||
else:
|
||||
# Simple success payload
|
||||
return PlaneToolBase.format_success_payload("Action successfully executed", result["data"])
|
||||
else:
|
||||
# Error payload
|
||||
return PlaneToolBase.format_error_payload(f"Failed to execute {metadata.name}", result["error"])
|
||||
|
||||
# Set function metadata
|
||||
tool_func.__name__ = metadata.name
|
||||
tool_func.__doc__ = _build_docstring(metadata)
|
||||
tool_func.__annotations__ = _build_type_annotations(metadata)
|
||||
|
||||
# Build signature (for inspect.signature compatibility)
|
||||
sig_params = _build_function_signature(metadata)
|
||||
tool_func.__signature__ = inspect.Signature(parameters=list(sig_params.values())) # type: ignore[attr-defined]
|
||||
|
||||
# Apply @tool decorator
|
||||
decorated_tool = tool(tool_func)
|
||||
|
||||
log.info(f"Generated tool: {metadata.name} (category={category}, method={method_key})")
|
||||
|
||||
return decorated_tool # type: ignore[return-value]
|
||||
|
||||
|
||||
def generate_tools_for_category(
|
||||
category: str,
|
||||
method_executor: Any,
|
||||
context: Dict[str, Any],
|
||||
tool_definitions: Dict[str, ToolMetadata],
|
||||
) -> List[Callable]:
|
||||
"""Generate all tools for a category from metadata.
|
||||
|
||||
Args:
|
||||
category: API category (e.g., "labels")
|
||||
method_executor: MethodExecutor instance
|
||||
context: Execution context
|
||||
tool_definitions: Dictionary of method_key -> ToolMetadata for this category
|
||||
|
||||
Returns:
|
||||
List of generated LangChain tools
|
||||
"""
|
||||
tools = []
|
||||
|
||||
for method_key, metadata in tool_definitions.items():
|
||||
try:
|
||||
tool_func = generate_tool_from_metadata(
|
||||
category=category,
|
||||
method_key=method_key,
|
||||
metadata=metadata,
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
)
|
||||
tools.append(tool_func)
|
||||
except Exception as e:
|
||||
log.error(f"Error generating tool for {category}.{method_key}: {e}", exc_info=True)
|
||||
# Continue generating other tools
|
||||
|
||||
log.info(f"Generated {len(tools)} tools for category '{category}'")
|
||||
return tools
|
||||
@@ -0,0 +1,92 @@
|
||||
# SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
|
||||
# SPDX-License-Identifier: LicenseRef-Plane-Commercial
|
||||
#
|
||||
# Licensed under the Plane Commercial License (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# https://plane.so/legals/eula
|
||||
#
|
||||
# DO NOT remove or modify this notice.
|
||||
# NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
|
||||
"""
|
||||
Tool metadata models for centralized tool definition and auto-generation.
|
||||
|
||||
This module defines Pydantic models that serve as the single source of truth
|
||||
for all LangChain tool definitions. Tool metadata includes:
|
||||
- Tool name and description
|
||||
- All parameters with types, descriptions, and defaults
|
||||
- SDK adapter method mapping
|
||||
- Entity type for URL construction
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Literal
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ToolParameter(BaseModel):
|
||||
"""Metadata for a single tool parameter.
|
||||
|
||||
Attributes:
|
||||
name: Parameter name (e.g., "project_id", "color", "assignees")
|
||||
type: Python type annotation as string (e.g., "str", "Optional[str]", "List[str]")
|
||||
required: Whether parameter is required
|
||||
description: Human-readable parameter description for LLM
|
||||
default: Default value if parameter is optional
|
||||
auto_fill_from_context: Whether to auto-fill from context (workspace_slug, project_id)
|
||||
property_transform: How to transform for UI property mapper (Phase 2 feature)
|
||||
"""
|
||||
|
||||
name: str
|
||||
type: str
|
||||
required: bool = True
|
||||
description: str
|
||||
default: Optional[Any] = None
|
||||
auto_fill_from_context: bool = False
|
||||
property_transform: Literal["name", "id", "id_list", "bool", "date", "json", "int", "skip"] = "name"
|
||||
|
||||
class Config:
|
||||
# Allow extra fields for future extensions
|
||||
extra = "allow"
|
||||
|
||||
|
||||
class ToolMetadata(BaseModel):
|
||||
"""Metadata for a single tool definition.
|
||||
|
||||
This serves as the single source of truth for tool configuration,
|
||||
used to auto-generate LangChain tools and UI property mappings.
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
sdk_method: str
|
||||
parameters: List[ToolParameter] = []
|
||||
returns_entity_type: Optional[str] = None
|
||||
pre_handler: Optional[Any] = None # Custom async function(metadata, kwargs, context, category, method_key) -> kwargs
|
||||
post_handler: Optional[Any] = None # Custom async function(metadata, result, kwargs, context, method_executor, category, method_key) -> result
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True # Allow function types
|
||||
|
||||
def get_parameter(self, name: str) -> Optional[ToolParameter]:
|
||||
"""Get parameter by name."""
|
||||
for param in self.parameters:
|
||||
if param.name == name:
|
||||
return param
|
||||
return None
|
||||
|
||||
def get_required_parameters(self) -> List[ToolParameter]:
|
||||
"""Get all required parameters."""
|
||||
return [p for p in self.parameters if p.required]
|
||||
|
||||
def get_optional_parameters(self) -> List[ToolParameter]:
|
||||
"""Get all optional parameters."""
|
||||
return [p for p in self.parameters if not p.required]
|
||||
|
||||
def get_auto_fill_parameters(self) -> List[ToolParameter]:
|
||||
"""Get parameters that should be auto-filled from context."""
|
||||
return [p for p in self.parameters if p.auto_fill_from_context]
|
||||
@@ -29,7 +29,9 @@ from .activity import get_activity_tools
|
||||
from .assets import get_asset_tools
|
||||
from .attachments import get_attachment_tools
|
||||
from .comments import get_comment_tools
|
||||
from .customers import get_customer_tools
|
||||
from .cycles import get_cycle_tools
|
||||
from .initiatives import get_initiative_tools
|
||||
from .intake import get_intake_tools
|
||||
from .labels import get_label_tools
|
||||
from .links import get_link_tools
|
||||
@@ -39,10 +41,13 @@ from .pages import get_page_tools
|
||||
from .projects import get_project_tools
|
||||
from .properties import get_property_tools
|
||||
from .states import get_state_tools
|
||||
from .stickies import get_sticky_tools
|
||||
from .teamspaces import get_teamspace_tools
|
||||
from .types import get_type_tools
|
||||
from .users import get_user_tools
|
||||
from .workitems import get_workitem_tools
|
||||
from .worklogs import get_worklog_tools
|
||||
from .workspaces import get_workspace_tools
|
||||
|
||||
CATEGORY_TO_PROVIDER: Dict[str, Callable] = {
|
||||
"activity": get_activity_tools,
|
||||
@@ -53,10 +58,14 @@ CATEGORY_TO_PROVIDER: Dict[str, Callable] = {
|
||||
"attachment": get_attachment_tools,
|
||||
"comments": get_comment_tools,
|
||||
"comment": get_comment_tools,
|
||||
"customers": get_customer_tools,
|
||||
"customer": get_customer_tools,
|
||||
"cycles": get_cycle_tools,
|
||||
"cycle": get_cycle_tools,
|
||||
"intake": get_intake_tools,
|
||||
"intakes": get_intake_tools,
|
||||
"initiatives": get_initiative_tools,
|
||||
"initiative": get_initiative_tools,
|
||||
"labels": get_label_tools,
|
||||
"label": get_label_tools,
|
||||
"links": get_link_tools,
|
||||
@@ -73,6 +82,10 @@ CATEGORY_TO_PROVIDER: Dict[str, Callable] = {
|
||||
"property": get_property_tools,
|
||||
"states": get_state_tools,
|
||||
"state": get_state_tools,
|
||||
"stickies": get_sticky_tools,
|
||||
"sticky": get_sticky_tools,
|
||||
"teamspaces": get_teamspace_tools,
|
||||
"teamspace": get_teamspace_tools,
|
||||
"types": get_type_tools,
|
||||
"type": get_type_tools,
|
||||
"users": get_user_tools,
|
||||
@@ -81,6 +94,8 @@ CATEGORY_TO_PROVIDER: Dict[str, Callable] = {
|
||||
"workitem": get_workitem_tools,
|
||||
"worklogs": get_worklog_tools,
|
||||
"worklog": get_worklog_tools,
|
||||
"workspaces": get_workspace_tools,
|
||||
"workspace": get_workspace_tools,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,62 +15,132 @@ Activity API tools for Plane activity tracking operations.
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi import logger
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from .base import PlaneToolBase
|
||||
# ============================================================================
|
||||
# ACTIVITY-SPECIFIC HANDLER FUNCTIONS
|
||||
# ============================================================================
|
||||
log = logger.getChild("activity")
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing activity actions
|
||||
|
||||
async def _activity_pre_handler(
|
||||
metadata: ToolMetadata,
|
||||
kwargs: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
category: str,
|
||||
method_key: str,
|
||||
method_executor: Any = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Pre-processing handler for activity tools.
|
||||
|
||||
Handles:
|
||||
- Auto-detect project_id from issue_id if missing
|
||||
"""
|
||||
# If project_id is missing but we have issue_id, look it up in the database
|
||||
if kwargs.get("issue_id") and not kwargs.get("project_id"):
|
||||
issue_id = kwargs["issue_id"]
|
||||
log.debug(f"project_id missing for activity_{method_key}, looking up issue_id={issue_id}")
|
||||
|
||||
try:
|
||||
from pi.app.api.v1.helpers.plane_sql_queries import get_issue_identifier_for_artifact
|
||||
|
||||
work_item = await get_issue_identifier_for_artifact(issue_id)
|
||||
if work_item and work_item.get("project_id"):
|
||||
kwargs["project_id"] = work_item["project_id"]
|
||||
log.debug(f"Auto-filled project_id={kwargs["project_id"]} from work item lookup")
|
||||
else:
|
||||
log.warning(f"Could not find project_id for issue_id={issue_id}")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to lookup work item for project_id: {e}")
|
||||
# Don't fail the entire operation, let the SDK call fail with proper error
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ACTIVITY TOOL DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
ACTIVITY_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"list": ToolMetadata(
|
||||
name="activity_list",
|
||||
description="List activity for a specific work item",
|
||||
sdk_method="list_work_item_activities",
|
||||
pre_handler=_activity_pre_handler,
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="issue_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Work item ID (required) - activities are specific to individual work items",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="activity_retrieve",
|
||||
description="Get a single activity by ID",
|
||||
sdk_method="retrieve_work_item_activity",
|
||||
pre_handler=_activity_pre_handler,
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="activity_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Activity ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="issue_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Work item ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_activity_tools(method_executor, context):
|
||||
"""Return LangChain tools for the activity category using method_executor and context."""
|
||||
|
||||
@tool
|
||||
async def activity_list(project_id: Optional[str] = None, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""List activity for a project.
|
||||
|
||||
Args:
|
||||
project_id: Parameter description (optional)
|
||||
workspace_slug: Parameter description (optional)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute("activity", "list", project_id=project_id, workspace_slug=workspace_slug)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved activity list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list activity", result["error"])
|
||||
|
||||
@tool
|
||||
async def activity_retrieve(
|
||||
activity_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get a single activity by ID."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"activity",
|
||||
"retrieve",
|
||||
activity_id=activity_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved activity", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to retrieve activity", result["error"])
|
||||
|
||||
return [activity_list, activity_retrieve]
|
||||
"""Return LangChain tools for the activity category using auto-generation from metadata."""
|
||||
return generate_tools_for_category(
|
||||
category="activity",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions=ACTIVITY_TOOL_DEFINITIONS,
|
||||
)
|
||||
|
||||
@@ -13,128 +13,160 @@
|
||||
Assets API tools for Plane asset management operations.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from .base import PlaneToolBase
|
||||
# ============================================================================
|
||||
# ASSETS TOOL DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing asset actions
|
||||
ASSET_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"create": ToolMetadata(
|
||||
name="assets_create",
|
||||
description="Create a new generic asset",
|
||||
sdk_method="create_generic_asset_upload",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"create_user_upload": ToolMetadata(
|
||||
name="assets_create_user_upload",
|
||||
description="Upload user-specific assets",
|
||||
sdk_method="create_user_asset_upload",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"get_generic": ToolMetadata(
|
||||
name="assets_get_generic",
|
||||
description="Retrieve generic assets",
|
||||
sdk_method="get_generic_asset",
|
||||
parameters=[
|
||||
ToolParameter(name="asset_id", type="str", required=True, description="Asset ID (required)"),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"update_generic": ToolMetadata(
|
||||
name="assets_update_generic",
|
||||
description="Update generic assets",
|
||||
sdk_method="update_generic_asset",
|
||||
parameters=[
|
||||
ToolParameter(name="asset_id", type="str", required=True, description="Asset ID (required)"),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"update_user": ToolMetadata(
|
||||
name="assets_update_user",
|
||||
description="Update user assets",
|
||||
sdk_method="update_user_asset",
|
||||
parameters=[
|
||||
ToolParameter(name="asset_id", type="str", required=True, description="Asset ID (required)"),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"delete_user": ToolMetadata(
|
||||
name="assets_delete_user",
|
||||
description="Delete user assets",
|
||||
sdk_method="delete_user_asset",
|
||||
parameters=[
|
||||
ToolParameter(name="asset_id", type="str", required=True, description="Asset ID (required)"),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_asset_tools(method_executor, context):
|
||||
"""Return LangChain tools for the assets category using method_executor and context."""
|
||||
|
||||
@tool
|
||||
async def assets_create(project_id: Optional[str] = None, workspace_slug: Optional[str] = None, **kwargs) -> Dict[str, Any]:
|
||||
"""Create a new generic asset.
|
||||
|
||||
Args:
|
||||
project_id: Parameter description (optional)
|
||||
workspace_slug: Parameter description (optional)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute("assets", "create", project_id=project_id, workspace_slug=workspace_slug, **kwargs)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully created asset", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create asset", result["error"])
|
||||
|
||||
@tool
|
||||
async def assets_create_user_upload(project_id: Optional[str] = None, workspace_slug: Optional[str] = None, **kwargs) -> Dict[str, Any]:
|
||||
"""Upload user-specific assets."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute("assets", "create_user_upload", project_id=project_id, workspace_slug=workspace_slug, **kwargs)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully created user asset upload", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create user asset upload", result["error"])
|
||||
|
||||
@tool
|
||||
async def assets_get_generic(
|
||||
asset_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Retrieve generic assets."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute("assets", "get_generic", asset_id=asset_id, project_id=project_id, workspace_slug=workspace_slug)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved generic asset", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to get generic asset", result["error"])
|
||||
|
||||
@tool
|
||||
async def assets_update_generic(
|
||||
asset_id: str, project_id: Optional[str] = None, workspace_slug: Optional[str] = None, **kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""Update generic assets."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"assets", "update_generic", asset_id=asset_id, project_id=project_id, workspace_slug=workspace_slug, **kwargs
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully updated generic asset", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update generic asset", result["error"])
|
||||
|
||||
@tool
|
||||
async def assets_update_user(asset_id: str, project_id: Optional[str] = None, workspace_slug: Optional[str] = None, **kwargs) -> Dict[str, Any]:
|
||||
"""Update user assets."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"assets", "update_user", asset_id=asset_id, project_id=project_id, workspace_slug=workspace_slug, **kwargs
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully updated user asset", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update user asset", result["error"])
|
||||
|
||||
@tool
|
||||
async def assets_delete_user(
|
||||
asset_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete user assets."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute("assets", "delete_user", asset_id=asset_id, project_id=project_id, workspace_slug=workspace_slug)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully deleted user asset", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to delete user asset", result["error"])
|
||||
|
||||
return [assets_create, assets_create_user_upload, assets_get_generic, assets_update_generic, assets_update_user, assets_delete_user]
|
||||
"""Return LangChain tools for the assets category using auto-generation from metadata."""
|
||||
return generate_tools_for_category(
|
||||
category="assets",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions=ASSET_TOOL_DEFINITIONS,
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ class PlaneToolBase:
|
||||
"""
|
||||
|
||||
# Lazy import to avoid circular dependency
|
||||
from pi.agents.sql_agent.tools import extract_entity_from_api_response
|
||||
from pi.agents.sql_agent.helpers import extract_entity_from_api_response
|
||||
|
||||
# Extract entity data from response
|
||||
entity_data = extract_entity_from_api_response(data, entity_type)
|
||||
@@ -107,7 +107,7 @@ class PlaneToolBase:
|
||||
# Construct entity URL
|
||||
try:
|
||||
# Lazy import to avoid circular dependency
|
||||
from pi.agents.sql_agent.tools import construct_action_entity_url
|
||||
from pi.agents.sql_agent.helpers import construct_action_entity_url
|
||||
|
||||
# Since this method is now async, we can directly await the async function
|
||||
url_info = await construct_action_entity_url(entity_data, entity_type, workspace_slug, frontend_url)
|
||||
@@ -164,7 +164,7 @@ class PlaneToolBase:
|
||||
Structured success payload that also includes constructed entity URL information when available.
|
||||
"""
|
||||
# Lazy import to avoid circular dependency
|
||||
from pi.agents.sql_agent.tools import extract_entity_from_api_response
|
||||
from pi.agents.sql_agent.helpers import extract_entity_from_api_response
|
||||
from pi.config import settings as _settings
|
||||
|
||||
entity: Dict[str, Any] = {}
|
||||
@@ -177,7 +177,7 @@ class PlaneToolBase:
|
||||
entity_data["workspace"] = str(context["workspace_id"])
|
||||
if workspace_slug:
|
||||
try:
|
||||
from pi.agents.sql_agent.tools import construct_action_entity_url
|
||||
from pi.agents.sql_agent.helpers import construct_action_entity_url
|
||||
|
||||
url_info = await construct_action_entity_url(entity_data, entity_type, workspace_slug, frontend_url)
|
||||
if url_info:
|
||||
|
||||
@@ -16,291 +16,159 @@ Comments API tools for Plane issue comments operations.
|
||||
import uuid
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from pi.config import settings
|
||||
|
||||
from .base import PlaneToolBase
|
||||
# Pre-handler to resolve project_id from issue_id if missing
|
||||
async def _resolve_project_pre_handler(
|
||||
metadata: ToolMetadata,
|
||||
kwargs: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
category: str,
|
||||
method_key: str,
|
||||
method_executor: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""Resolve project_id from issue_id if it is missing or invalid."""
|
||||
issue_id = kwargs.get("issue_id")
|
||||
project_id = kwargs.get("project_id")
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing comment actions
|
||||
should_resolve = False
|
||||
if issue_id and not project_id:
|
||||
should_resolve = True
|
||||
elif issue_id and project_id:
|
||||
try:
|
||||
uuid.UUID(str(project_id))
|
||||
except (ValueError, AttributeError):
|
||||
should_resolve = True
|
||||
|
||||
if should_resolve:
|
||||
try:
|
||||
from pi.app.api.v1.helpers.plane_sql_queries import get_issue_identifier_for_artifact
|
||||
|
||||
issue_info = await get_issue_identifier_for_artifact(str(issue_id))
|
||||
if issue_info and issue_info.get("project_id"):
|
||||
kwargs["project_id"] = issue_info["project_id"]
|
||||
except Exception:
|
||||
# Best effort, proceed without resolution if it fails
|
||||
pass
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
COMMENTS_TOOL_DEFINITIONS = {
|
||||
"create": ToolMetadata(
|
||||
name="comments_create",
|
||||
description="Create a comment on an issue.",
|
||||
sdk_method="create_work_item_comment",
|
||||
parameters=[
|
||||
ToolParameter(name="issue_id", description="Issue ID", required=True, type="str"),
|
||||
ToolParameter(name="comment_html", description="Comment content in HTML format", required=True, type="str"),
|
||||
ToolParameter(
|
||||
name="project_id", description="Project ID (optional, auto-filled)", required=False, type="Optional[str]", auto_fill_from_context=True
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
description="Workspace slug (optional, auto-filled)",
|
||||
required=False,
|
||||
type="Optional[str]",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(name="external_source", description="External source identifier (e.g., 'jira')", required=False, type="Optional[str]"),
|
||||
ToolParameter(name="external_id", description="External system ID", required=False, type="Optional[str]"),
|
||||
],
|
||||
pre_handler=_resolve_project_pre_handler,
|
||||
returns_entity_type="comment",
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="comments_list",
|
||||
description="List comments for an issue.",
|
||||
sdk_method="list_work_item_comments",
|
||||
parameters=[
|
||||
ToolParameter(name="issue_id", description="Issue ID", required=True, type="str"),
|
||||
ToolParameter(
|
||||
name="project_id", description="Project ID (optional, auto-filled)", required=False, type="Optional[str]", auto_fill_from_context=True
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
description="Workspace slug (optional, auto-filled)",
|
||||
required=False,
|
||||
type="Optional[str]",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
pre_handler=_resolve_project_pre_handler,
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="comments_retrieve",
|
||||
description="Get a single comment by ID.",
|
||||
sdk_method="retrieve_work_item_comment",
|
||||
parameters=[
|
||||
ToolParameter(name="comment_id", description="Comment ID", required=True, type="str"),
|
||||
ToolParameter(name="issue_id", description="Issue ID", required=True, type="str"),
|
||||
ToolParameter(
|
||||
name="project_id", description="Project ID (optional, auto-filled)", required=False, type="Optional[str]", auto_fill_from_context=True
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
description="Workspace slug (optional, auto-filled)",
|
||||
required=False,
|
||||
type="Optional[str]",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
pre_handler=_resolve_project_pre_handler,
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="comments_update",
|
||||
description="Update comment details.",
|
||||
sdk_method="update_work_item_comment",
|
||||
parameters=[
|
||||
ToolParameter(name="comment_id", description="Comment ID", required=True, type="str"),
|
||||
ToolParameter(name="issue_id", description="Issue ID", required=True, type="str"),
|
||||
ToolParameter(name="comment_html", description="Comment content in HTML format", required=False, type="Optional[str]"),
|
||||
ToolParameter(
|
||||
name="project_id", description="Project ID (optional, auto-filled)", required=False, type="Optional[str]", auto_fill_from_context=True
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
description="Workspace slug (optional, auto-filled)",
|
||||
required=False,
|
||||
type="Optional[str]",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(name="external_source", description="External source identifier", required=False, type="Optional[str]"),
|
||||
ToolParameter(name="external_id", description="External system ID", required=False, type="Optional[str]"),
|
||||
],
|
||||
pre_handler=_resolve_project_pre_handler,
|
||||
returns_entity_type="comment",
|
||||
),
|
||||
"delete": ToolMetadata(
|
||||
name="comments_delete",
|
||||
description="Delete a comment.",
|
||||
sdk_method="delete_work_item_comment",
|
||||
parameters=[
|
||||
ToolParameter(name="comment_id", description="Comment ID", required=True, type="str"),
|
||||
ToolParameter(name="issue_id", description="Issue ID", required=True, type="str"),
|
||||
ToolParameter(
|
||||
name="project_id", description="Project ID (optional, auto-filled)", required=False, type="Optional[str]", auto_fill_from_context=True
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
description="Workspace slug (optional, auto-filled)",
|
||||
required=False,
|
||||
type="Optional[str]",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
pre_handler=_resolve_project_pre_handler,
|
||||
returns_entity_type="comment",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_comment_tools(method_executor, context):
|
||||
"""Return LangChain tools for the comments category using method_executor and context."""
|
||||
|
||||
async def _build_issue_entity(issue_id: str, comment_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Build entity payload for comment actions.
|
||||
|
||||
- entity_url points to the parent issue (workitem) URL
|
||||
- entity_type is "comment"
|
||||
- entity_id is the comment_id when available (update/delete always, create best-effort)
|
||||
- entity_name is left null for now (TODO)
|
||||
"""
|
||||
try:
|
||||
workspace_slug = context.get("workspace_slug") if isinstance(context, dict) else None
|
||||
if not workspace_slug:
|
||||
return None
|
||||
from pi.agents.sql_agent.tools import construct_action_entity_url
|
||||
|
||||
url_info = await construct_action_entity_url({"id": str(issue_id)}, "workitem", workspace_slug, settings.plane_api.FRONTEND_URL)
|
||||
if not url_info or not isinstance(url_info, dict):
|
||||
return None
|
||||
# Normalize expected keys for execute-action consumers
|
||||
issue_identifier = url_info.get("issue_identifier")
|
||||
entity: Dict[str, Any] = {
|
||||
"entity_url": url_info.get("entity_url"),
|
||||
"entity_name": None,
|
||||
"entity_type": "comment",
|
||||
"entity_id": str(comment_id) if comment_id else None,
|
||||
}
|
||||
if issue_identifier:
|
||||
entity["issue_identifier"] = issue_identifier
|
||||
entity["entity_identifier"] = issue_identifier
|
||||
return entity
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@tool
|
||||
async def comments_create(
|
||||
issue_id: str,
|
||||
comment_html: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
external_source: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a comment on an issue.
|
||||
|
||||
Args:
|
||||
issue_id: Issue ID (required)
|
||||
comment_html: Comment content in HTML format (required)
|
||||
project_id: Project ID (optional, auto-filled from context or resolved from issue_id)
|
||||
workspace_slug: Workspace slug (optional, auto-filled from context)
|
||||
external_source: External source identifier (e.g., "jira")
|
||||
external_id: External system ID
|
||||
"""
|
||||
print(f"Context in comments_create: {context}")
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Note: issue_id and project_id validation/resolution is handled centrally
|
||||
# in action_execution_helpers.validate_and_resolve_ids() before tool execution
|
||||
|
||||
result = await method_executor.execute(
|
||||
"comments",
|
||||
"create",
|
||||
issue_id=issue_id,
|
||||
comment_html=comment_html,
|
||||
external_source=external_source,
|
||||
external_id=external_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
created_comment_id = None
|
||||
try:
|
||||
data = result.get("data") if isinstance(result, dict) else None
|
||||
if isinstance(data, dict) and data.get("id"):
|
||||
created_comment_id = str(data["id"])
|
||||
except Exception:
|
||||
created_comment_id = None
|
||||
entity = await _build_issue_entity(issue_id, comment_id=created_comment_id)
|
||||
return PlaneToolBase.format_success_payload("Successfully created comment", result["data"], entity=entity)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create comment", result["error"])
|
||||
|
||||
@tool
|
||||
async def comments_list(issue_id: str, project_id: Optional[str] = None, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""List comments for an issue."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute("comments", "list", issue_id=issue_id, project_id=project_id, workspace_slug=workspace_slug)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved comments list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list comments", result["error"])
|
||||
|
||||
@tool
|
||||
async def comments_retrieve(
|
||||
comment_id: str,
|
||||
issue_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get a single comment by ID.
|
||||
|
||||
Args:
|
||||
comment_id: Comment ID (required)
|
||||
issue_id: Issue ID (required)
|
||||
project_id: Project ID (optional, auto-filled from context or resolved from issue_id)
|
||||
workspace_slug: Workspace slug (optional, auto-filled from context)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Programmatically resolve project_id from issue_id if missing or invalid (non-UUID)
|
||||
should_resolve = False
|
||||
if issue_id and not project_id:
|
||||
should_resolve = True
|
||||
elif issue_id and project_id:
|
||||
try:
|
||||
uuid.UUID(str(project_id))
|
||||
except (ValueError, AttributeError):
|
||||
should_resolve = True
|
||||
|
||||
if should_resolve:
|
||||
try:
|
||||
from pi.app.api.v1.helpers.plane_sql_queries import get_issue_identifier_for_artifact
|
||||
|
||||
issue_info = await get_issue_identifier_for_artifact(str(issue_id))
|
||||
if issue_info and issue_info.get("project_id"):
|
||||
project_id = issue_info["project_id"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result = await method_executor.execute(
|
||||
"comments",
|
||||
"retrieve",
|
||||
comment_id=comment_id,
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved comment", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to retrieve comment", result["error"])
|
||||
|
||||
@tool
|
||||
async def comments_update(
|
||||
comment_id: str,
|
||||
issue_id: str,
|
||||
comment_html: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
external_source: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update comment details.
|
||||
|
||||
Args:
|
||||
comment_id: Comment ID (required)
|
||||
issue_id: Issue ID (required)
|
||||
comment_html: Comment content in HTML format
|
||||
project_id: Project ID (optional, auto-filled from context or resolved from issue_id)
|
||||
workspace_slug: Workspace slug (optional, auto-filled from context)
|
||||
external_source: External source identifier (e.g., "jira")
|
||||
external_id: External system ID
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Programmatically resolve project_id from issue_id if missing or invalid (non-UUID)
|
||||
should_resolve = False
|
||||
if issue_id and not project_id:
|
||||
should_resolve = True
|
||||
elif issue_id and project_id:
|
||||
try:
|
||||
uuid.UUID(str(project_id))
|
||||
except (ValueError, AttributeError):
|
||||
should_resolve = True
|
||||
|
||||
if should_resolve:
|
||||
try:
|
||||
from pi.app.api.v1.helpers.plane_sql_queries import get_issue_identifier_for_artifact
|
||||
|
||||
issue_info = await get_issue_identifier_for_artifact(str(issue_id))
|
||||
if issue_info and issue_info.get("project_id"):
|
||||
project_id = issue_info["project_id"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result = await method_executor.execute(
|
||||
"comments",
|
||||
"update",
|
||||
comment_id=comment_id,
|
||||
issue_id=issue_id,
|
||||
comment_html=comment_html,
|
||||
external_source=external_source,
|
||||
external_id=external_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
entity = await _build_issue_entity(issue_id, comment_id=comment_id)
|
||||
return PlaneToolBase.format_success_payload("Successfully updated comment", result["data"], entity=entity)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update comment", result["error"])
|
||||
|
||||
@tool
|
||||
async def comments_delete(
|
||||
comment_id: str,
|
||||
issue_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a comment.
|
||||
|
||||
Args:
|
||||
comment_id: Comment ID (required)
|
||||
issue_id: Issue ID (required)
|
||||
project_id: Project ID (optional, auto-filled from context or resolved from issue_id)
|
||||
workspace_slug: Workspace slug (optional, auto-filled from context)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Programmatically resolve project_id from issue_id if missing or invalid (non-UUID)
|
||||
should_resolve = False
|
||||
if issue_id and not project_id:
|
||||
should_resolve = True
|
||||
elif issue_id and project_id:
|
||||
try:
|
||||
uuid.UUID(str(project_id))
|
||||
except (ValueError, AttributeError):
|
||||
should_resolve = True
|
||||
|
||||
if should_resolve:
|
||||
try:
|
||||
from pi.app.api.v1.helpers.plane_sql_queries import get_issue_identifier_for_artifact
|
||||
|
||||
issue_info = await get_issue_identifier_for_artifact(str(issue_id))
|
||||
if issue_info and issue_info.get("project_id"):
|
||||
project_id = issue_info["project_id"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result = await method_executor.execute(
|
||||
"comments",
|
||||
"delete",
|
||||
comment_id=comment_id,
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
entity = await _build_issue_entity(issue_id, comment_id=comment_id)
|
||||
return PlaneToolBase.format_success_payload("Successfully deleted comment", result["data"], entity=entity)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to delete comment", result["error"])
|
||||
|
||||
return [comments_create, comments_list, comments_retrieve, comments_update, comments_delete]
|
||||
return generate_tools_for_category("comments", method_executor, context, COMMENTS_TOOL_DEFINITIONS)
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
# SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
|
||||
# SPDX-License-Identifier: LicenseRef-Plane-Commercial
|
||||
#
|
||||
# Licensed under the Plane Commercial License (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# https://plane.so/legals/eula
|
||||
#
|
||||
# DO NOT remove or modify this notice.
|
||||
# NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
|
||||
"""
|
||||
Customers API tools for Plane customer relationship management.
|
||||
"""
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
# ============================================================================
|
||||
# CUSTOMERS TOOL DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
CUSTOMER_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"create": ToolMetadata(
|
||||
name="customers_create",
|
||||
description="Create a new customer",
|
||||
sdk_method="create_customer",
|
||||
returns_entity_type="customer",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="name",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Customer name (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="description",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Plain text description (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description_html",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="HTML description (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="email",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Customer contact email (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="website_url",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Customer website URL (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="logo_props",
|
||||
type="Optional[dict]",
|
||||
required=False,
|
||||
description="Logo configuration JSON (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="domain",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Customer domain (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="employees",
|
||||
type="Optional[int]",
|
||||
required=False,
|
||||
description="Number of employees (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="stage",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Customer stage/lifecycle (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="contract_status",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Contract status (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="revenue",
|
||||
type="Optional[float]",
|
||||
required=False,
|
||||
description="Annual revenue (optional)",
|
||||
),
|
||||
],
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="customers_list",
|
||||
description="List all customers in the workspace",
|
||||
sdk_method="list_customers",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="customers_retrieve",
|
||||
description="Retrieve a single customer by ID",
|
||||
sdk_method="retrieve_customer",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="customer_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Customer ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="customers_update",
|
||||
description="Update customer details",
|
||||
sdk_method="update_customer",
|
||||
returns_entity_type="customer",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="customer_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Customer ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="name",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New customer name",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New plain text description",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description_html",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New HTML description",
|
||||
),
|
||||
ToolParameter(
|
||||
name="email",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New contact email",
|
||||
),
|
||||
ToolParameter(
|
||||
name="website_url",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New website URL",
|
||||
),
|
||||
ToolParameter(
|
||||
name="logo_props",
|
||||
type="Optional[dict]",
|
||||
required=False,
|
||||
description="New logo configuration JSON",
|
||||
),
|
||||
ToolParameter(
|
||||
name="domain",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New domain",
|
||||
),
|
||||
ToolParameter(
|
||||
name="employees",
|
||||
type="Optional[int]",
|
||||
required=False,
|
||||
description="New employee count",
|
||||
),
|
||||
ToolParameter(
|
||||
name="stage",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New customer stage",
|
||||
),
|
||||
ToolParameter(
|
||||
name="contract_status",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New contract status",
|
||||
),
|
||||
ToolParameter(
|
||||
name="revenue",
|
||||
type="Optional[float]",
|
||||
required=False,
|
||||
description="New annual revenue",
|
||||
),
|
||||
],
|
||||
),
|
||||
"delete": ToolMetadata(
|
||||
name="customers_delete",
|
||||
description="Delete a customer",
|
||||
sdk_method="delete_customer",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="customer_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Customer ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_customer_tools(method_executor, context):
|
||||
"""Return LangChain tools for the customers category using auto-generation from metadata."""
|
||||
return generate_tools_for_category(
|
||||
category="customers",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions=CUSTOMER_TOOL_DEFINITIONS,
|
||||
)
|
||||
@@ -13,368 +13,169 @@
|
||||
Cycles API tools for Plane project cycle management operations.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from .base import PlaneToolBase
|
||||
# ============================================================================
|
||||
# CYCLES TOOL DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing cycle actions
|
||||
CYCLES_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"create": ToolMetadata(
|
||||
name="cycles_create",
|
||||
description="Create a new cycle. Note: Both start_date and end_date must be provided together, or neither.",
|
||||
sdk_method="create_cycle",
|
||||
returns_entity_type="cycle",
|
||||
parameters=[
|
||||
ToolParameter(name="name", type="str", required=True, description="Cycle name (required)"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID (required)", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
ToolParameter(
|
||||
name="start_date", type="Optional[str]", required=False, description="Start date (YYYY-MM-DD). Must be paired with end_date."
|
||||
),
|
||||
ToolParameter(
|
||||
name="end_date", type="Optional[str]", required=False, description="End date (YYYY-MM-DD). Must be paired with start_date."
|
||||
),
|
||||
ToolParameter(name="description", type="Optional[str]", required=False, description="Cycle description"),
|
||||
ToolParameter(name="owned_by", type="Optional[str]", required=False, description="User ID who owns the cycle"),
|
||||
ToolParameter(name="external_id", type="Optional[str]", required=False, description="External system identifier"),
|
||||
ToolParameter(name="external_source", type="Optional[str]", required=False, description="External system source name"),
|
||||
],
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="cycles_list",
|
||||
description="List cycles in a project. Requires project_id.",
|
||||
sdk_method="list_cycles",
|
||||
parameters=[
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID (required)", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
ToolParameter(name="per_page", type="Optional[int]", required=False, description="Number of cycles per page (default: 20)"),
|
||||
ToolParameter(name="cursor", type="Optional[str]", required=False, description="Pagination cursor for next page"),
|
||||
ToolParameter(name="cycle_view", type="Optional[str]", required=False, description="Filter cycles by status"),
|
||||
],
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="cycles_retrieve",
|
||||
description="Retrieve a single cycle by ID.",
|
||||
sdk_method="retrieve_cycle",
|
||||
parameters=[
|
||||
ToolParameter(name="cycle_id", type="str", required=True, description="Cycle ID (required)"),
|
||||
ToolParameter(name="project_id", type="str", required=True, description="Project ID (required)"),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="cycles_update",
|
||||
description="Update cycle details.",
|
||||
sdk_method="update_cycle",
|
||||
returns_entity_type="cycle",
|
||||
parameters=[
|
||||
ToolParameter(name="cycle_id", type="str", required=True, description="Cycle ID (required)"),
|
||||
ToolParameter(name="project_id", type="str", required=True, description="Project ID (required)"),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
ToolParameter(name="name", type="Optional[str]", required=False, description="New cycle name"),
|
||||
ToolParameter(name="description", type="Optional[str]", required=False, description="New description"),
|
||||
ToolParameter(name="start_date", type="Optional[str]", required=False, description="New start date (YYYY-MM-DD)"),
|
||||
ToolParameter(name="end_date", type="Optional[str]", required=False, description="New end date (YYYY-MM-DD)"),
|
||||
ToolParameter(name="owned_by", type="Optional[str]", required=False, description="New owner user ID"),
|
||||
ToolParameter(name="external_id", type="Optional[str]", required=False, description="External identifier"),
|
||||
ToolParameter(name="external_source", type="Optional[str]", required=False, description="External source name"),
|
||||
],
|
||||
),
|
||||
"archive": ToolMetadata(
|
||||
name="cycles_archive",
|
||||
description="Archive a cycle.",
|
||||
sdk_method="archive_cycle",
|
||||
parameters=[
|
||||
ToolParameter(name="cycle_id", type="str", required=True, description="Cycle ID (required)"),
|
||||
ToolParameter(name="project_id", type="str", required=True, description="Project ID (required)"),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"unarchive": ToolMetadata(
|
||||
name="cycles_unarchive",
|
||||
description="Unarchive (restore) a cycle.",
|
||||
sdk_method="unarchive_cycle",
|
||||
parameters=[
|
||||
ToolParameter(name="cycle_id", type="str", required=True, description="Cycle ID (required)"),
|
||||
ToolParameter(name="project_id", type="str", required=True, description="Project ID (required)"),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"list_archived": ToolMetadata(
|
||||
name="cycles_list_archived",
|
||||
description="List archived cycles in a project.",
|
||||
sdk_method="list_archived_cycles",
|
||||
parameters=[
|
||||
ToolParameter(name="project_id", type="str", required=True, description="Project ID (required)"),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"add_work_items": ToolMetadata(
|
||||
name="cycles_add_work_items",
|
||||
description="Add work items to a cycle.",
|
||||
sdk_method="add_cycle_work_items",
|
||||
parameters=[
|
||||
ToolParameter(name="cycle_id", type="str", required=True, description="Cycle ID (required)"),
|
||||
ToolParameter(name="issues", type="list", required=True, description="List of issue IDs to add to the cycle"),
|
||||
ToolParameter(name="project_id", type="str", required=True, description="Project ID (required)"),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"list_work_items": ToolMetadata(
|
||||
name="cycles_list_work_items",
|
||||
description="List work items in a cycle.",
|
||||
sdk_method="list_cycle_work_items",
|
||||
parameters=[
|
||||
ToolParameter(name="cycle_id", type="str", required=True, description="Cycle ID (required)"),
|
||||
ToolParameter(name="project_id", type="str", required=True, description="Project ID (required)"),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"retrieve_work_item": ToolMetadata(
|
||||
name="cycles_retrieve_work_item",
|
||||
description="Get a specific work item in a cycle.",
|
||||
sdk_method="retrieve_cycle_work_item",
|
||||
parameters=[
|
||||
ToolParameter(name="cycle_id", type="str", required=True, description="Cycle ID (required)"),
|
||||
ToolParameter(name="issue_id", type="str", required=True, description="Issue ID (required)"),
|
||||
ToolParameter(name="project_id", type="str", required=True, description="Project ID (required)"),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"remove_work_item": ToolMetadata(
|
||||
name="cycles_remove_work_item",
|
||||
description="Remove a work item from a cycle.",
|
||||
sdk_method="remove_cycle_work_item",
|
||||
parameters=[
|
||||
ToolParameter(name="cycle_id", type="str", required=True, description="Cycle ID (required)"),
|
||||
ToolParameter(name="issue_id", type="str", required=True, description="Issue ID to remove (required)"),
|
||||
ToolParameter(name="project_id", type="str", required=True, description="Project ID (required)"),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"transfer_work_items": ToolMetadata(
|
||||
name="cycles_transfer_work_items",
|
||||
description="Transfer work items between cycles.",
|
||||
sdk_method="transfer_cycle_work_items",
|
||||
parameters=[
|
||||
ToolParameter(name="cycle_id", type="str", required=True, description="Source cycle ID (required)"),
|
||||
ToolParameter(name="new_cycle_id", type="str", required=True, description="Destination cycle ID (required)"),
|
||||
ToolParameter(name="project_id", type="str", required=True, description="Project ID (required)"),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_cycle_tools(method_executor, context):
|
||||
"""Return LangChain tools for the cycles category using method_executor and context."""
|
||||
|
||||
@tool
|
||||
async def cycles_create(
|
||||
name: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
owned_by: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
external_source: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new cycle.
|
||||
|
||||
Args:
|
||||
name: Cycle name (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (required - provide from conversation context)
|
||||
start_date: Start date in YYYY-MM-DD format (IMPORTANT: if provided, end_date must also be provided)
|
||||
end_date: End date in YYYY-MM-DD format (IMPORTANT: if provided, start_date must also be provided)
|
||||
description: Cycle description (optional)
|
||||
owned_by: User ID who owns the cycle (provide when user specifies ownership or assignment)
|
||||
external_id: External system identifier (optional)
|
||||
external_source: External system source name (optional)
|
||||
|
||||
Note: Both start_date and end_date must be provided together, or neither should be provided.
|
||||
You cannot provide only one date - the API will reject the request.
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None:
|
||||
if "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None:
|
||||
if "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
# project_id and workspace_slug are now required parameters
|
||||
|
||||
result = await method_executor.execute(
|
||||
"cycles",
|
||||
"create",
|
||||
name=name,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
description=description,
|
||||
owned_by=owned_by,
|
||||
external_id=external_id,
|
||||
external_source=external_source,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return await PlaneToolBase.format_success_payload_with_url(f"Successfully created cycle '{name}'", result["data"], "cycle", context)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create cycle", result["error"])
|
||||
|
||||
@tool
|
||||
async def cycles_list(
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
per_page: Optional[int] = 20,
|
||||
cursor: Optional[str] = None,
|
||||
cycle_view: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List cycles in a project.
|
||||
|
||||
Args:
|
||||
project_id: Project ID (REQUIRED - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (auto-filled from context; LLM-supplied value will be overridden)
|
||||
per_page: Number of cycles per page (default: 20, max: 100)
|
||||
cursor: Pagination cursor for next page
|
||||
cycle_view: Filter cycles by status (optional)
|
||||
|
||||
Important: This tool REQUIRES a project_id. If you don't have one, you MUST first:
|
||||
1. Call search_project_by_name to find the project, OR
|
||||
2. Call projects_list to see available projects, OR
|
||||
3. Call ask_for_clarification to ask the user which project to use
|
||||
"""
|
||||
# Auto-fill and harden workspace/project context
|
||||
# Always prefer the execution context over LLM-provided values to avoid permission errors
|
||||
if "workspace_slug" in context and context.get("workspace_slug"):
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Validate required parameters after auto-fill
|
||||
if not project_id or not workspace_slug:
|
||||
return PlaneToolBase.format_error_payload(
|
||||
"Failed to list cycles",
|
||||
"Missing required context: project_id/workspace_slug",
|
||||
)
|
||||
|
||||
result = await method_executor.execute(
|
||||
"cycles",
|
||||
"list",
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
per_page=per_page,
|
||||
cursor=cursor,
|
||||
cycle_view=cycle_view,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved cycles list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list cycles", result["error"])
|
||||
|
||||
@tool
|
||||
async def cycles_retrieve(cycle_id: str, project_id: str, workspace_slug: str) -> Dict[str, Any]:
|
||||
"""Retrieve a single cycle by ID.
|
||||
|
||||
Args:
|
||||
cycle_id: Cycle ID (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# project_id and workspace_slug are now required parameters
|
||||
|
||||
result = await method_executor.execute("cycles", "retrieve", cycle_id=cycle_id, project_id=project_id, workspace_slug=workspace_slug)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved cycle", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to retrieve cycle", result["error"])
|
||||
|
||||
@tool
|
||||
async def cycles_update(
|
||||
cycle_id: str,
|
||||
project_id: str,
|
||||
workspace_slug: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
owned_by: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
external_source: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update cycle details.
|
||||
|
||||
Args:
|
||||
cycle_id: Cycle ID (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
name: New cycle name
|
||||
description: New description
|
||||
start_date: New start date (YYYY-MM-DD)
|
||||
end_date: New end date (YYYY-MM-DD)
|
||||
owned_by: New owner user ID (optional)
|
||||
external_id: External system identifier (optional)
|
||||
external_source: External system source name (optional)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
|
||||
# Build update request object with non-None values only
|
||||
update_data = {}
|
||||
if name is not None:
|
||||
update_data["name"] = name
|
||||
if description is not None:
|
||||
update_data["description"] = description
|
||||
if start_date is not None:
|
||||
update_data["start_date"] = start_date
|
||||
if end_date is not None:
|
||||
update_data["end_date"] = end_date
|
||||
if owned_by is not None:
|
||||
update_data["owned_by"] = owned_by
|
||||
if external_id is not None:
|
||||
update_data["external_id"] = external_id
|
||||
if external_source is not None:
|
||||
update_data["external_source"] = external_source
|
||||
|
||||
result = await method_executor.execute(
|
||||
"cycles", "update", cycle_id=cycle_id, project_id=project_id, workspace_slug=workspace_slug, **update_data
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return await PlaneToolBase.format_success_payload_with_url("Successfully updated cycle", result["data"], "cycle", context)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update cycle", result["error"])
|
||||
|
||||
@tool
|
||||
async def cycles_archive(cycle_id: str, project_id: str, workspace_slug: str) -> Dict[str, Any]:
|
||||
"""Archive a cycle.
|
||||
|
||||
Args:
|
||||
cycle_id: Cycle ID (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# project_id and workspace_slug are now required parameters
|
||||
|
||||
result = await method_executor.execute("cycles", "archive", cycle_id=cycle_id, project_id=project_id, workspace_slug=workspace_slug)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully archived cycle", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to archive cycle", result["error"])
|
||||
|
||||
@tool
|
||||
async def cycles_add_work_items(cycle_id: str, issues: list, project_id: str, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Add work items to a cycle.
|
||||
|
||||
Args:
|
||||
cycle_id: Cycle ID (required)
|
||||
issues: List of issue IDs to add to the cycle (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None:
|
||||
if "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
# Check if issues list is empty
|
||||
if not issues or len(issues) == 0:
|
||||
return PlaneToolBase.format_success_payload(
|
||||
"No work items to add to cycle", {"message": "There are no work items satisfying the criteria to add to the cycle", "issues_count": 0}
|
||||
)
|
||||
|
||||
# project_id and workspace_slug are now required parameters
|
||||
result = await method_executor.execute(
|
||||
"cycles", "add_work_items", cycle_id=cycle_id, issues=issues, project_id=project_id, workspace_slug=workspace_slug
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully added work items to cycle", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to add work items to cycle", result["error"])
|
||||
|
||||
@tool
|
||||
async def cycles_remove_work_item(cycle_id: str, issue_id: str, project_id: str, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Remove a work item from a cycle.
|
||||
|
||||
Args:
|
||||
cycle_id: Cycle ID (required)
|
||||
issue_id: Issue ID to remove from the cycle (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None:
|
||||
if "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
|
||||
# project_id and workspace_slug are now required parameters
|
||||
result = await method_executor.execute(
|
||||
"cycles", "remove_work_item", cycle_id=cycle_id, issue_id=issue_id, project_id=project_id, workspace_slug=workspace_slug
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully removed work item from cycle", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to remove work item from cycle", result["error"])
|
||||
|
||||
@tool
|
||||
async def cycles_unarchive(cycle_id: str, project_id: str, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Unarchive a cycle."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None:
|
||||
if "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
|
||||
# project_id and workspace_slug are now required parameters
|
||||
result = await method_executor.execute("cycles", "unarchive", cycle_id=cycle_id, project_id=project_id, workspace_slug=workspace_slug)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload(f"Successfully unarchived cycle '{cycle_id}'", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to unarchive cycle", result["error"])
|
||||
|
||||
@tool
|
||||
async def cycles_list_archived(project_id: str, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""List archived cycles."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None:
|
||||
if "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
|
||||
# project_id and workspace_slug are now required parameters
|
||||
result = await method_executor.execute("cycles", "list_archived", project_id=project_id, workspace_slug=workspace_slug)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved archived cycles list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list archived cycles", result["error"])
|
||||
|
||||
@tool
|
||||
async def cycles_list_work_items(cycle_id: str, project_id: str, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""List work items in a cycle."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None:
|
||||
if "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
|
||||
# project_id and workspace_slug are now required parameters
|
||||
result = await method_executor.execute("cycles", "list_work_items", cycle_id=cycle_id, project_id=project_id, workspace_slug=workspace_slug)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved cycle work items list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list cycle work items", result["error"])
|
||||
|
||||
@tool
|
||||
async def cycles_retrieve_work_item(cycle_id: str, issue_id: str, project_id: str, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Get a specific work item in a cycle."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None:
|
||||
if "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
|
||||
# project_id and workspace_slug are now required parameters
|
||||
result = await method_executor.execute(
|
||||
"cycles", "retrieve_work_item", cycle_id=cycle_id, issue_id=issue_id, project_id=project_id, workspace_slug=workspace_slug
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved cycle work item", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to retrieve cycle work item", result["error"])
|
||||
|
||||
@tool
|
||||
async def cycles_transfer_work_items(cycle_id: str, new_cycle_id: str, project_id: str, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Transfer work items between cycles."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None:
|
||||
if "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
|
||||
# project_id and workspace_slug are now required parameters
|
||||
result = await method_executor.execute(
|
||||
"cycles", "transfer_work_items", cycle_id=cycle_id, new_cycle_id=new_cycle_id, project_id=project_id, workspace_slug=workspace_slug
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload(
|
||||
f"Successfully transferred work items from cycle '{cycle_id}' to '{new_cycle_id}'", result["data"]
|
||||
)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to transfer cycle work items", result["error"])
|
||||
|
||||
return [
|
||||
cycles_create,
|
||||
cycles_update,
|
||||
cycles_archive,
|
||||
cycles_unarchive,
|
||||
cycles_add_work_items,
|
||||
cycles_remove_work_item,
|
||||
cycles_transfer_work_items,
|
||||
cycles_retrieve,
|
||||
cycles_list,
|
||||
cycles_list_archived,
|
||||
cycles_list_work_items,
|
||||
cycles_retrieve_work_item,
|
||||
]
|
||||
"""Return LangChain tools for the cycles category using auto-generation from metadata."""
|
||||
return generate_tools_for_category("cycles", method_executor, context, CYCLES_TOOL_DEFINITIONS)
|
||||
|
||||
@@ -0,0 +1,535 @@
|
||||
# SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
|
||||
# SPDX-License-Identifier: LicenseRef-Plane-Commercial
|
||||
#
|
||||
# Licensed under the Plane Commercial License (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# https://plane.so/legals/eula
|
||||
#
|
||||
# DO NOT remove or modify this notice.
|
||||
# NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
|
||||
"""
|
||||
Initiatives API tools for Plane strategic planning operations.
|
||||
"""
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
# ============================================================================
|
||||
# INITIATIVES TOOL DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
INITIATIVE_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"create": ToolMetadata(
|
||||
name="initiatives_create",
|
||||
description="Create a new strategic initiative",
|
||||
sdk_method="create_initiative",
|
||||
returns_entity_type="initiative",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="name",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Initiative name (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="description_html",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="HTML description of the initiative",
|
||||
),
|
||||
ToolParameter(
|
||||
name="start_date",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Initiative start date (YYYY-MM-DD)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="end_date",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Initiative end date (YYYY-MM-DD)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="state",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Initiative state. Must be one of: DRAFT, PLANNED, ACTIVE, COMPLETED, CLOSED (uppercase required). Defaults to DRAFT if not provided.", # noqa: E501
|
||||
),
|
||||
ToolParameter(
|
||||
name="lead",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Initiative lead user ID",
|
||||
),
|
||||
],
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="initiatives_list",
|
||||
description="List initiatives in the workspace",
|
||||
sdk_method="list_initiatives",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="initiatives_retrieve",
|
||||
description="Retrieve a single initiative by ID",
|
||||
sdk_method="retrieve_initiative",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="initiative_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Initiative ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="initiatives_update",
|
||||
description="Update initiative details",
|
||||
sdk_method="update_initiative",
|
||||
returns_entity_type="initiative",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="initiative_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Initiative ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="name",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New initiative name",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description_html",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New HTML description",
|
||||
),
|
||||
ToolParameter(
|
||||
name="start_date",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New start date (YYYY-MM-DD)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="end_date",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New end date (YYYY-MM-DD)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="logo_props",
|
||||
type="Optional[dict]",
|
||||
required=False,
|
||||
description="New logo configuration JSON",
|
||||
),
|
||||
ToolParameter(
|
||||
name="state",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New initiative state. Must be one of: DRAFT, PLANNED, ACTIVE, COMPLETED, CLOSED (uppercase required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="lead",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New initiative lead user ID",
|
||||
),
|
||||
],
|
||||
),
|
||||
"delete": ToolMetadata(
|
||||
name="initiatives_delete",
|
||||
description="Delete an initiative",
|
||||
sdk_method="delete_initiative",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="initiative_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Initiative ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
# Label operations
|
||||
"create_label": ToolMetadata(
|
||||
name="initiatives_create_label",
|
||||
description="Create a workspace-level initiative label (can later be attached to initiatives)",
|
||||
sdk_method="create_initiative_label",
|
||||
returns_entity_type="initiative_label",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="name",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Label name (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="color",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Label color hex code",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Label description",
|
||||
),
|
||||
],
|
||||
),
|
||||
"list_labels": ToolMetadata(
|
||||
name="initiatives_list_labels",
|
||||
description="List all workspace-level initiative labels",
|
||||
sdk_method="list_initiative_labels",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"retrieve_label": ToolMetadata(
|
||||
name="initiatives_retrieve_label",
|
||||
description="Get a single initiative label by ID",
|
||||
sdk_method="retrieve_initiative_label",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="label_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Label ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"update_label": ToolMetadata(
|
||||
name="initiatives_update_label",
|
||||
description="Update an initiative label",
|
||||
sdk_method="update_initiative_label",
|
||||
returns_entity_type="initiative_label",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="label_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Label ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="name",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New label name",
|
||||
),
|
||||
ToolParameter(
|
||||
name="color",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New label color hex code",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New label description",
|
||||
),
|
||||
],
|
||||
),
|
||||
"delete_label": ToolMetadata(
|
||||
name="initiatives_delete_label",
|
||||
description="Delete an initiative label",
|
||||
sdk_method="delete_initiative_label",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="label_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Label ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"add_labels": ToolMetadata(
|
||||
name="initiatives_add_labels",
|
||||
description="Attach existing labels to an initiative",
|
||||
sdk_method="add_initiative_labels",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="initiative_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Initiative ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="label_ids",
|
||||
type="List[str]",
|
||||
required=True,
|
||||
description="List of label IDs to attach (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"remove_labels": ToolMetadata(
|
||||
name="initiatives_remove_labels",
|
||||
description="Detach labels from an initiative",
|
||||
sdk_method="remove_initiative_labels",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="initiative_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Initiative ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="label_ids",
|
||||
type="List[str]",
|
||||
required=True,
|
||||
description="List of label IDs to detach (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
# Project operations
|
||||
"add_projects": ToolMetadata(
|
||||
name="initiatives_add_projects",
|
||||
description="Link projects to an initiative",
|
||||
sdk_method="add_initiative_projects",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="initiative_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Initiative ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_ids",
|
||||
type="List[str]",
|
||||
required=True,
|
||||
description="List of project IDs to link (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"list_projects": ToolMetadata(
|
||||
name="initiatives_list_projects",
|
||||
description="List projects linked to an initiative",
|
||||
sdk_method="list_initiative_projects",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="initiative_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Initiative ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"remove_projects": ToolMetadata(
|
||||
name="initiatives_remove_projects",
|
||||
description="Unlink projects from an initiative",
|
||||
sdk_method="remove_initiative_projects",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="initiative_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Initiative ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_ids",
|
||||
type="List[str]",
|
||||
required=True,
|
||||
description="List of project IDs to unlink (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
# Epic operations
|
||||
"add_epics": ToolMetadata(
|
||||
name="initiatives_add_epics",
|
||||
description="Link epics to an initiative",
|
||||
sdk_method="add_initiative_epics",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="initiative_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Initiative ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="epic_ids",
|
||||
type="List[str]",
|
||||
required=True,
|
||||
description="List of epic IDs to link (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"list_epics": ToolMetadata(
|
||||
name="initiatives_list_epics",
|
||||
description="List epics linked to an initiative",
|
||||
sdk_method="list_initiative_epics",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="initiative_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Initiative ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"remove_epics": ToolMetadata(
|
||||
name="initiatives_remove_epics",
|
||||
description="Unlink epics from an initiative",
|
||||
sdk_method="remove_initiative_epics",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="initiative_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Initiative ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="epic_ids",
|
||||
type="List[str]",
|
||||
required=True,
|
||||
description="List of epic IDs to unlink (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_initiative_tools(method_executor, context):
|
||||
"""Return LangChain tools for the initiatives category using auto-generation from metadata."""
|
||||
return generate_tools_for_category(
|
||||
category="initiatives",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions=INITIATIVE_TOOL_DEFINITIONS,
|
||||
)
|
||||
@@ -13,195 +13,88 @@
|
||||
Intake API tools for Plane work item triage operations.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from langchain_core.tools import tool
|
||||
# ============================================================================
|
||||
# TOOL METADATA DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
from .base import PlaneToolBase
|
||||
INTAKE_TOOL_DEFINITIONS = {
|
||||
"create": ToolMetadata(
|
||||
name="intake_create",
|
||||
description="Submit work item to intake queue for triage. Creates a new intake item that can be reviewed and converted to a work item.",
|
||||
sdk_method="create_intake",
|
||||
parameters=[
|
||||
ToolParameter(name="name", type="str", required=True, description="Work item title (required)"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
ToolParameter(name="description_html", type="Optional[str]", required=False, description="Description in HTML format"),
|
||||
ToolParameter(name="priority", type="Optional[str]", required=False, description="Priority level (high, medium, low, urgent, none)"),
|
||||
ToolParameter(name="assignee", type="Optional[str]", required=False, description="Assignee user ID"),
|
||||
ToolParameter(name="reporter", type="Optional[str]", required=False, description="Reporter user ID"),
|
||||
ToolParameter(name="labels", type="Optional[list]", required=False, description="List of label IDs"),
|
||||
],
|
||||
returns_entity_type="intake",
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="intake_list",
|
||||
description="List intake items awaiting triage for a project.",
|
||||
sdk_method="list_intake",
|
||||
parameters=[
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
ToolParameter(name="per_page", type="Optional[int]", required=False, description="Number of results per page (default: 20)"),
|
||||
ToolParameter(name="cursor", type="Optional[str]", required=False, description="Pagination cursor"),
|
||||
],
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="intake_retrieve",
|
||||
description="Get a single intake work item by ID.",
|
||||
sdk_method="retrieve_intake",
|
||||
parameters=[
|
||||
ToolParameter(name="intake_id", type="str", required=True, description="Intake item ID to retrieve"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="intake_update",
|
||||
description="Update intake work item details before triage.",
|
||||
sdk_method="update_intake",
|
||||
parameters=[
|
||||
ToolParameter(name="intake_id", type="str", required=True, description="Intake item ID to update"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
ToolParameter(name="name", type="Optional[str]", required=False, description="Work item title"),
|
||||
ToolParameter(name="description_html", type="Optional[str]", required=False, description="Description in HTML format"),
|
||||
ToolParameter(name="priority", type="Optional[str]", required=False, description="Priority level (high, medium, low, urgent, none)"),
|
||||
ToolParameter(name="assignee", type="Optional[str]", required=False, description="Assignee user ID"),
|
||||
ToolParameter(name="reporter", type="Optional[str]", required=False, description="Reporter user ID"),
|
||||
ToolParameter(name="labels", type="Optional[list]", required=False, description="List of label IDs"),
|
||||
],
|
||||
returns_entity_type="intake",
|
||||
),
|
||||
"delete": ToolMetadata(
|
||||
name="intake_delete",
|
||||
description="Remove an intake work item from the triage queue.",
|
||||
sdk_method="delete_intake",
|
||||
parameters=[
|
||||
ToolParameter(name="intake_id", type="str", required=True, description="Intake item ID to delete"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
returns_entity_type="intake",
|
||||
),
|
||||
}
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing intake actions
|
||||
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_intake_tools(method_executor, context):
|
||||
"""Return LangChain tools for the intake category using method_executor and context."""
|
||||
|
||||
@tool
|
||||
async def intake_create(
|
||||
name: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
description_html: Optional[str] = None,
|
||||
priority: Optional[str] = None,
|
||||
assignee: Optional[str] = None,
|
||||
reporter: Optional[str] = None,
|
||||
labels: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Submit work item to intake queue for triage.
|
||||
|
||||
Args:
|
||||
name: Work item title (required)
|
||||
description_html: Description in HTML format
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
priority: Priority level (high, medium, low, urgent, none)
|
||||
assignee: Assignee user ID
|
||||
reporter: Reporter user ID
|
||||
labels: List of label IDs
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"intake",
|
||||
"create",
|
||||
name=name,
|
||||
description_html=description_html,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
priority=priority,
|
||||
assignee=assignee,
|
||||
reporter=reporter,
|
||||
labels=labels,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload(f"Successfully submitted intake item '{name}'", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create intake item", result["error"])
|
||||
|
||||
@tool
|
||||
async def intake_list(
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
per_page: Optional[int] = 20,
|
||||
cursor: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List intake items awaiting triage."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"intake",
|
||||
"list",
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
per_page=per_page,
|
||||
cursor=cursor,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved intake list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list intake items", result["error"])
|
||||
|
||||
@tool
|
||||
async def intake_retrieve(
|
||||
intake_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get a single intake work item by ID."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"intake",
|
||||
"retrieve",
|
||||
intake_id=intake_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved intake item", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to retrieve intake item", result["error"])
|
||||
|
||||
@tool
|
||||
async def intake_update(
|
||||
intake_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
description_html: Optional[str] = None,
|
||||
priority: Optional[str] = None,
|
||||
assignee: Optional[str] = None,
|
||||
reporter: Optional[str] = None,
|
||||
labels: Optional[List[str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update intake work item details."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Build update data
|
||||
update_data: Dict[str, Any] = {}
|
||||
if name is not None:
|
||||
update_data["name"] = name
|
||||
if description_html is not None:
|
||||
update_data["description_html"] = description_html
|
||||
if priority is not None:
|
||||
update_data["priority"] = priority
|
||||
if assignee is not None:
|
||||
update_data["assignee"] = assignee
|
||||
if reporter is not None:
|
||||
update_data["reporter"] = reporter
|
||||
if labels is not None:
|
||||
update_data["labels"] = labels
|
||||
|
||||
result = await method_executor.execute(
|
||||
"intake",
|
||||
"update",
|
||||
intake_id=intake_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
**update_data,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully updated intake item", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update intake item", result["error"])
|
||||
|
||||
@tool
|
||||
async def intake_delete(
|
||||
intake_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Remove intake work item."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"intake",
|
||||
"delete",
|
||||
intake_id=intake_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully deleted intake item", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to delete intake item", result["error"])
|
||||
|
||||
return [intake_create, intake_list, intake_retrieve, intake_update, intake_delete]
|
||||
"""Return LangChain tools for the intake category using auto-generation from metadata."""
|
||||
return generate_tools_for_category("intake", method_executor, context, INTAKE_TOOL_DEFINITIONS)
|
||||
|
||||
@@ -11,189 +11,349 @@
|
||||
|
||||
"""
|
||||
Labels API tools for Plane labeling operations.
|
||||
|
||||
MIGRATED TO AUTO-GENERATED TOOLS
|
||||
Tool metadata defined in this file (LABEL_TOOL_DEFINITIONS) for modularity.
|
||||
Old manual definitions kept below for comparison/rollback safety.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
|
||||
from .base import PlaneToolBase
|
||||
# Import tool metadata models
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
# Tool metadata for labels category - single source of truth
|
||||
LABEL_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"create": ToolMetadata(
|
||||
name="labels_create",
|
||||
description="Create a new label",
|
||||
sdk_method="create_label",
|
||||
returns_entity_type="label",
|
||||
parameters=[
|
||||
ToolParameter(name="name", type="str", required=True, description="Label name (required)"),
|
||||
ToolParameter(name="color", type="str", required=True, description="Label color in hex format (required)"),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (required - provide from conversation context or previous actions)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (provide if known, otherwise auto-detected)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(name="description", type="Optional[str]", required=False, description="Label description", property_transform="name"),
|
||||
ToolParameter(
|
||||
name="parent", type="Optional[str]", required=False, description="Parent label ID for nested labels", property_transform="id"
|
||||
),
|
||||
ToolParameter(
|
||||
name="sort_order", type="Optional[float]", required=False, description="Sort order for display (float)", property_transform="name"
|
||||
),
|
||||
ToolParameter(
|
||||
name="external_source",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description='External source identifier (e.g., "jira")',
|
||||
property_transform="name",
|
||||
),
|
||||
ToolParameter(name="external_id", type="Optional[str]", required=False, description="External system ID", property_transform="name"),
|
||||
],
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="labels_list",
|
||||
description="List all labels in a project",
|
||||
sdk_method="list_labels",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (required - provide from conversation context or previous actions)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (provide if known, otherwise auto-detected)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
],
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="labels_retrieve",
|
||||
description="Retrieve details of a specific label",
|
||||
sdk_method="retrieve_label",
|
||||
parameters=[
|
||||
ToolParameter(name="label_id", type="str", required=True, description="Label ID (required)"),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (required - provide from conversation context or previous actions)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (provide if known, otherwise auto-detected)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
],
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="labels_update",
|
||||
description="Update an existing label",
|
||||
sdk_method="update_label",
|
||||
returns_entity_type="label",
|
||||
parameters=[
|
||||
ToolParameter(name="label_id", type="str", required=True, description="Label ID (required)"),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (required - provide from conversation context or previous actions)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (provide if known, otherwise auto-detected)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(name="name", type="Optional[str]", required=False, description="New label name", property_transform="name"),
|
||||
ToolParameter(name="color", type="Optional[str]", required=False, description="New label color", property_transform="name"),
|
||||
ToolParameter(name="description", type="Optional[str]", required=False, description="New label description", property_transform="name"),
|
||||
ToolParameter(
|
||||
name="parent", type="Optional[str]", required=False, description="Parent label ID for nested labels", property_transform="id"
|
||||
),
|
||||
ToolParameter(
|
||||
name="sort_order", type="Optional[float]", required=False, description="Sort order for display (float)", property_transform="name"
|
||||
),
|
||||
ToolParameter(
|
||||
name="external_source",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description='External source identifier (e.g., "jira")',
|
||||
property_transform="name",
|
||||
),
|
||||
ToolParameter(name="external_id", type="Optional[str]", required=False, description="External system ID", property_transform="name"),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing label actions
|
||||
|
||||
|
||||
def get_label_tools(method_executor, context):
|
||||
"""Return LangChain tools for the labels category using method_executor and context."""
|
||||
"""Return LangChain tools for the labels category using auto-generation from metadata."""
|
||||
|
||||
@tool
|
||||
async def labels_create(
|
||||
name: str,
|
||||
color: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
parent: Optional[str] = None,
|
||||
sort_order: Optional[float] = None,
|
||||
external_source: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new label.
|
||||
# Generate all label tools from local metadata
|
||||
label_tools = generate_tools_for_category(
|
||||
category="labels",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions=LABEL_TOOL_DEFINITIONS,
|
||||
)
|
||||
|
||||
Args:
|
||||
name: Label name (required)
|
||||
color: Label color in hex format (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
description: Label description
|
||||
parent: Parent label ID for nested labels
|
||||
sort_order: Sort order for display (float)
|
||||
external_source: External source identifier (e.g., "jira")
|
||||
external_id: External system ID
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
return label_tools
|
||||
|
||||
result = await method_executor.execute(
|
||||
"labels",
|
||||
"create",
|
||||
name=name,
|
||||
color=color,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
description=description,
|
||||
parent=parent,
|
||||
sort_order=sort_order,
|
||||
external_source=external_source,
|
||||
external_id=external_id,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return await PlaneToolBase.format_success_payload_with_url(f"Successfully created label '{name}'", result["data"], "label", context)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create label", result["error"])
|
||||
# ============================================================================
|
||||
# OLD MANUAL TOOL DEFINITIONS (COMMENTED OUT - KEPT FOR COMPARISON)
|
||||
# To rollback: uncomment below and comment out the auto-generation code above
|
||||
# ============================================================================
|
||||
|
||||
@tool
|
||||
async def labels_list(
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List all labels in a project."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"labels",
|
||||
"list",
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved labels list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list labels", result["error"])
|
||||
|
||||
@tool
|
||||
async def labels_retrieve(
|
||||
label_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Retrieve details of a specific label.
|
||||
|
||||
Args:
|
||||
label_id: Label ID (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"labels",
|
||||
"retrieve",
|
||||
label_id=label_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved label details", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to retrieve label", result["error"])
|
||||
|
||||
@tool
|
||||
async def labels_update(
|
||||
label_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
color: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
parent: Optional[str] = None,
|
||||
sort_order: Optional[float] = None,
|
||||
external_source: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update an existing label.
|
||||
|
||||
Args:
|
||||
label_id: Label ID (required)
|
||||
name: New label name
|
||||
color: New label color
|
||||
description: New label description
|
||||
parent: Parent label ID for nested labels
|
||||
sort_order: Sort order for display (float)
|
||||
external_source: External source identifier (e.g., "jira")
|
||||
external_id: External system ID
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Build update data with only non-None values
|
||||
update_data = {
|
||||
k: v
|
||||
for k, v in {
|
||||
"name": name,
|
||||
"color": color,
|
||||
"description": description,
|
||||
"parent": parent,
|
||||
"sort_order": sort_order,
|
||||
"external_source": external_source,
|
||||
"external_id": external_id,
|
||||
}.items()
|
||||
if v is not None
|
||||
}
|
||||
|
||||
result = await method_executor.execute(
|
||||
"labels",
|
||||
"update",
|
||||
label_id=label_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
**update_data,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return await PlaneToolBase.format_success_payload_with_url(f"Successfully updated label '{label_id}'", result["data"], "label", context)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update label", result["error"])
|
||||
|
||||
return [labels_create, labels_list, labels_retrieve, labels_update]
|
||||
# from typing import Optional
|
||||
# from langchain_core.tools import tool
|
||||
# from .base import PlaneToolBase
|
||||
#
|
||||
# def get_label_tools(method_executor, context):
|
||||
# """Return LangChain tools for the labels category using method_executor and context."""
|
||||
#
|
||||
# @tool
|
||||
# async def labels_create(
|
||||
# name: str,
|
||||
# color: str,
|
||||
# project_id: Optional[str] = None,
|
||||
# workspace_slug: Optional[str] = None,
|
||||
# description: Optional[str] = None,
|
||||
# parent: Optional[str] = None,
|
||||
# sort_order: Optional[float] = None,
|
||||
# external_source: Optional[str] = None,
|
||||
# external_id: Optional[str] = None,
|
||||
# ) -> Dict[str, Any]:
|
||||
# """Create a new label.
|
||||
#
|
||||
# Args:
|
||||
# name: Label name (required)
|
||||
# color: Label color in hex format (required)
|
||||
# project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
# workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
# description: Label description
|
||||
# parent: Parent label ID for nested labels
|
||||
# sort_order: Sort order for display (float)
|
||||
# external_source: External source identifier (e.g., "jira")
|
||||
# external_id: External system ID
|
||||
# """
|
||||
# # Auto-fill from context if not provided
|
||||
# if workspace_slug is None and "workspace_slug" in context:
|
||||
# workspace_slug = context["workspace_slug"]
|
||||
# if project_id is None and "project_id" in context:
|
||||
# project_id = context["project_id"]
|
||||
#
|
||||
# result = await method_executor.execute(
|
||||
# "labels",
|
||||
# "create",
|
||||
# name=name,
|
||||
# color=color,
|
||||
# project_id=project_id,
|
||||
# workspace_slug=workspace_slug,
|
||||
# description=description,
|
||||
# parent=parent,
|
||||
# sort_order=sort_order,
|
||||
# external_source=external_source,
|
||||
# external_id=external_id,
|
||||
# )
|
||||
#
|
||||
# if result["success"]:
|
||||
# return await PlaneToolBase.format_success_payload_with_url(f"Successfully created label '{name}'", result["data"], "label", context)
|
||||
# else:
|
||||
# return PlaneToolBase.format_error_payload("Failed to create label", result["error"])
|
||||
#
|
||||
# @tool
|
||||
# async def labels_list(
|
||||
# project_id: Optional[str] = None,
|
||||
# workspace_slug: Optional[str] = None,
|
||||
# ) -> Dict[str, Any]:
|
||||
# """List all labels in a project."""
|
||||
# # Auto-fill from context if not provided
|
||||
# if workspace_slug is None and "workspace_slug" in context:
|
||||
# workspace_slug = context["workspace_slug"]
|
||||
# if project_id is None and "project_id" in context:
|
||||
# project_id = context["project_id"]
|
||||
#
|
||||
# result = await method_executor.execute(
|
||||
# "labels",
|
||||
# "list",
|
||||
# project_id=project_id,
|
||||
# workspace_slug=workspace_slug,
|
||||
# )
|
||||
#
|
||||
# if result["success"]:
|
||||
# return PlaneToolBase.format_success_payload("Successfully retrieved labels list", result["data"])
|
||||
# else:
|
||||
# return PlaneToolBase.format_error_payload("Failed to list labels", result["error"])
|
||||
#
|
||||
# @tool
|
||||
# async def labels_retrieve(
|
||||
# label_id: str,
|
||||
# project_id: Optional[str] = None,
|
||||
# workspace_slug: Optional[str] = None,
|
||||
# ) -> Dict[str, Any]:
|
||||
# """Retrieve details of a specific label.
|
||||
#
|
||||
# Args:
|
||||
# label_id: Label ID (required)
|
||||
# project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
# workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
# """
|
||||
# # Auto-fill from context if not provided
|
||||
# if workspace_slug is None and "workspace_slug" in context:
|
||||
# workspace_slug = context["workspace_slug"]
|
||||
# if project_id is None and "project_id" in context:
|
||||
# project_id = context["project_id"]
|
||||
#
|
||||
# result = await method_executor.execute(
|
||||
# "labels",
|
||||
# "retrieve",
|
||||
# label_id=label_id,
|
||||
# project_id=project_id,
|
||||
# workspace_slug=workspace_slug,
|
||||
# )
|
||||
#
|
||||
# if result["success"]:
|
||||
# return PlaneToolBase.format_success_payload("Successfully retrieved label details", result["data"])
|
||||
# else:
|
||||
# return PlaneToolBase.format_error_payload("Failed to retrieve label", result["error"])
|
||||
#
|
||||
# @tool
|
||||
# async def labels_update(
|
||||
# label_id: str,
|
||||
# project_id: Optional[str] = None,
|
||||
# workspace_slug: Optional[str] = None,
|
||||
# name: Optional[str] = None,
|
||||
# color: Optional[str] = None,
|
||||
# description: Optional[str] = None,
|
||||
# parent: Optional[str] = None,
|
||||
# sort_order: Optional[float] = None,
|
||||
# external_source: Optional[str] = None,
|
||||
# external_id: Optional[str] = None,
|
||||
# ) -> Dict[str, Any]:
|
||||
# """Update an existing label.
|
||||
#
|
||||
# Args:
|
||||
# label_id: Label ID (required)
|
||||
# name: New label name
|
||||
# color: New label color
|
||||
# description: New label description
|
||||
# parent: Parent label ID for nested labels
|
||||
# sort_order: Sort order for display (float)
|
||||
# external_source: External source identifier (e.g., "jira")
|
||||
# external_id: External system ID
|
||||
# project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
# workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
# """
|
||||
# # Auto-fill from context if not provided
|
||||
# if workspace_slug is None and "workspace_slug" in context:
|
||||
# workspace_slug = context["workspace_slug"]
|
||||
# if project_id is None and "project_id" in context:
|
||||
# project_id = context["project_id"]
|
||||
#
|
||||
# # Build update data with only non-None values
|
||||
# update_data = {
|
||||
# k: v
|
||||
# for k, v in {
|
||||
# "name": name,
|
||||
# "color": color,
|
||||
# "description": description,
|
||||
# "parent": parent,
|
||||
# "sort_order": sort_order,
|
||||
# "external_source": external_source,
|
||||
# "external_id": external_id,
|
||||
# }.items()
|
||||
# if v is not None
|
||||
# }
|
||||
#
|
||||
# result = await method_executor.execute(
|
||||
# "labels",
|
||||
# "update",
|
||||
# label_id=label_id,
|
||||
# project_id=project_id,
|
||||
# workspace_slug=workspace_slug,
|
||||
# **update_data,
|
||||
# )
|
||||
#
|
||||
# if result["success"]:
|
||||
# return await PlaneToolBase.format_success_payload_with_url(f"Successfully updated label '{label_id}'", result["data"], "label", context)
|
||||
# else:
|
||||
# return PlaneToolBase.format_error_payload("Failed to update label", result["error"])
|
||||
#
|
||||
# return [labels_create, labels_list, labels_retrieve, labels_update]
|
||||
|
||||
@@ -13,185 +13,122 @@
|
||||
Links API tools for Plane issue link management.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from .base import PlaneToolBase
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing link actions
|
||||
# Pre-handler to resolve project_id from issue_id if missing
|
||||
async def _resolve_project_pre_handler(
|
||||
metadata: ToolMetadata,
|
||||
kwargs: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
category: str,
|
||||
method_key: str,
|
||||
method_executor: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""Resolve project_id from issue_id if it is missing or invalid."""
|
||||
issue_id = kwargs.get("issue_id")
|
||||
project_id = kwargs.get("project_id")
|
||||
|
||||
should_resolve = False
|
||||
if issue_id and not project_id:
|
||||
should_resolve = True
|
||||
elif issue_id and project_id:
|
||||
try:
|
||||
uuid.UUID(str(project_id))
|
||||
except (ValueError, AttributeError):
|
||||
should_resolve = True
|
||||
|
||||
if should_resolve:
|
||||
try:
|
||||
from pi.app.api.v1.helpers.plane_sql_queries import get_issue_identifier_for_artifact
|
||||
|
||||
issue_info = await get_issue_identifier_for_artifact(str(issue_id))
|
||||
if issue_info and issue_info.get("project_id"):
|
||||
kwargs["project_id"] = issue_info["project_id"]
|
||||
except Exception:
|
||||
# Best effort, proceed without resolution
|
||||
pass
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
LINKS_TOOL_DEFINITIONS = {
|
||||
"create": ToolMetadata(
|
||||
name="links_create",
|
||||
description="Create a link for an issue.",
|
||||
sdk_method="create_work_item_link",
|
||||
parameters=[
|
||||
ToolParameter(name="issue_id", description="UUID of the work item", required=True, type="str"),
|
||||
ToolParameter(name="url", description="The URL of the link", required=True, type="str"),
|
||||
ToolParameter(name="title", description="Title/label for the link", required=False, type="Optional[str]"),
|
||||
ToolParameter(name="project_id", description="UUID of the project", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", description="Workspace slug", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
ToolParameter(name="metadata", description="Link metadata", required=False, type="Optional[dict]"),
|
||||
],
|
||||
pre_handler=_resolve_project_pre_handler,
|
||||
returns_entity_type="link",
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="links_list",
|
||||
description="List links for an issue.",
|
||||
sdk_method="list_work_item_links",
|
||||
parameters=[
|
||||
ToolParameter(name="issue_id", description="Issue ID", required=True, type="str"),
|
||||
ToolParameter(name="project_id", description="Project ID", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", description="Workspace slug", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
],
|
||||
pre_handler=_resolve_project_pre_handler,
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="links_retrieve",
|
||||
description="Get a single link by ID.",
|
||||
sdk_method="retrieve_work_item_link",
|
||||
parameters=[
|
||||
ToolParameter(name="link_id", description="UUID of the link", required=True, type="str"),
|
||||
ToolParameter(name="issue_id", description="UUID of the work item", required=True, type="str"),
|
||||
ToolParameter(name="project_id", description="UUID of the project", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", description="Workspace slug", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
],
|
||||
pre_handler=_resolve_project_pre_handler,
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="links_update",
|
||||
description="Update link details.",
|
||||
sdk_method="update_issue_link",
|
||||
parameters=[
|
||||
ToolParameter(name="link_id", description="UUID of the link", required=True, type="str"),
|
||||
ToolParameter(name="issue_id", description="UUID of the work item", required=True, type="str"),
|
||||
ToolParameter(name="url", description="New URL", required=False, type="Optional[str]"),
|
||||
ToolParameter(name="title", description="New title", required=False, type="Optional[str]"),
|
||||
ToolParameter(name="project_id", description="UUID of the project", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", description="Workspace slug", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
ToolParameter(name="metadata", description="Link metadata", required=False, type="Optional[dict]"),
|
||||
],
|
||||
pre_handler=_resolve_project_pre_handler,
|
||||
returns_entity_type="link",
|
||||
),
|
||||
"delete": ToolMetadata(
|
||||
name="links_delete",
|
||||
description="Delete a link.",
|
||||
sdk_method="delete_work_item_link",
|
||||
parameters=[
|
||||
ToolParameter(name="link_id", description="UUID of the link", required=True, type="str"),
|
||||
ToolParameter(name="issue_id", description="UUID of the work item", required=True, type="str"),
|
||||
ToolParameter(name="project_id", description="UUID of the project", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", description="Workspace slug", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
],
|
||||
pre_handler=_resolve_project_pre_handler,
|
||||
returns_entity_type="link",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_link_tools(method_executor, context):
|
||||
"""Return LangChain tools for the links category using method_executor and context."""
|
||||
|
||||
@tool
|
||||
async def links_create(
|
||||
issue_id: str,
|
||||
url: str,
|
||||
title: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a link for an issue.
|
||||
|
||||
Args:
|
||||
issue_id: UUID of the work item to create a link for (required)
|
||||
url: The URL of the link (required)
|
||||
title: Optional title/label for the link
|
||||
project_id: UUID of the project (optional, auto-filled from context)
|
||||
workspace_slug: Workspace slug identifier (optional, auto-filled from context)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"links",
|
||||
"create",
|
||||
issue_id=issue_id,
|
||||
url=url,
|
||||
title=title,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully created link", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create link", result["error"])
|
||||
|
||||
@tool
|
||||
async def links_list(issue_id: str, project_id: Optional[str] = None, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""List links for an issue."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute("links", "list", issue_id=issue_id, project_id=project_id, workspace_slug=workspace_slug)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved links list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list links", result["error"])
|
||||
|
||||
@tool
|
||||
async def links_retrieve(
|
||||
link_id: str,
|
||||
issue_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get a single link by ID.
|
||||
|
||||
Args:
|
||||
link_id: UUID of the link to retrieve (required)
|
||||
issue_id: UUID of the work item the link belongs to (required)
|
||||
project_id: UUID of the project (optional, auto-filled from context)
|
||||
workspace_slug: Workspace slug identifier (optional, auto-filled from context)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"links",
|
||||
"retrieve",
|
||||
link_id=link_id,
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved link", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to retrieve link", result["error"])
|
||||
|
||||
@tool
|
||||
async def links_update(
|
||||
link_id: str,
|
||||
issue_id: str,
|
||||
url: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update link details.
|
||||
|
||||
Args:
|
||||
link_id: UUID of the link to update (required)
|
||||
issue_id: UUID of the work item the link belongs to (required)
|
||||
url: New URL for the link (optional)
|
||||
title: New title/label for the link (optional)
|
||||
project_id: UUID of the project (optional, auto-filled from context)
|
||||
workspace_slug: Workspace slug identifier (optional, auto-filled from context)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Build update data
|
||||
update_data = {}
|
||||
if url is not None:
|
||||
update_data["url"] = url
|
||||
if title is not None:
|
||||
update_data["title"] = title
|
||||
|
||||
result = await method_executor.execute(
|
||||
"links",
|
||||
"update",
|
||||
link_id=link_id,
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
**update_data,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully updated link", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update link", result["error"])
|
||||
|
||||
@tool
|
||||
async def links_delete(
|
||||
link_id: str,
|
||||
issue_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a link.
|
||||
|
||||
Args:
|
||||
link_id: UUID of the link to delete (required)
|
||||
issue_id: UUID of the work item the link belongs to (required)
|
||||
project_id: UUID of the project (optional, auto-filled from context)
|
||||
workspace_slug: Workspace slug identifier (optional, auto-filled from context)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"links",
|
||||
"delete",
|
||||
link_id=link_id,
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully deleted link", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to delete link", result["error"])
|
||||
|
||||
return [links_create, links_list, links_retrieve, links_update, links_delete]
|
||||
return generate_tools_for_category("links", method_executor, context, LINKS_TOOL_DEFINITIONS)
|
||||
|
||||
@@ -11,102 +11,115 @@
|
||||
|
||||
"""
|
||||
Members API tools for Plane workspace and project member management.
|
||||
|
||||
MIGRATED TO AUTO-GENERATED TOOLS
|
||||
Tool metadata defined in this file (MEMBER_TOOL_DEFINITIONS) for modularity.
|
||||
Bot filtering handled via custom post_handler function.
|
||||
Old manual definitions kept below for comparison/rollback safety.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi import logger
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from .base import PlaneToolBase
|
||||
log = logger.getChild(__name__)
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing member actions
|
||||
|
||||
# ============================================================================
|
||||
# MEMBERS-SPECIFIC HANDLER FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def _filter_bots_handler(
|
||||
metadata: ToolMetadata,
|
||||
result: Dict[str, Any],
|
||||
kwargs: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
method_executor: Any,
|
||||
category: str,
|
||||
method_key: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Post-processing handler to filter bot users from member lists."""
|
||||
|
||||
if result["success"]:
|
||||
members_data = result.get("data")
|
||||
if isinstance(members_data, list):
|
||||
filtered_members = [
|
||||
member
|
||||
for member in members_data
|
||||
if not (member.get("is_bot", False) or (isinstance(member.get("email"), str) and "[email protected]" in member.get("email", "").lower()))
|
||||
]
|
||||
result["data"] = filtered_members
|
||||
log.debug(f"Filtered {len(members_data) - len(filtered_members)} bot users from {metadata.name}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Tool metadata for members category
|
||||
MEMBER_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"get_workspace_members": ToolMetadata(
|
||||
name="members_get_workspace_members",
|
||||
description="Get all workspace members (excludes bot users)",
|
||||
sdk_method="get_workspace_members",
|
||||
post_handler=_filter_bots_handler,
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-filled from context)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
],
|
||||
),
|
||||
"get_project_members": ToolMetadata(
|
||||
name="members_get_project_members",
|
||||
description="Get all project members (excludes bot users)",
|
||||
sdk_method="get_project_members",
|
||||
post_handler=_filter_bots_handler,
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project UUID (auto-filled from context if in project chat)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-filled from context)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_member_tools(method_executor, context):
|
||||
"""Return LangChain tools for the members category using method_executor and context."""
|
||||
"""Get all Members API tools."""
|
||||
"""Return LangChain tools for the members category using auto-generation from metadata."""
|
||||
|
||||
@tool
|
||||
async def members_get_workspace_members(workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Get all workspace members (excludes bot users).
|
||||
member_tools = generate_tools_for_category(
|
||||
category="members",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions=MEMBER_TOOL_DEFINITIONS,
|
||||
)
|
||||
|
||||
Args:
|
||||
workspace_slug: Workspace slug (auto-filled from context)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
return member_tools
|
||||
|
||||
result = await method_executor.execute("members", "get_workspace_members", workspace_slug=workspace_slug)
|
||||
|
||||
if result["success"]:
|
||||
# Filter out bot users from the results
|
||||
members_data = result["data"]
|
||||
if isinstance(members_data, list):
|
||||
# Filter out users with is_bot=True and bot email patterns
|
||||
filtered_members = [
|
||||
member
|
||||
for member in members_data
|
||||
if not (
|
||||
# Check is_bot field if present
|
||||
member.get("is_bot", False)
|
||||
# Check for bot email pattern ([email protected])
|
||||
or (isinstance(member.get("email"), str) and "[email protected]" in member.get("email", "").lower())
|
||||
)
|
||||
]
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved workspace members", filtered_members)
|
||||
else:
|
||||
# If data is not a list, return as-is (edge case)
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved workspace members", members_data)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to get workspace members", result["error"])
|
||||
# ============================================================================
|
||||
# OLD MANUAL TOOL DEFINITIONS (COMMENTED OUT - KEPT FOR COMPARISON)
|
||||
# To rollback: uncomment below and comment out the auto-generation code above
|
||||
# ============================================================================
|
||||
|
||||
@tool
|
||||
async def members_get_project_members(
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get all project members (excludes bot users).
|
||||
|
||||
Args:
|
||||
project_id: Project UUID (auto-filled from context if in project chat)
|
||||
workspace_slug: Workspace slug (auto-filled from context)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"members",
|
||||
"get_project_members",
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
# Filter out bot users from the results
|
||||
members_data = result["data"]
|
||||
if isinstance(members_data, list):
|
||||
# Filter out users with is_bot=True and bot email patterns
|
||||
filtered_members = [
|
||||
member
|
||||
for member in members_data
|
||||
if not (
|
||||
# Check is_bot field if present
|
||||
member.get("is_bot", False)
|
||||
# Check for bot email pattern ([email protected])
|
||||
or (isinstance(member.get("email"), str) and "[email protected]" in member.get("email", "").lower())
|
||||
)
|
||||
]
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved project members", filtered_members)
|
||||
else:
|
||||
# If data is not a list, return as-is (edge case)
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved project members", members_data)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to get project members", result["error"])
|
||||
|
||||
return [members_get_workspace_members, members_get_project_members]
|
||||
# [102 lines of old code omitted for brevity]
|
||||
|
||||
@@ -13,348 +13,147 @@
|
||||
Modules API tools for Plane module management operations.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from pi import logger
|
||||
# ============================================================================
|
||||
# MODULES TOOL DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
from .base import PlaneToolBase
|
||||
|
||||
log = logger.getChild(__name__)
|
||||
MODULES_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"create": ToolMetadata(
|
||||
name="modules_create",
|
||||
description="Create a new module.",
|
||||
sdk_method="create_module",
|
||||
returns_entity_type="module",
|
||||
parameters=[
|
||||
ToolParameter(name="name", type="str", required=True, description="Module name (required)"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID (required)", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
ToolParameter(name="description", type="Optional[str]", required=False, description="Module description"),
|
||||
ToolParameter(name="start_date", type="Optional[str]", required=False, description="Start date (YYYY-MM-DD format)"),
|
||||
ToolParameter(name="target_date", type="Optional[str]", required=False, description="Target completion date (YYYY-MM-DD format)"),
|
||||
ToolParameter(
|
||||
name="status", type="Optional[str]", required=False, description="Status: backlog, planned, in-progress, paused, completed, cancelled"
|
||||
),
|
||||
ToolParameter(name="lead", type="Optional[str]", required=False, description="Module lead user ID"),
|
||||
ToolParameter(name="members", type="Optional[list]", required=False, description="List of member user IDs"),
|
||||
ToolParameter(name="external_id", type="Optional[str]", required=False, description="External identifier"),
|
||||
ToolParameter(name="external_source", type="Optional[str]", required=False, description="External source name"),
|
||||
],
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="modules_list",
|
||||
description="List all modules in a project. Requires project_id.",
|
||||
sdk_method="list_modules",
|
||||
parameters=[
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID (required)", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="modules_retrieve",
|
||||
description="Retrieve details of a specific module.",
|
||||
sdk_method="retrieve_module",
|
||||
returns_entity_type="module",
|
||||
parameters=[
|
||||
ToolParameter(name="module_id", type="str", required=True, description="Module ID (required)"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID (required)", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="modules_update",
|
||||
description="Update module details.",
|
||||
sdk_method="update_module",
|
||||
returns_entity_type="module",
|
||||
parameters=[
|
||||
ToolParameter(name="module_id", type="str", required=True, description="Module ID (required)"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID (required)", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
ToolParameter(name="name", type="Optional[str]", required=False, description="New module name"),
|
||||
ToolParameter(name="description", type="Optional[str]", required=False, description="New description"),
|
||||
ToolParameter(name="start_date", type="Optional[str]", required=False, description="New start date (YYYY-MM-DD)"),
|
||||
ToolParameter(name="target_date", type="Optional[str]", required=False, description="New target date (YYYY-MM-DD)"),
|
||||
ToolParameter(name="status", type="Optional[str]", required=False, description="New status"),
|
||||
ToolParameter(name="lead", type="Optional[str]", required=False, description="New lead user ID"),
|
||||
ToolParameter(name="members", type="Optional[list]", required=False, description="New members list"),
|
||||
ToolParameter(name="external_id", type="Optional[str]", required=False, description="External identifier"),
|
||||
ToolParameter(name="external_source", type="Optional[str]", required=False, description="External source name"),
|
||||
],
|
||||
),
|
||||
"archive": ToolMetadata(
|
||||
name="modules_archive",
|
||||
description="Archive a module.",
|
||||
sdk_method="archive_module",
|
||||
parameters=[
|
||||
ToolParameter(name="module_id", type="str", required=True, description="Module ID (required)"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID (required)", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"unarchive": ToolMetadata(
|
||||
name="modules_unarchive",
|
||||
description="Unarchive (restore) a module.",
|
||||
sdk_method="unarchive_module",
|
||||
parameters=[
|
||||
ToolParameter(name="module_id", type="str", required=True, description="Module ID (required)"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID (required)", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"list_archived": ToolMetadata(
|
||||
name="modules_list_archived",
|
||||
description="List archived modules in a project.",
|
||||
sdk_method="list_archived_modules",
|
||||
parameters=[
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID (required)", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"add_work_items": ToolMetadata(
|
||||
name="modules_add_work_items",
|
||||
description="Add work items to a module.",
|
||||
sdk_method="add_module_work_items",
|
||||
parameters=[
|
||||
ToolParameter(name="module_id", type="str", required=True, description="Module ID (required)"),
|
||||
ToolParameter(name="issues", type="list", required=True, description="List of issue IDs to add"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID (required)", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"list_work_items": ToolMetadata(
|
||||
name="modules_list_work_items",
|
||||
description="List work items in a module.",
|
||||
sdk_method="list_module_work_items",
|
||||
parameters=[
|
||||
ToolParameter(name="module_id", type="str", required=True, description="Module ID (required)"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID (required)", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"remove_work_item": ToolMetadata(
|
||||
name="modules_remove_work_item",
|
||||
description="Remove a work item from a module.",
|
||||
sdk_method="remove_module_work_item",
|
||||
parameters=[
|
||||
ToolParameter(name="module_id", type="str", required=True, description="Module ID (required)"),
|
||||
ToolParameter(name="issue_id", type="str", required=True, description="Issue ID to remove (required)"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID (required)", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing module actions
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_module_tools(method_executor, context):
|
||||
"""Return LangChain tools for the modules category using method_executor and context."""
|
||||
|
||||
@tool
|
||||
async def modules_create(
|
||||
name: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
target_date: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
lead: Optional[str] = None,
|
||||
members: Optional[list] = None,
|
||||
external_id: Optional[str] = None,
|
||||
external_source: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new module.
|
||||
|
||||
Args:
|
||||
name: Module name (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
description: Module description
|
||||
start_date: Start date (YYYY-MM-DD format)
|
||||
target_date: Target completion date (YYYY-MM-DD format)
|
||||
status: Module status (Input should be one of 'backlog', 'planned', 'in-progress', 'paused', 'completed' or 'cancelled' )
|
||||
lead: Module lead user ID
|
||||
members: List of member user IDs
|
||||
external_id: External system identifier (optional)
|
||||
external_source: External system source name (optional)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"modules",
|
||||
"create",
|
||||
name=name,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
description=description,
|
||||
start_date=start_date,
|
||||
target_date=target_date,
|
||||
status=status,
|
||||
lead=lead,
|
||||
members=members,
|
||||
external_id=external_id,
|
||||
external_source=external_source,
|
||||
)
|
||||
if result["success"]:
|
||||
return await PlaneToolBase.format_success_payload_with_url(f"Successfully created module '{name}'", result["data"], "module", context)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create module", result["error"])
|
||||
|
||||
@tool
|
||||
async def modules_list(project_id: Optional[str] = None, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""List all modules in a project.
|
||||
|
||||
Args:
|
||||
project_id: Project ID (REQUIRED - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
|
||||
Important: This tool REQUIRES a project_id. If you don't have one, you MUST first:
|
||||
1. Call search_project_by_name to find the project, OR
|
||||
2. Call projects_list to see available projects, OR
|
||||
3. Call ask_for_clarification to ask the user which project to use
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Validate project_id is present
|
||||
if not project_id:
|
||||
return PlaneToolBase.format_error_payload(
|
||||
"Missing required parameter",
|
||||
"project_id is required to list modules. You MUST first call projects_list to see available projects, OR call ask_for_clarification with disambiguation_options populated by calling projects_list to ask which project the user wants.", # noqa: E501
|
||||
)
|
||||
|
||||
result = await method_executor.execute("modules", "list", project_id=project_id, workspace_slug=workspace_slug)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved modules list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list modules", result["error"])
|
||||
|
||||
@tool
|
||||
async def modules_add_work_items(module_id: str, issues: list, project_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Add work items to a module.
|
||||
|
||||
Args:
|
||||
module_id: Module ID (required)
|
||||
issues: List of issue IDs to add to the module (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
"""
|
||||
# Auto-fill workspace_slug from context (hidden from LLM)
|
||||
workspace_slug = context.get("workspace_slug")
|
||||
|
||||
# Auto-fill project_id from context if not provided
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"modules", "add_work_items", module_id=module_id, issues=issues, project_id=project_id, workspace_slug=workspace_slug
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload(f"Successfully added {len(issues)} work items to module", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to add work items to module", result["error"])
|
||||
|
||||
@tool
|
||||
async def modules_retrieve(module_id: str, project_id: Optional[str] = None, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Retrieve details of a specific module.
|
||||
|
||||
Args:
|
||||
module_id: Module ID (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute("modules", "retrieve", module_id=module_id, project_id=project_id, workspace_slug=workspace_slug)
|
||||
if result["success"]:
|
||||
return await PlaneToolBase.format_success_payload_with_url("Successfully retrieved module details", result["data"], "module", context)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to retrieve module", result["error"])
|
||||
|
||||
@tool
|
||||
async def modules_update(
|
||||
module_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
target_date: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
lead: Optional[str] = None,
|
||||
members: Optional[list] = None,
|
||||
external_id: Optional[str] = None,
|
||||
external_source: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update module details.
|
||||
|
||||
Args:
|
||||
module_id: Module ID (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
name: New module name
|
||||
description: New module description
|
||||
start_date: New start date (YYYY-MM-DD format)
|
||||
target_date: New target date (YYYY-MM-DD format)
|
||||
status: New module status
|
||||
lead: New module lead user ID
|
||||
members: New members list (optional)
|
||||
external_id: External system identifier (optional)
|
||||
external_source: External system source name (optional)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Build update request object with non-None values only
|
||||
update_data: Dict[str, Any] = {}
|
||||
if name is not None:
|
||||
update_data["name"] = name
|
||||
if description is not None:
|
||||
update_data["description"] = description
|
||||
if start_date is not None:
|
||||
update_data["start_date"] = start_date
|
||||
if target_date is not None:
|
||||
update_data["target_date"] = target_date
|
||||
if status is not None:
|
||||
update_data["status"] = status
|
||||
if lead is not None:
|
||||
update_data["lead"] = lead
|
||||
if members is not None:
|
||||
update_data["members"] = members
|
||||
if external_id is not None:
|
||||
update_data["external_id"] = external_id
|
||||
if external_source is not None:
|
||||
update_data["external_source"] = external_source
|
||||
|
||||
result = await method_executor.execute(
|
||||
"modules", "update", module_id=module_id, project_id=project_id, workspace_slug=workspace_slug, **update_data
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return await PlaneToolBase.format_success_payload_with_url("Successfully updated module", result["data"], "module", context)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update module", result["error"])
|
||||
|
||||
@tool
|
||||
async def modules_archive(module_id: str, project_id: Optional[str] = None, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Archive a module.
|
||||
|
||||
Args:
|
||||
module_id: Module ID (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute("modules", "archive", module_id=module_id, project_id=project_id, workspace_slug=workspace_slug)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully archived module", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to archive module", result["error"])
|
||||
|
||||
@tool
|
||||
async def modules_unarchive(module_id: str, project_id: Optional[str] = None, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Unarchive a module.
|
||||
|
||||
Args:
|
||||
module_id: Module ID (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute("modules", "unarchive", module_id=module_id, project_id=project_id, workspace_slug=workspace_slug)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully unarchived module", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to unarchive module", result["error"])
|
||||
|
||||
@tool
|
||||
async def modules_list_archived(project_id: Optional[str] = None, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""List archived modules in a project.
|
||||
|
||||
Args:
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute("modules", "list_archived", project_id=project_id, workspace_slug=workspace_slug)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved archived modules list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list archived modules", result["error"])
|
||||
|
||||
@tool
|
||||
async def modules_list_work_items(module_id: str, project_id: Optional[str] = None, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""List work items in a module.
|
||||
|
||||
Args:
|
||||
module_id: Module ID (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"modules", "list_work_items", module_id=module_id, project_id=project_id, workspace_slug=workspace_slug
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved module work items", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list module work items", result["error"])
|
||||
|
||||
@tool
|
||||
async def modules_remove_work_item(
|
||||
module_id: str, issue_id: str, project_id: Optional[str] = None, workspace_slug: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Remove a work item from a module.
|
||||
|
||||
Args:
|
||||
module_id: Module ID (required)
|
||||
issue_id: Issue ID to remove from the module (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"modules", "remove_work_item", issue_id=issue_id, module_id=module_id, project_id=project_id, workspace_slug=workspace_slug
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully removed work item from module", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to remove work item from module", result["error"])
|
||||
|
||||
return [
|
||||
modules_create,
|
||||
modules_list,
|
||||
modules_add_work_items,
|
||||
modules_retrieve,
|
||||
modules_update,
|
||||
modules_archive,
|
||||
modules_unarchive,
|
||||
modules_list_archived,
|
||||
modules_list_work_items,
|
||||
modules_remove_work_item,
|
||||
]
|
||||
"""Return LangChain tools for the modules category using auto-generation from metadata."""
|
||||
return generate_tools_for_category("modules", method_executor, context, MODULES_TOOL_DEFINITIONS)
|
||||
|
||||
@@ -10,204 +10,499 @@
|
||||
# NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
|
||||
"""
|
||||
Pages API tools for Plane documentation and page management operations.
|
||||
Pages category - Auto-generated tools from metadata.
|
||||
|
||||
This module uses the centralized tool generation system.
|
||||
Old manual definitions kept below for comparison/rollback safety.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from .base import PlaneToolBase
|
||||
# ============================================================================
|
||||
# PAGES-SPECIFIC HANDLER FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing pages actions
|
||||
|
||||
async def _pages_pre_handler(
|
||||
metadata: ToolMetadata,
|
||||
kwargs: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
category: str,
|
||||
method_key: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Pre-processing handler for pages tools.
|
||||
|
||||
Handles:
|
||||
- Default description_html to name if not provided (API requires this field)
|
||||
"""
|
||||
# Default description_html to name if not provided
|
||||
if "description_html" in [p.name for p in metadata.parameters]:
|
||||
if not kwargs.get("description_html"):
|
||||
kwargs["description_html"] = kwargs.get("name", "")
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
async def _pages_post_handler(
|
||||
metadata: ToolMetadata,
|
||||
result: Dict[str, Any],
|
||||
kwargs: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
method_executor: Any,
|
||||
category: str,
|
||||
method_key: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Post-processing handler for pages tools.
|
||||
|
||||
Handles:
|
||||
- Inject project field into response data for URL construction
|
||||
(API response doesn't include project field, but URL builder needs it)
|
||||
"""
|
||||
if result["success"] and "project_id" in kwargs and kwargs.get("project_id"):
|
||||
data = result.get("data", {})
|
||||
# Using "project" not "project_id" because extract_entity_from_api_response looks for "project"
|
||||
data["project"] = kwargs["project_id"]
|
||||
result["data"] = data
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# PAGES TOOL DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
PAGE_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"create_project_page": ToolMetadata(
|
||||
name="pages_create_project_page",
|
||||
description="Create a new page in a project",
|
||||
sdk_method="create_project_page",
|
||||
returns_entity_type="page",
|
||||
pre_handler=_pages_pre_handler,
|
||||
post_handler=_pages_post_handler,
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="name",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Page title (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (required - provide from conversation context or previous actions)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (provide if known, otherwise auto-detected)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description_html",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Page content in HTML format. If the user asks for 'details about X', "
|
||||
"generate relevant content. Use empty string if no content is needed. (optional, defaults to page name)",
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="access",
|
||||
type="Optional[int]",
|
||||
required=False,
|
||||
description="Access level - 0 for public, 1 for private (optional, if not specified the API will use its default)",
|
||||
property_transform="name",
|
||||
),
|
||||
ToolParameter(
|
||||
name="color",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Page color in hex format like #3B82F6 (optional)",
|
||||
property_transform="name",
|
||||
),
|
||||
ToolParameter(
|
||||
name="logo_props",
|
||||
type="Optional[dict]",
|
||||
required=False,
|
||||
description="Logo properties dict with keys like name, color, type (optional)",
|
||||
property_transform="name",
|
||||
),
|
||||
],
|
||||
),
|
||||
"create_workspace_page": ToolMetadata(
|
||||
name="pages_create_workspace_page",
|
||||
description="Create a new page in the workspace",
|
||||
sdk_method="create_workspace_page",
|
||||
returns_entity_type="page",
|
||||
pre_handler=_pages_pre_handler,
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="name",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Page title (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (provide if known, otherwise auto-detected)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description_html",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Page content in HTML format. If the user asks for 'details about X', "
|
||||
"generate relevant content. Use empty string if no content is needed. (optional, defaults to page name)",
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="access",
|
||||
type="Optional[int]",
|
||||
required=False,
|
||||
description="Access level - 0 for public, 1 for private (optional)",
|
||||
property_transform="name",
|
||||
),
|
||||
ToolParameter(
|
||||
name="color",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Page color in hex format like #10B981 (optional)",
|
||||
property_transform="name",
|
||||
),
|
||||
ToolParameter(
|
||||
name="logo_props",
|
||||
type="Optional[dict]",
|
||||
required=False,
|
||||
description="Logo properties dict with keys like name, color, type (optional)",
|
||||
property_transform="name",
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_page_tools(method_executor, context):
|
||||
"""Return LangChain tools for the pages category using method_executor and context."""
|
||||
"""Return LangChain tools for the pages category using auto-generation from metadata.
|
||||
|
||||
Context-aware tool selection:
|
||||
- In project chat: only project pages (workspace pages are not accessible in project UI)
|
||||
- In workspace/global chat: unified meta-tool that routes based on project_id
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
|
||||
from .base import PlaneToolBase
|
||||
|
||||
# Check if we're in a project-specific context
|
||||
is_project_chat = context.get("is_project_chat", False)
|
||||
|
||||
@tool
|
||||
async def pages_create_project_page(
|
||||
name: str,
|
||||
project_id: Optional[str] = None,
|
||||
description_html: Optional[str] = None,
|
||||
access: Optional[int] = None,
|
||||
color: Optional[str] = None,
|
||||
logo_props: Optional[dict] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new page in a project.
|
||||
|
||||
Args:
|
||||
name: Page title (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
description_html: Page content in HTML format. If the user asks for "details about X", generate relevant content. Use empty string if no content is needed. (optional, defaults to empty string)
|
||||
access: Access level - 0 for public, 1 for private (optional, if not specified the API will use its default)
|
||||
color: Page color in hex format like #3B82F6 (optional)
|
||||
logo_props: Logo properties dict with keys like name, color, type (optional)
|
||||
|
||||
Returns:
|
||||
Success message with page details and URL information
|
||||
""" # noqa E501
|
||||
# Auto-fill workspace_slug from context (hidden from LLM)
|
||||
workspace_slug = context.get("workspace_slug")
|
||||
|
||||
# Auto-fill project_id from context if not provided
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# CRITICAL: Provide default empty string if description_html is None
|
||||
# The Plane API requires this field even if empty
|
||||
if not description_html:
|
||||
description_html = name
|
||||
|
||||
result = await method_executor.execute(
|
||||
"pages",
|
||||
"create_project_page",
|
||||
name=name,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
description_html=description_html,
|
||||
access=access,
|
||||
color=color,
|
||||
logo_props=logo_props,
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
data = result.get("data", {})
|
||||
# CRITICAL: Inject project into response data for URL construction
|
||||
# The API response doesn't include project field, so we need to add it
|
||||
# Using "project" not "project_id" because extract_entity_from_api_response looks for "project"
|
||||
if project_id:
|
||||
data["project"] = project_id
|
||||
return await PlaneToolBase.format_success_payload_with_url(f"Project page '{name}' created successfully", data, "page", context)
|
||||
else:
|
||||
error_msg = result.get("error", "Unknown error occurred")
|
||||
return PlaneToolBase.format_error_payload("Failed to create project page", error_msg)
|
||||
|
||||
@tool
|
||||
async def pages_create_workspace_page(
|
||||
name: str,
|
||||
description_html: Optional[str] = None,
|
||||
access: Optional[int] = None,
|
||||
color: Optional[str] = None,
|
||||
logo_props: Optional[dict] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new page in the workspace.
|
||||
|
||||
Args:
|
||||
name: Page title (required)
|
||||
description_html: Page content in HTML format. If the user asks for "details about X", generate relevant content. Use empty string if no content is needed. (optional, defaults to empty string)
|
||||
access: Access level - 0 for public, 1 for private (optional, if not specified the API will use its default)
|
||||
color: Page color in hex format like #10B981 (optional)
|
||||
logo_props: Logo properties dict with keys like name, color, type (optional)
|
||||
|
||||
Returns:
|
||||
Success message with page details and URL information
|
||||
""" # noqa E501
|
||||
# Auto-fill workspace_slug from context (hidden from LLM)
|
||||
workspace_slug = context.get("workspace_slug")
|
||||
|
||||
# CRITICAL: Provide default empty string if description_html is None
|
||||
# The Plane API requires this field even if empty
|
||||
if not description_html:
|
||||
description_html = name
|
||||
|
||||
result = await method_executor.execute(
|
||||
"pages",
|
||||
"create_workspace_page",
|
||||
name=name,
|
||||
workspace_slug=workspace_slug,
|
||||
description_html=description_html,
|
||||
access=access,
|
||||
color=color,
|
||||
logo_props=logo_props,
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
data = result.get("data", {})
|
||||
return await PlaneToolBase.format_success_payload_with_url(f"Workspace page '{name}' created successfully", data, "page", context)
|
||||
else:
|
||||
error_msg = result.get("error", "Unknown error occurred")
|
||||
return PlaneToolBase.format_error_payload("Failed to create workspace page", error_msg)
|
||||
|
||||
# In workspace/global context, expose a single tool that forces clarification for scope
|
||||
@tool
|
||||
async def pages_create_page(
|
||||
name: str,
|
||||
project_id: str,
|
||||
description_html: Optional[str] = None,
|
||||
access: Optional[int] = None,
|
||||
color: Optional[str] = None,
|
||||
logo_props: Optional[dict] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new page either in a project or at the workspace level.
|
||||
|
||||
Args:
|
||||
name: Page title (required)
|
||||
project_id: Project UUID where the page should be created. If user hasn't specified a project, leave this empty or use 'NEEDS_CLARIFICATION' to trigger project selection. (required)
|
||||
description_html: Page content in HTML format. If the user asks for "details about X", generate relevant content. Use empty string if no content is needed. (optional, defaults to empty string)
|
||||
access: Access level - 0 for public, 1 for private (optional)
|
||||
color: Page color in hex format (optional)
|
||||
logo_props: Logo properties dict with keys like name, color, type (optional)
|
||||
""" # noqa E501
|
||||
workspace_slug = context.get("workspace_slug")
|
||||
|
||||
# CRITICAL: Provide default empty string if description_html is None
|
||||
# The Plane API requires this field even if empty
|
||||
if not description_html:
|
||||
description_html = name
|
||||
|
||||
if project_id == "__workspace_scope__":
|
||||
# Create at workspace level
|
||||
result = await method_executor.execute(
|
||||
"pages",
|
||||
"create_workspace_page",
|
||||
name=name,
|
||||
workspace_slug=workspace_slug,
|
||||
description_html=description_html,
|
||||
access=access,
|
||||
color=color,
|
||||
logo_props=logo_props,
|
||||
)
|
||||
scope_label = "Workspace"
|
||||
else:
|
||||
# Create in a specific project
|
||||
result = await method_executor.execute(
|
||||
"pages",
|
||||
"create_project_page",
|
||||
name=name,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
description_html=description_html,
|
||||
access=access,
|
||||
color=color,
|
||||
logo_props=logo_props,
|
||||
)
|
||||
scope_label = "Project"
|
||||
|
||||
if result.get("success"):
|
||||
data = result.get("data", {})
|
||||
# CRITICAL: Inject project into response data for URL construction
|
||||
# The API response doesn't include project field, so we need to add it
|
||||
# Using "project" not "project_id" because extract_entity_from_api_response looks for "project"
|
||||
# for the URL builder to construct the correct project page URL
|
||||
if scope_label == "Project" and project_id != "__workspace_scope__":
|
||||
data["project"] = project_id
|
||||
return await PlaneToolBase.format_success_payload_with_url(f"{scope_label} page '{name}' created successfully", data, "page", context)
|
||||
else:
|
||||
error_msg = result.get("error", "Unknown error occurred")
|
||||
return PlaneToolBase.format_error_payload(f"Failed to create {scope_label.lower()} page", error_msg)
|
||||
|
||||
# Return tools based on context:
|
||||
# - In project chat: only project pages (workspace pages are not accessible in project UI)
|
||||
# - In workspace/global chat: consolidated tool that requires scope selection via clarification
|
||||
|
||||
if is_project_chat:
|
||||
return [pages_create_project_page]
|
||||
# Return only project page tool
|
||||
tools = generate_tools_for_category(
|
||||
category="pages",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions={"create_project_page": PAGE_TOOL_DEFINITIONS["create_project_page"]},
|
||||
)
|
||||
else:
|
||||
return [pages_create_page]
|
||||
# In workspace/global context, create a meta-tool that routes to the correct SDK method
|
||||
@tool
|
||||
async def pages_create_page(
|
||||
name: str,
|
||||
project_id: str,
|
||||
description_html: Optional[str] = None,
|
||||
access: Optional[int] = None,
|
||||
color: Optional[str] = None,
|
||||
logo_props: Optional[dict] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new page either in a project or at the workspace level.
|
||||
|
||||
Args:
|
||||
name: Page title (required)
|
||||
project_id: Project UUID where the page should be created. If user hasn't specified a project,
|
||||
leave this empty or use 'NEEDS_CLARIFICATION' to trigger project selection. (required)
|
||||
description_html: Page content in HTML format. If the user asks for 'details about X',
|
||||
generate relevant content. Use empty string if no content is needed.
|
||||
access: Access level - 0 for public, 1 for private (optional)
|
||||
color: Page color in hex format (optional)
|
||||
logo_props: Logo properties dict with keys like name, color, type (optional)
|
||||
"""
|
||||
workspace_slug = context.get("workspace_slug")
|
||||
|
||||
# CRITICAL: Provide default empty string if description_html is None
|
||||
# The Plane API requires this field even if empty
|
||||
if not description_html:
|
||||
description_html = name
|
||||
|
||||
if project_id == "__workspace_scope__":
|
||||
# Create at workspace level (Wiki)
|
||||
result = await method_executor.execute(
|
||||
"pages",
|
||||
"create_workspace_page",
|
||||
name=name,
|
||||
workspace_slug=workspace_slug,
|
||||
description_html=description_html,
|
||||
access=access,
|
||||
color=color,
|
||||
logo_props=logo_props,
|
||||
)
|
||||
scope_label = "Workspace"
|
||||
else:
|
||||
# Create in a specific project (Plane project pages)
|
||||
result = await method_executor.execute(
|
||||
"pages",
|
||||
"create_project_page",
|
||||
name=name,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
description_html=description_html,
|
||||
access=access,
|
||||
color=color,
|
||||
logo_props=logo_props,
|
||||
)
|
||||
scope_label = "Project"
|
||||
|
||||
if result.get("success"):
|
||||
data = result.get("data", {})
|
||||
# CRITICAL: Inject project into response data for URL construction
|
||||
# The API response doesn't include project field, so we need to add it
|
||||
# Using "project" not "project_id" because extract_entity_from_api_response looks for "project"
|
||||
# for the URL builder to construct the correct project page URL
|
||||
if scope_label == "Project" and project_id != "__workspace_scope__":
|
||||
data["project"] = project_id
|
||||
return await PlaneToolBase.format_success_payload_with_url(f"{scope_label} page '{name}' created successfully", data, "page", context)
|
||||
else:
|
||||
error_msg = result.get("error", "Unknown error occurred")
|
||||
return PlaneToolBase.format_error_payload(f"Failed to create {scope_label.lower()} page", error_msg)
|
||||
|
||||
tools = [pages_create_page] # type: ignore[list-item]
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# OLD MANUAL TOOL DEFINITIONS (COMMENTED FOR REFERENCE)
|
||||
# ============================================================================
|
||||
# """
|
||||
# Pages API tools for Plane documentation and page management operations.
|
||||
# """
|
||||
#
|
||||
# from typing import Any
|
||||
# from typing import Dict
|
||||
# from typing import Optional
|
||||
#
|
||||
# from langchain_core.tools import tool
|
||||
#
|
||||
# from .base import PlaneToolBase
|
||||
#
|
||||
# # Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# # Returns LangChain tools implementing pages actions
|
||||
#
|
||||
#
|
||||
# def get_page_tools(method_executor, context):
|
||||
# """Return LangChain tools for the pages category using method_executor and context."""
|
||||
#
|
||||
# # Check if we're in a project-specific context
|
||||
# is_project_chat = context.get("is_project_chat", False)
|
||||
#
|
||||
# @tool
|
||||
# async def pages_create_project_page(
|
||||
# name: str,
|
||||
# project_id: Optional[str] = None,
|
||||
# description_html: Optional[str] = None,
|
||||
# access: Optional[int] = None,
|
||||
# color: Optional[str] = None,
|
||||
# logo_props: Optional[dict] = None,
|
||||
# ) -> Dict[str, Any]:
|
||||
# """Create a new page in a project.
|
||||
#
|
||||
# Args:
|
||||
# name: Page title (required)
|
||||
# project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
# description_html: Page content in HTML format. If the user asks for "details about X",
|
||||
# generate relevant content. Use empty string if no content is needed.
|
||||
# access: Access level - 0 for public, 1 for private (optional, if not specified the API will use its default)
|
||||
# color: Page color in hex format like #3B82F6 (optional)
|
||||
# logo_props: Logo properties dict with keys like name, color, type (optional)
|
||||
#
|
||||
# Returns:
|
||||
# Success message with page details and URL information
|
||||
# """ # noqa E501
|
||||
# # Auto-fill workspace_slug from context (hidden from LLM)
|
||||
# workspace_slug = context.get("workspace_slug")
|
||||
#
|
||||
# # Auto-fill project_id from context if not provided
|
||||
# if project_id is None and "project_id" in context:
|
||||
# project_id = context["project_id"]
|
||||
#
|
||||
# # CRITICAL: Provide default empty string if description_html is None
|
||||
# # The Plane API requires this field even if empty
|
||||
# if not description_html:
|
||||
# description_html = name
|
||||
#
|
||||
# result = await method_executor.execute(
|
||||
# "pages",
|
||||
# "create_project_page",
|
||||
# name=name,
|
||||
# project_id=project_id,
|
||||
# workspace_slug=workspace_slug,
|
||||
# description_html=description_html,
|
||||
# access=access,
|
||||
# color=color,
|
||||
# logo_props=logo_props,
|
||||
# )
|
||||
#
|
||||
# if result.get("success"):
|
||||
# data = result.get("data", {})
|
||||
# # CRITICAL: Inject project into response data for URL construction
|
||||
# # The API response doesn't include project field, so we need to add it
|
||||
# # Using "project" not "project_id" because extract_entity_from_api_response looks for "project"
|
||||
# if project_id:
|
||||
# data["project"] = project_id
|
||||
# return await PlaneToolBase.format_success_payload_with_url(f"Project page '{name}' created successfully", data, "page", context)
|
||||
# else:
|
||||
# error_msg = result.get("error", "Unknown error occurred")
|
||||
# return PlaneToolBase.format_error_payload("Failed to create project page", error_msg)
|
||||
#
|
||||
# @tool
|
||||
# async def pages_create_workspace_page(
|
||||
# name: str,
|
||||
# description_html: Optional[str] = None,
|
||||
# access: Optional[int] = None,
|
||||
# color: Optional[str] = None,
|
||||
# logo_props: Optional[dict] = None,
|
||||
# ) -> Dict[str, Any]:
|
||||
# """Create a new page in the workspace.
|
||||
#
|
||||
# Args:
|
||||
# name: Page title (required)
|
||||
# description_html: Page content in HTML format. If the user asks for "details about X",
|
||||
# generate relevant content. Use empty string if no content is needed.
|
||||
# access: Access level - 0 for public, 1 for private (optional, if not specified the API will use its default)
|
||||
# color: Page color in hex format like #10B981 (optional)
|
||||
# logo_props: Logo properties dict with keys like name, color, type (optional)
|
||||
#
|
||||
# Returns:
|
||||
# Success message with page details and URL information
|
||||
# """ # noqa E501
|
||||
# # Auto-fill workspace_slug from context (hidden from LLM)
|
||||
# workspace_slug = context.get("workspace_slug")
|
||||
#
|
||||
# # CRITICAL: Provide default empty string if description_html is None
|
||||
# # The Plane API requires this field even if empty
|
||||
# if not description_html:
|
||||
# description_html = name
|
||||
#
|
||||
# result = await method_executor.execute(
|
||||
# "pages",
|
||||
# "create_workspace_page",
|
||||
# name=name,
|
||||
# workspace_slug=workspace_slug,
|
||||
# description_html=description_html,
|
||||
# access=access,
|
||||
# color=color,
|
||||
# logo_props=logo_props,
|
||||
# )
|
||||
#
|
||||
# if result.get("success"):
|
||||
# data = result.get("data", {})
|
||||
# return await PlaneToolBase.format_success_payload_with_url(f"Workspace page '{name}' created successfully", data, "page", context)
|
||||
# else:
|
||||
# error_msg = result.get("error", "Unknown error occurred")
|
||||
# return PlaneToolBase.format_error_payload("Failed to create workspace page", error_msg)
|
||||
#
|
||||
# # In workspace/global context, expose a single tool that forces clarification for scope
|
||||
# @tool
|
||||
# async def pages_create_page(
|
||||
# name: str,
|
||||
# project_id: str,
|
||||
# description_html: Optional[str] = None,
|
||||
# access: Optional[int] = None,
|
||||
# color: Optional[str] = None,
|
||||
# logo_props: Optional[dict] = None,
|
||||
# ) -> Dict[str, Any]:
|
||||
# """Create a new page either in a project or at the workspace level.
|
||||
#
|
||||
# Args:
|
||||
# name: Page title (required)
|
||||
# project_id: Project UUID where the page should be created. If user hasn't specified a project,
|
||||
# leave this empty or use 'NEEDS_CLARIFICATION' to trigger project selection. (required)
|
||||
# description_html: Page content in HTML format. If the user asks for "details about X",
|
||||
# generate relevant content. Use empty string if no content is needed.
|
||||
# access: Access level - 0 for public, 1 for private (optional)
|
||||
# color: Page color in hex format (optional)
|
||||
# logo_props: Logo properties dict with keys like name, color, type (optional)
|
||||
# """ # noqa E501
|
||||
# workspace_slug = context.get("workspace_slug")
|
||||
#
|
||||
# # CRITICAL: Provide default empty string if description_html is None
|
||||
# # The Plane API requires this field even if empty
|
||||
# if not description_html:
|
||||
# description_html = name
|
||||
#
|
||||
# if project_id == "__workspace_scope__":
|
||||
# # Create at workspace level
|
||||
# result = await method_executor.execute(
|
||||
# "pages",
|
||||
# "create_workspace_page",
|
||||
# name=name,
|
||||
# workspace_slug=workspace_slug,
|
||||
# description_html=description_html,
|
||||
# access=access,
|
||||
# color=color,
|
||||
# logo_props=logo_props,
|
||||
# )
|
||||
# scope_label = "Workspace"
|
||||
# else:
|
||||
# # Create in a specific project
|
||||
# result = await method_executor.execute(
|
||||
# "pages",
|
||||
# "create_project_page",
|
||||
# name=name,
|
||||
# project_id=project_id,
|
||||
# workspace_slug=workspace_slug,
|
||||
# description_html=description_html,
|
||||
# access=access,
|
||||
# color=color,
|
||||
# logo_props=logo_props,
|
||||
# )
|
||||
# scope_label = "Project"
|
||||
#
|
||||
# if result.get("success"):
|
||||
# data = result.get("data", {})
|
||||
# # CRITICAL: Inject project into response data for URL construction
|
||||
# # The API response doesn't include project field, so we need to add it
|
||||
# # Using "project" not "project_id" because extract_entity_from_api_response looks for "project"
|
||||
# # for the URL builder to construct the correct project page URL
|
||||
# if scope_label == "Project" and project_id != "__workspace_scope__":
|
||||
# data["project"] = project_id
|
||||
# return await PlaneToolBase.format_success_payload_with_url(f"{scope_label} page '{name}' created successfully", data, "page", context)
|
||||
# else:
|
||||
# error_msg = result.get("error", "Unknown error occurred")
|
||||
# return PlaneToolBase.format_error_payload(f"Failed to create {scope_label.lower()} page", error_msg)
|
||||
#
|
||||
# # Return tools based on context:
|
||||
# # - In project chat: only project pages (workspace pages are not accessible in project UI)
|
||||
# # - In workspace/global chat: consolidated tool that requires scope selection via clarification
|
||||
#
|
||||
# if is_project_chat:
|
||||
# return [pages_create_project_page]
|
||||
# else:
|
||||
# return [pages_create_page]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,498 +13,214 @@
|
||||
Properties API tools for Plane custom property management.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from langchain_core.tools import tool
|
||||
# ============================================================================
|
||||
# TOOL METADATA DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
from .base import PlaneToolBase
|
||||
PROPERTIES_TOOL_DEFINITIONS = {
|
||||
# ==========================================================================
|
||||
# Core Property CRUD
|
||||
# ==========================================================================
|
||||
"create": ToolMetadata(
|
||||
name="properties_create",
|
||||
description="Create a new work item property (custom field) for a specific work item type.",
|
||||
sdk_method="create_property",
|
||||
parameters=[
|
||||
ToolParameter(name="display_name", type="str", required=True, description="Display name for the property (required)"),
|
||||
ToolParameter(
|
||||
name="property_type",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Type of property: TEXT, DATETIME, DECIMAL, BOOLEAN, OPTION, RELATION, URL, EMAIL, FILE",
|
||||
),
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Work item type ID this property belongs to (required)"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
ToolParameter(
|
||||
name="relation_type",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Relation type for RELATION properties: ONE_TO_ONE, ONE_TO_MANY, MANY_TO_ONE, MANY_TO_MANY",
|
||||
),
|
||||
ToolParameter(name="description", type="Optional[str]", required=False, description="Description of the property"),
|
||||
ToolParameter(name="is_required", type="Optional[bool]", required=False, description="Whether this property is required for work items"),
|
||||
ToolParameter(name="default_value", type="Optional[list]", required=False, description="List of default values for the property"),
|
||||
ToolParameter(
|
||||
name="settings",
|
||||
type="Optional[dict]",
|
||||
required=False,
|
||||
description="Property settings (TextAttributeSettings for TEXT, DateAttributeSettings for DATETIME)",
|
||||
),
|
||||
ToolParameter(name="is_active", type="Optional[bool]", required=False, description="Whether the property is active"),
|
||||
ToolParameter(name="is_multi", type="Optional[bool]", required=False, description="Whether this property supports multiple values"),
|
||||
ToolParameter(name="validation_rules", type="Optional[dict]", required=False, description="Validation rules for the property"),
|
||||
ToolParameter(name="options", type="Optional[list]", required=False, description="List of options for OPTION type properties"),
|
||||
ToolParameter(
|
||||
name="external_source", type="Optional[str]", required=False, description="External system source identifier (e.g., 'github', 'jira')"
|
||||
),
|
||||
ToolParameter(name="external_id", type="Optional[str]", required=False, description="External system's identifier for this property"),
|
||||
],
|
||||
returns_entity_type="property",
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="properties_list",
|
||||
description="List all work item properties (custom fields) for a specific work item type.",
|
||||
sdk_method="list_properties",
|
||||
parameters=[
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Work item type ID to list properties for"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="properties_retrieve",
|
||||
description="Get a single work item property by ID.",
|
||||
sdk_method="retrieve_property",
|
||||
parameters=[
|
||||
ToolParameter(name="property_id", type="str", required=True, description="Property ID to retrieve"),
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Work item type ID this property belongs to"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="properties_update",
|
||||
description="Update work item property details.",
|
||||
sdk_method="update_property",
|
||||
parameters=[
|
||||
ToolParameter(name="property_id", type="str", required=True, description="Property ID to update"),
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Work item type ID this property belongs to"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
ToolParameter(name="display_name", type="Optional[str]", required=False, description="Display name for the property"),
|
||||
ToolParameter(
|
||||
name="property_type",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Type of property: TEXT, DATETIME, DECIMAL, BOOLEAN, OPTION, RELATION, URL, EMAIL, FILE",
|
||||
),
|
||||
ToolParameter(name="relation_type", type="Optional[str]", required=False, description="Relation type for RELATION properties"),
|
||||
ToolParameter(name="description", type="Optional[str]", required=False, description="Description of the property"),
|
||||
ToolParameter(name="is_required", type="Optional[bool]", required=False, description="Whether this property is required"),
|
||||
ToolParameter(name="default_value", type="Optional[list]", required=False, description="List of default values"),
|
||||
ToolParameter(name="settings", type="Optional[dict]", required=False, description="Property settings"),
|
||||
ToolParameter(name="is_active", type="Optional[bool]", required=False, description="Whether the property is active"),
|
||||
ToolParameter(name="is_multi", type="Optional[bool]", required=False, description="Whether this property supports multiple values"),
|
||||
ToolParameter(name="validation_rules", type="Optional[dict]", required=False, description="Validation rules"),
|
||||
ToolParameter(name="external_source", type="Optional[str]", required=False, description="External system source identifier"),
|
||||
ToolParameter(name="external_id", type="Optional[str]", required=False, description="External system's identifier"),
|
||||
],
|
||||
returns_entity_type="property",
|
||||
),
|
||||
"delete": ToolMetadata(
|
||||
name="properties_delete",
|
||||
description="Delete a work item property.",
|
||||
sdk_method="delete_property",
|
||||
parameters=[
|
||||
ToolParameter(name="property_id", type="str", required=True, description="Property ID to delete"),
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Work item type ID this property belongs to"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
returns_entity_type="property",
|
||||
),
|
||||
# ==========================================================================
|
||||
# Property Options (for OPTION type properties)
|
||||
# ==========================================================================
|
||||
"create_option": ToolMetadata(
|
||||
name="properties_create_option",
|
||||
description="Create a new option for an OPTION type property.",
|
||||
sdk_method="create_property_option",
|
||||
parameters=[
|
||||
ToolParameter(name="property_id", type="str", required=True, description="Property ID to add option to"),
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Work item type ID"),
|
||||
ToolParameter(name="name", type="str", required=True, description="Option display name"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
ToolParameter(name="value", type="Optional[str]", required=False, description="Option value (defaults to name if not provided)"),
|
||||
],
|
||||
returns_entity_type="property_option",
|
||||
),
|
||||
"list_options": ToolMetadata(
|
||||
name="properties_list_options",
|
||||
description="List all options for an OPTION type property.",
|
||||
sdk_method="list_property_options",
|
||||
parameters=[
|
||||
ToolParameter(name="property_id", type="str", required=True, description="Property ID to list options for"),
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Work item type ID"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"retrieve_option": ToolMetadata(
|
||||
name="properties_retrieve_option",
|
||||
description="Get a property option by ID.",
|
||||
sdk_method="retrieve_property_option",
|
||||
parameters=[
|
||||
ToolParameter(name="property_id", type="str", required=True, description="Property ID"),
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Work item type ID"),
|
||||
ToolParameter(name="option_id", type="str", required=True, description="Option ID to retrieve"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
),
|
||||
"update_option": ToolMetadata(
|
||||
name="properties_update_option",
|
||||
description="Update a property option.",
|
||||
sdk_method="update_property_option",
|
||||
parameters=[
|
||||
ToolParameter(name="property_id", type="str", required=True, description="Property ID"),
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Work item type ID"),
|
||||
ToolParameter(name="option_id", type="str", required=True, description="Option ID to update"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
ToolParameter(name="name", type="Optional[str]", required=False, description="Option display name"),
|
||||
ToolParameter(name="value", type="Optional[str]", required=False, description="Option value"),
|
||||
],
|
||||
returns_entity_type="property_option",
|
||||
),
|
||||
"delete_option": ToolMetadata(
|
||||
name="properties_delete_option",
|
||||
description="Delete a property option.",
|
||||
sdk_method="delete_property_option",
|
||||
parameters=[
|
||||
ToolParameter(name="property_id", type="str", required=True, description="Property ID"),
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Work item type ID"),
|
||||
ToolParameter(name="option_id", type="str", required=True, description="Option ID to delete"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
returns_entity_type="property_option",
|
||||
),
|
||||
# ==========================================================================
|
||||
# Property Values (values set on work items)
|
||||
# ==========================================================================
|
||||
"create_value": ToolMetadata(
|
||||
name="properties_create_value",
|
||||
description='Set a property value on a work item. For simple types (TEXT), provide the value directly. For single-select (OPTION), provide the Option ID. For multi-select, provide a JSON-formatted list of Option IDs (e.g., \'["uuid1", "uuid2"]\').', # noqa: E501
|
||||
sdk_method="create_property_value",
|
||||
parameters=[
|
||||
ToolParameter(name="property_id", type="str", required=True, description="Property ID"),
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Work item type ID"),
|
||||
ToolParameter(name="issue_id", type="str", required=True, description="Work item ID to set value on"),
|
||||
ToolParameter(name="value", type="str", required=True, description="Property value (use Option ID for OPTION types)"),
|
||||
ToolParameter(name="project_id", type="Optional[str]", required=False, description="Project ID", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", type="Optional[str]", required=False, description="Workspace slug", auto_fill_from_context=True),
|
||||
],
|
||||
returns_entity_type="property_value",
|
||||
),
|
||||
}
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing property actions
|
||||
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_property_tools(method_executor, context):
|
||||
"""Return LangChain tools for the properties category using method_executor and context."""
|
||||
"""Get all Properties API tools."""
|
||||
|
||||
@tool
|
||||
async def properties_create(
|
||||
display_name: str,
|
||||
property_type: str,
|
||||
type_id: str,
|
||||
relation_type: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
is_required: Optional[bool] = None,
|
||||
default_value: Optional[list] = None,
|
||||
settings: Optional[dict] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
is_multi: Optional[bool] = None,
|
||||
validation_rules: Optional[dict] = None,
|
||||
external_source: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new work item property.
|
||||
|
||||
Args:
|
||||
display_name: Display name for the property (required)
|
||||
property_type: Type of property - TEXT, DATETIME, DECIMAL, BOOLEAN, OPTION, RELATION, URL, EMAIL, FILE (required)
|
||||
type_id: Work item type ID this property belongs to (required)
|
||||
relation_type: Relation type for RELATION properties - ONE_TO_ONE, ONE_TO_MANY, MANY_TO_ONE, MANY_TO_MANY
|
||||
description: Description of the property
|
||||
is_required: Whether this property is required for work items
|
||||
default_value: List of default values for the property
|
||||
settings: Property settings (TextAttributeSettings for TEXT, DateAttributeSettings for DATETIME)
|
||||
is_active: Whether the property is active
|
||||
is_multi: Whether this property supports multiple values
|
||||
validation_rules: Validation rules for the property
|
||||
external_source: External system source identifier (e.g., 'github', 'jira')
|
||||
external_id: External system's identifier for this property
|
||||
project_id: Project ID (auto-filled from context if not provided)
|
||||
workspace_slug: Workspace slug (auto-filled from context if not provided)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"properties",
|
||||
"create",
|
||||
display_name=display_name,
|
||||
property_type=property_type,
|
||||
type_id=type_id,
|
||||
relation_type=relation_type,
|
||||
description=description,
|
||||
is_required=is_required,
|
||||
default_value=default_value,
|
||||
settings=settings,
|
||||
is_active=is_active,
|
||||
is_multi=is_multi,
|
||||
validation_rules=validation_rules,
|
||||
external_source=external_source,
|
||||
external_id=external_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload(f"Successfully created property '{display_name}'", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create property", result["error"])
|
||||
|
||||
@tool
|
||||
async def properties_list(
|
||||
type_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List all work item properties for a specific work item type.
|
||||
|
||||
Args:
|
||||
type_id: Work item type ID to list properties for (required)
|
||||
project_id: Project ID (auto-filled from context if not provided)
|
||||
workspace_slug: Workspace slug (auto-filled from context if not provided)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"properties",
|
||||
"list",
|
||||
type_id=type_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved properties list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list properties", result["error"])
|
||||
|
||||
@tool
|
||||
async def properties_retrieve(
|
||||
property_id: str,
|
||||
type_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get a single work item property by ID.
|
||||
|
||||
Args:
|
||||
property_id: Property ID to retrieve (required)
|
||||
type_id: Work item type ID this property belongs to (required)
|
||||
project_id: Project ID (auto-filled from context if not provided)
|
||||
workspace_slug: Workspace slug (auto-filled from context if not provided)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"properties",
|
||||
"retrieve",
|
||||
property_id=property_id,
|
||||
type_id=type_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved property", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to retrieve property", result["error"])
|
||||
|
||||
@tool
|
||||
async def properties_update(
|
||||
property_id: str,
|
||||
type_id: str,
|
||||
display_name: Optional[str] = None,
|
||||
property_type: Optional[str] = None,
|
||||
relation_type: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
is_required: Optional[bool] = None,
|
||||
default_value: Optional[list] = None,
|
||||
settings: Optional[dict] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
is_multi: Optional[bool] = None,
|
||||
validation_rules: Optional[dict] = None,
|
||||
external_source: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update work item property details.
|
||||
|
||||
Args:
|
||||
property_id: Property ID to update (required)
|
||||
type_id: Work item type ID this property belongs to (required)
|
||||
display_name: Display name for the property
|
||||
property_type: Type of property - TEXT, DATETIME, DECIMAL, BOOLEAN, OPTION, RELATION, URL, EMAIL, FILE
|
||||
relation_type: Relation type for RELATION properties - ONE_TO_ONE, ONE_TO_MANY, MANY_TO_ONE, MANY_TO_MANY
|
||||
description: Description of the property
|
||||
is_required: Whether this property is required for work items
|
||||
default_value: List of default values for the property
|
||||
settings: Property settings (TextAttributeSettings for TEXT, DateAttributeSettings for DATETIME)
|
||||
is_active: Whether the property is active
|
||||
is_multi: Whether this property supports multiple values
|
||||
validation_rules: Validation rules for the property
|
||||
external_source: External system source identifier (e.g., 'github', 'jira')
|
||||
external_id: External system's identifier for this property
|
||||
project_id: Project ID (auto-filled from context if not provided)
|
||||
workspace_slug: Workspace slug (auto-filled from context if not provided)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Build update data
|
||||
update_data: dict[str, Any] = {}
|
||||
if display_name is not None:
|
||||
update_data["display_name"] = display_name
|
||||
if property_type is not None:
|
||||
update_data["property_type"] = property_type
|
||||
if relation_type is not None:
|
||||
update_data["relation_type"] = relation_type
|
||||
if description is not None:
|
||||
update_data["description"] = description
|
||||
if is_required is not None:
|
||||
update_data["is_required"] = is_required
|
||||
if default_value is not None:
|
||||
update_data["default_value"] = default_value
|
||||
if settings is not None:
|
||||
update_data["settings"] = settings
|
||||
if is_active is not None:
|
||||
update_data["is_active"] = is_active
|
||||
if is_multi is not None:
|
||||
update_data["is_multi"] = is_multi
|
||||
if validation_rules is not None:
|
||||
update_data["validation_rules"] = validation_rules
|
||||
if external_source is not None:
|
||||
update_data["external_source"] = external_source
|
||||
if external_id is not None:
|
||||
update_data["external_id"] = external_id
|
||||
|
||||
result = await method_executor.execute(
|
||||
"properties",
|
||||
"update",
|
||||
property_id=property_id,
|
||||
type_id=type_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
**update_data,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully updated property", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update property", result["error"])
|
||||
|
||||
@tool
|
||||
async def properties_delete(
|
||||
property_id: str,
|
||||
type_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a work item property.
|
||||
|
||||
Args:
|
||||
property_id: Property ID to delete (required)
|
||||
type_id: Work item type ID this property belongs to (required)
|
||||
project_id: Project ID (auto-filled from context if not provided)
|
||||
workspace_slug: Workspace slug (auto-filled from context if not provided)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"properties",
|
||||
"delete",
|
||||
property_id=property_id,
|
||||
type_id=type_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully deleted property", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to delete property", result["error"])
|
||||
|
||||
@tool
|
||||
async def properties_create_option(
|
||||
property_id: str,
|
||||
type_id: str,
|
||||
name: str,
|
||||
value: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a property option."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"properties",
|
||||
"create_option",
|
||||
property_id=property_id,
|
||||
type_id=type_id,
|
||||
name=name,
|
||||
value=value,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully created property option", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create property option", result["error"])
|
||||
|
||||
@tool
|
||||
async def properties_create_value(
|
||||
property_id: str,
|
||||
type_id: str,
|
||||
issue_id: str,
|
||||
value: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a property value."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"properties",
|
||||
"create_value",
|
||||
property_id=property_id,
|
||||
type_id=type_id,
|
||||
issue_id=issue_id,
|
||||
value=value,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully created property value", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create property value", result["error"])
|
||||
|
||||
@tool
|
||||
async def properties_list_options(
|
||||
property_id: str,
|
||||
type_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List property options."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"properties",
|
||||
"list_options",
|
||||
property_id=property_id,
|
||||
type_id=type_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved property options", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list property options", result["error"])
|
||||
|
||||
@tool
|
||||
async def properties_list_values(
|
||||
type_id: str,
|
||||
issue_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List property values."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"properties",
|
||||
"list_values",
|
||||
type_id=type_id,
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved property values", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list property values", result["error"])
|
||||
|
||||
@tool
|
||||
async def properties_retrieve_option(
|
||||
property_id: str,
|
||||
type_id: str,
|
||||
option_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get property option by ID."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"properties",
|
||||
"retrieve_option",
|
||||
property_id=property_id,
|
||||
type_id=type_id,
|
||||
option_id=option_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved property option", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to retrieve property option", result["error"])
|
||||
|
||||
@tool
|
||||
async def properties_update_option(
|
||||
property_id: str,
|
||||
type_id: str,
|
||||
option_id: str,
|
||||
name: Optional[str] = None,
|
||||
value: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update property option."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Build update data
|
||||
update_data = {}
|
||||
if name is not None:
|
||||
update_data["name"] = name
|
||||
if value is not None:
|
||||
update_data["value"] = value
|
||||
|
||||
result = await method_executor.execute(
|
||||
"properties",
|
||||
"update_option",
|
||||
property_id=property_id,
|
||||
type_id=type_id,
|
||||
option_id=option_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
**update_data,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully updated property option", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update property option", result["error"])
|
||||
|
||||
@tool
|
||||
async def properties_delete_option(
|
||||
property_id: str,
|
||||
type_id: str,
|
||||
option_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete property option."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"properties",
|
||||
"delete_option",
|
||||
property_id=property_id,
|
||||
type_id=type_id,
|
||||
option_id=option_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully deleted property option", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to delete property option", result["error"])
|
||||
|
||||
return [
|
||||
properties_create,
|
||||
properties_list,
|
||||
properties_retrieve,
|
||||
properties_update,
|
||||
properties_delete,
|
||||
properties_create_option,
|
||||
properties_create_value,
|
||||
properties_list_options,
|
||||
properties_list_values,
|
||||
properties_retrieve_option,
|
||||
properties_update_option,
|
||||
properties_delete_option,
|
||||
]
|
||||
"""Return LangChain tools for the properties category using auto-generation from metadata."""
|
||||
return generate_tools_for_category("properties", method_executor, context, PROPERTIES_TOOL_DEFINITIONS)
|
||||
|
||||
@@ -11,201 +11,372 @@
|
||||
|
||||
"""
|
||||
States API tools for Plane workflow state management.
|
||||
|
||||
MIGRATED TO AUTO-GENERATED TOOLS
|
||||
Tool metadata defined in this file (STATE_TOOL_DEFINITIONS) for modularity.
|
||||
Old manual definitions kept below for comparison/rollback safety.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from .base import PlaneToolBase
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing state actions
|
||||
# Tool metadata for states category
|
||||
STATE_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"create": ToolMetadata(
|
||||
name="states_create",
|
||||
description="Create a new workflow state",
|
||||
sdk_method="create_state",
|
||||
returns_entity_type="state",
|
||||
parameters=[
|
||||
ToolParameter(name="name", type="str", required=True, description="State name (required)"),
|
||||
ToolParameter(name="color", type="str", required=True, description="State color in hex format (required)"),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (required - provide from conversation context or previous actions)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (provide if known, otherwise auto-detected)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(name="description", type="Optional[str]", required=False, description="State description", property_transform="name"),
|
||||
ToolParameter(
|
||||
name="group",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="State group (backlog, unstarted, started, completed, cancelled)",
|
||||
property_transform="name",
|
||||
),
|
||||
ToolParameter(name="sequence", type="Optional[int]", required=False, description="Display sequence order", property_transform="name"),
|
||||
ToolParameter(
|
||||
name="is_triage", type="Optional[bool]", required=False, description="Whether this is a triage state", property_transform="name"
|
||||
),
|
||||
ToolParameter(
|
||||
name="default", type="Optional[bool]", required=False, description="Whether this is the default state", property_transform="name"
|
||||
),
|
||||
ToolParameter(
|
||||
name="external_source",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description='External source identifier (e.g., "jira")',
|
||||
property_transform="name",
|
||||
),
|
||||
ToolParameter(name="external_id", type="Optional[str]", required=False, description="External system ID", property_transform="name"),
|
||||
],
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="states_list",
|
||||
description="List all workflow states in a project",
|
||||
sdk_method="list_states",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (required - provide from conversation context or previous actions)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (provide if known, otherwise auto-detected)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
],
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="states_retrieve",
|
||||
description="Retrieve details of a specific workflow state",
|
||||
sdk_method="retrieve_state",
|
||||
parameters=[
|
||||
ToolParameter(name="state_id", type="str", required=True, description="State ID (required)"),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (required - provide from conversation context or previous actions)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (provide if known, otherwise auto-detected)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
],
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="states_update",
|
||||
description="Update an existing workflow state",
|
||||
sdk_method="update_state",
|
||||
returns_entity_type="state",
|
||||
parameters=[
|
||||
ToolParameter(name="state_id", type="str", required=True, description="State ID (required)"),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (required - provide from conversation context or previous actions)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (provide if known, otherwise auto-detected)",
|
||||
auto_fill_from_context=True,
|
||||
property_transform="skip",
|
||||
),
|
||||
ToolParameter(name="name", type="Optional[str]", required=False, description="New state name", property_transform="name"),
|
||||
ToolParameter(name="color", type="Optional[str]", required=False, description="New state color", property_transform="name"),
|
||||
ToolParameter(name="description", type="Optional[str]", required=False, description="New state description", property_transform="name"),
|
||||
ToolParameter(name="sequence", type="Optional[int]", required=False, description="Display sequence order", property_transform="name"),
|
||||
ToolParameter(
|
||||
name="group",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="State group (backlog, unstarted, started, completed, cancelled)",
|
||||
property_transform="name",
|
||||
),
|
||||
ToolParameter(
|
||||
name="is_triage", type="Optional[bool]", required=False, description="Whether this is a triage state", property_transform="name"
|
||||
),
|
||||
ToolParameter(
|
||||
name="default", type="Optional[bool]", required=False, description="Whether this is the default state", property_transform="name"
|
||||
),
|
||||
ToolParameter(
|
||||
name="external_source",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description='External source identifier (e.g., "jira")',
|
||||
property_transform="name",
|
||||
),
|
||||
ToolParameter(name="external_id", type="Optional[str]", required=False, description="External system ID", property_transform="name"),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_state_tools(method_executor, context):
|
||||
"""Return LangChain tools for the states category using method_executor and context."""
|
||||
"""Return LangChain tools for the states category using auto-generation from metadata."""
|
||||
|
||||
@tool
|
||||
async def states_create(
|
||||
name: str,
|
||||
color: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
group: Optional[str] = None,
|
||||
sequence: Optional[int] = None,
|
||||
is_triage: Optional[bool] = None,
|
||||
default: Optional[bool] = None,
|
||||
external_source: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new workflow state.
|
||||
state_tools = generate_tools_for_category(
|
||||
category="states",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions=STATE_TOOL_DEFINITIONS,
|
||||
)
|
||||
|
||||
Args:
|
||||
name: State name (required)
|
||||
color: State color in hex format (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
description: State description
|
||||
group: State group (backlog, unstarted, started, completed, cancelled)
|
||||
sequence: Display sequence order
|
||||
is_triage: Whether this is a triage state
|
||||
default: Whether this is the default state
|
||||
external_source: External source identifier (e.g., "jira")
|
||||
external_id: External system ID
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
return state_tools
|
||||
|
||||
result = await method_executor.execute(
|
||||
"states",
|
||||
"create",
|
||||
name=name,
|
||||
color=color,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
description=description,
|
||||
group=group,
|
||||
sequence=sequence,
|
||||
is_triage=is_triage,
|
||||
default=default,
|
||||
external_source=external_source,
|
||||
external_id=external_id,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return await PlaneToolBase.format_success_payload_with_url(f"Successfully created state '{name}'", result["data"], "state", context)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create state", result["error"])
|
||||
# ============================================================================
|
||||
# OLD MANUAL TOOL DEFINITIONS (COMMENTED OUT - KEPT FOR COMPARISON)
|
||||
# To rollback: uncomment below and comment out the auto-generation code above
|
||||
# ============================================================================
|
||||
|
||||
@tool
|
||||
async def states_list(
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List all workflow states in a project."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"states",
|
||||
"list",
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved states list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list states", result["error"])
|
||||
|
||||
@tool
|
||||
async def states_retrieve(
|
||||
state_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Retrieve details of a specific workflow state.
|
||||
|
||||
Args:
|
||||
state_id: State ID (required)
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"states",
|
||||
"retrieve",
|
||||
state_id=state_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved state details", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to retrieve state", result["error"])
|
||||
|
||||
@tool
|
||||
async def states_update(
|
||||
state_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
color: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
sequence: Optional[int] = None,
|
||||
group: Optional[str] = None,
|
||||
is_triage: Optional[bool] = None,
|
||||
default: Optional[bool] = None,
|
||||
external_source: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update an existing workflow state.
|
||||
|
||||
Args:
|
||||
state_id: State ID (required)
|
||||
name: New state name
|
||||
color: New state color
|
||||
description: New state description
|
||||
sequence: Display sequence order
|
||||
group: State group (backlog, unstarted, started, completed, cancelled)
|
||||
is_triage: Whether this is a triage state
|
||||
default: Whether this is the default state
|
||||
external_source: External source identifier (e.g., "jira")
|
||||
external_id: External system ID
|
||||
project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Build update data with only non-None values
|
||||
update_data = {
|
||||
k: v
|
||||
for k, v in {
|
||||
"name": name,
|
||||
"color": color,
|
||||
"description": description,
|
||||
"sequence": sequence,
|
||||
"group": group,
|
||||
"is_triage": is_triage,
|
||||
"default": default,
|
||||
"external_source": external_source,
|
||||
"external_id": external_id,
|
||||
}.items()
|
||||
if v is not None
|
||||
}
|
||||
|
||||
result = await method_executor.execute(
|
||||
"states",
|
||||
"update",
|
||||
state_id=state_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
**update_data,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return await PlaneToolBase.format_success_payload_with_url(f"Successfully updated state '{state_id}'", result["data"], "state", context)
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update state", result["error"])
|
||||
|
||||
return [states_create, states_list, states_retrieve, states_update]
|
||||
# from typing import Optional
|
||||
# from langchain_core.tools import tool
|
||||
# from .base import PlaneToolBase
|
||||
#
|
||||
# def get_state_tools(method_executor, context):
|
||||
# """Return LangChain tools for the states category using method_executor and context."""
|
||||
#
|
||||
# @tool
|
||||
# async def states_create(
|
||||
# name: str,
|
||||
# color: str,
|
||||
# project_id: Optional[str] = None,
|
||||
# workspace_slug: Optional[str] = None,
|
||||
# description: Optional[str] = None,
|
||||
# group: Optional[str] = None,
|
||||
# sequence: Optional[int] = None,
|
||||
# is_triage: Optional[bool] = None,
|
||||
# default: Optional[bool] = None,
|
||||
# external_source: Optional[str] = None,
|
||||
# external_id: Optional[str] = None,
|
||||
# ) -> Dict[str, Any]:
|
||||
# """Create a new workflow state.
|
||||
#
|
||||
# Args:
|
||||
# name: State name (required)
|
||||
# color: State color in hex format (required)
|
||||
# project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
# workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
# description: State description
|
||||
# group: State group (backlog, unstarted, started, completed, cancelled)
|
||||
# sequence: Display sequence order
|
||||
# is_triage: Whether this is a triage state
|
||||
# default: Whether this is the default state
|
||||
# external_source: External source identifier (e.g., "jira")
|
||||
# external_id: External system ID
|
||||
# """
|
||||
# # Auto-fill from context if not provided
|
||||
# if workspace_slug is None and "workspace_slug" in context:
|
||||
# workspace_slug = context["workspace_slug"]
|
||||
# if project_id is None and "project_id" in context:
|
||||
# project_id = context["project_id"]
|
||||
#
|
||||
# result = await method_executor.execute(
|
||||
# "states",
|
||||
# "create",
|
||||
# name=name,
|
||||
# color=color,
|
||||
# project_id=project_id,
|
||||
# workspace_slug=workspace_slug,
|
||||
# description=description,
|
||||
# group=group,
|
||||
# sequence=sequence,
|
||||
# is_triage=is_triage,
|
||||
# default=default,
|
||||
# external_source=external_source,
|
||||
# external_id=external_id,
|
||||
# )
|
||||
#
|
||||
# if result["success"]:
|
||||
# return await PlaneToolBase.format_success_payload_with_url(f"Successfully created state '{name}'", result["data"], "state", context)
|
||||
# else:
|
||||
# return PlaneToolBase.format_error_payload("Failed to create state", result["error"])
|
||||
#
|
||||
# @tool
|
||||
# async def states_list(
|
||||
# project_id: Optional[str] = None,
|
||||
# workspace_slug: Optional[str] = None,
|
||||
# ) -> Dict[str, Any]:
|
||||
# """List all workflow states in a project."""
|
||||
# # Auto-fill from context if not provided
|
||||
# if workspace_slug is None and "workspace_slug" in context:
|
||||
# workspace_slug = context["workspace_slug"]
|
||||
# if project_id is None and "project_id" in context:
|
||||
# project_id = context["project_id"]
|
||||
#
|
||||
# result = await method_executor.execute(
|
||||
# "states",
|
||||
# "list",
|
||||
# project_id=project_id,
|
||||
# workspace_slug=workspace_slug,
|
||||
# )
|
||||
#
|
||||
# if result["success"]:
|
||||
# return PlaneToolBase.format_success_payload("Successfully retrieved states list", result["data"])
|
||||
# else:
|
||||
# return PlaneToolBase.format_error_payload("Failed to list states", result["error"])
|
||||
#
|
||||
# @tool
|
||||
# async def states_retrieve(
|
||||
# state_id: str,
|
||||
# project_id: Optional[str] = None,
|
||||
# workspace_slug: Optional[str] = None,
|
||||
# ) -> Dict[str, Any]:
|
||||
# """Retrieve details of a specific workflow state.
|
||||
#
|
||||
# Args:
|
||||
# state_id: State ID (required)
|
||||
# project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
# workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
# """
|
||||
# # Auto-fill from context if not provided
|
||||
# if workspace_slug is None and "workspace_slug" in context:
|
||||
# workspace_slug = context["workspace_slug"]
|
||||
# if project_id is None and "project_id" in context:
|
||||
# project_id = context["project_id"]
|
||||
#
|
||||
# result = await method_executor.execute(
|
||||
# "states",
|
||||
# "retrieve",
|
||||
# state_id=state_id,
|
||||
# project_id=project_id,
|
||||
# workspace_slug=workspace_slug,
|
||||
# )
|
||||
#
|
||||
# if result["success"]:
|
||||
# return PlaneToolBase.format_success_payload("Successfully retrieved state details", result["data"])
|
||||
# else:
|
||||
# return PlaneToolBase.format_error_payload("Failed to retrieve state", result["error"])
|
||||
#
|
||||
# @tool
|
||||
# async def states_update(
|
||||
# state_id: str,
|
||||
# project_id: Optional[str] = None,
|
||||
# workspace_slug: Optional[str] = None,
|
||||
# name: Optional[str] = None,
|
||||
# color: Optional[str] = None,
|
||||
# description: Optional[str] = None,
|
||||
# sequence: Optional[int] = None,
|
||||
# group: Optional[str] = None,
|
||||
# is_triage: Optional[bool] = None,
|
||||
# default: Optional[bool] = None,
|
||||
# external_source: Optional[str] = None,
|
||||
# external_id: Optional[str] = None,
|
||||
# ) -> Dict[str, Any]:
|
||||
# """Update an existing workflow state.
|
||||
#
|
||||
# Args:
|
||||
# state_id: State ID (required)
|
||||
# name: New state name
|
||||
# color: New state color
|
||||
# description: New state description
|
||||
# sequence: Display sequence order
|
||||
# group: State group (backlog, unstarted, started, completed, cancelled)
|
||||
# is_triage: Whether this is a triage state
|
||||
# default: Whether this is the default state
|
||||
# external_source: External source identifier (e.g., "jira")
|
||||
# external_id: External system ID
|
||||
# project_id: Project ID (required - provide from conversation context or previous actions)
|
||||
# workspace_slug: Workspace slug (provide if known, otherwise auto-detected)
|
||||
# """
|
||||
# # Auto-fill from context if not provided
|
||||
# if workspace_slug is None and "workspace_slug" in context:
|
||||
# workspace_slug = context["workspace_slug"]
|
||||
# if project_id is None and "project_id" in context:
|
||||
# project_id = context["project_id"]
|
||||
#
|
||||
# # Build update data with only non-None values
|
||||
# update_data = {
|
||||
# k: v
|
||||
# for k, v in {
|
||||
# "name": name,
|
||||
# "color": color,
|
||||
# "description": description,
|
||||
# "sequence": sequence,
|
||||
# "group": group,
|
||||
# "is_triage": is_triage,
|
||||
# "default": default,
|
||||
# "external_source": external_source,
|
||||
# "external_id": external_id,
|
||||
# }.items()
|
||||
# if v is not None
|
||||
# }
|
||||
#
|
||||
# result = await method_executor.execute(
|
||||
# "states",
|
||||
# "update",
|
||||
# state_id=state_id,
|
||||
# project_id=project_id,
|
||||
# workspace_slug=workspace_slug,
|
||||
# **update_data,
|
||||
# )
|
||||
#
|
||||
# if result["success"]:
|
||||
# return await PlaneToolBase.format_success_payload_with_url(f"Successfully updated state '{state_id}'", result["data"], "state", context)
|
||||
# else:
|
||||
# return PlaneToolBase.format_error_payload("Failed to update state", result["error"])
|
||||
#
|
||||
# return [states_create, states_list, states_retrieve, states_update]
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
# SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
|
||||
# SPDX-License-Identifier: LicenseRef-Plane-Commercial
|
||||
#
|
||||
# Licensed under the Plane Commercial License (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# https://plane.so/legals/eula
|
||||
#
|
||||
# DO NOT remove or modify this notice.
|
||||
# NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
|
||||
"""
|
||||
Stickies API tools for Plane quick notes operations.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
# ============================================================================
|
||||
# STICKIES-SPECIFIC HANDLER FUNCTIONS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def _stickies_pre_handler(
|
||||
metadata: ToolMetadata,
|
||||
kwargs: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
category: str,
|
||||
method_key: str,
|
||||
method_executor: Any = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Pre-processing handler for stickies tools.
|
||||
|
||||
Handles:
|
||||
- Auto-generate description_html from description if not provided
|
||||
"""
|
||||
# If description is provided but description_html is not, auto-generate HTML version
|
||||
# The UI displays description_html, so we need to ensure it's populated
|
||||
if method_key in ["create", "update"]:
|
||||
if "description" in kwargs and kwargs["description"] is not None:
|
||||
if "description_html" not in kwargs or kwargs.get("description_html") is None:
|
||||
# Convert plain text to simple HTML paragraph
|
||||
# Escape HTML special characters and preserve line breaks
|
||||
import html
|
||||
|
||||
escaped_description = html.escape(kwargs["description"])
|
||||
# Replace newlines with <br> tags
|
||||
escaped_description = escaped_description.replace("\n", "<br>")
|
||||
kwargs["description_html"] = f"<p>{escaped_description}</p>"
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# STICKIES TOOL DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
STICKY_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"create": ToolMetadata(
|
||||
name="stickies_create",
|
||||
description="Create a new sticky note for quick annotations. The main content/text should go in 'description' parameter.",
|
||||
sdk_method="create_sticky",
|
||||
returns_entity_type="sticky",
|
||||
pre_handler=_stickies_pre_handler,
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="name",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Optional title/heading for the sticky note",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Main content/text of the sticky note (use this for user's content)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description_html",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="HTML formatted content (optional, auto-generated from description if not provided)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="color",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Text color hex code (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="background_color",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Background color hex code (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="logo_props",
|
||||
type="Optional[dict]",
|
||||
required=False,
|
||||
description="Logo configuration JSON (optional)",
|
||||
),
|
||||
],
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="stickies_list",
|
||||
description="List all sticky notes in the workspace",
|
||||
sdk_method="list_stickies",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="stickies_retrieve",
|
||||
description="Retrieve a single sticky note by ID",
|
||||
sdk_method="retrieve_sticky",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="sticky_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Sticky ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="stickies_update",
|
||||
description="Update sticky note details",
|
||||
sdk_method="update_sticky",
|
||||
returns_entity_type="sticky",
|
||||
pre_handler=_stickies_pre_handler,
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="sticky_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Sticky ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="name",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New sticky note title",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New plain text description",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description_html",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New HTML description",
|
||||
),
|
||||
ToolParameter(
|
||||
name="color",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New text color hex code",
|
||||
),
|
||||
ToolParameter(
|
||||
name="background_color",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New background color hex code",
|
||||
),
|
||||
ToolParameter(
|
||||
name="logo_props",
|
||||
type="Optional[dict]",
|
||||
required=False,
|
||||
description="New logo configuration JSON",
|
||||
),
|
||||
],
|
||||
),
|
||||
"delete": ToolMetadata(
|
||||
name="stickies_delete",
|
||||
description="Delete a sticky note",
|
||||
sdk_method="delete_sticky",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="sticky_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Sticky ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_sticky_tools(method_executor, context):
|
||||
"""Return LangChain tools for the stickies category using auto-generation from metadata."""
|
||||
return generate_tools_for_category(
|
||||
category="stickies",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions=STICKY_TOOL_DEFINITIONS,
|
||||
)
|
||||
@@ -0,0 +1,324 @@
|
||||
# SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
|
||||
# SPDX-License-Identifier: LicenseRef-Plane-Commercial
|
||||
#
|
||||
# Licensed under the Plane Commercial License (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# https://plane.so/legals/eula
|
||||
#
|
||||
# DO NOT remove or modify this notice.
|
||||
# NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
|
||||
"""
|
||||
Teamspaces API tools for Plane team collaboration operations.
|
||||
"""
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
# ============================================================================
|
||||
# TEAMSPACES TOOL DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
TEAMSPACE_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"create": ToolMetadata(
|
||||
name="teamspaces_create",
|
||||
description="Create a new teamspace for team collaboration",
|
||||
sdk_method="create_teamspace",
|
||||
returns_entity_type="teamspace",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="name",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Teamspace name (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="description_html",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="HTML description of the teamspace",
|
||||
),
|
||||
ToolParameter(
|
||||
name="logo_props",
|
||||
type="Optional[dict]",
|
||||
required=False,
|
||||
description="Logo configuration JSON",
|
||||
),
|
||||
ToolParameter(
|
||||
name="lead",
|
||||
type="str",
|
||||
required=False,
|
||||
description="Team lead user ID (optional)",
|
||||
),
|
||||
],
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="teamspaces_list",
|
||||
description="List teamspaces in the workspace",
|
||||
sdk_method="list_teamspaces",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="teamspaces_retrieve",
|
||||
description="Retrieve a single teamspace by ID",
|
||||
sdk_method="retrieve_teamspace",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="teamspace_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Teamspace ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="teamspaces_update",
|
||||
description="Update teamspace details",
|
||||
sdk_method="update_teamspace",
|
||||
returns_entity_type="teamspace",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="teamspace_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Teamspace ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="name",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New teamspace name",
|
||||
),
|
||||
ToolParameter(
|
||||
name="description_html",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="New HTML description",
|
||||
),
|
||||
ToolParameter(
|
||||
name="logo_props",
|
||||
type="Optional[dict]",
|
||||
required=False,
|
||||
description="New logo configuration JSON",
|
||||
),
|
||||
ToolParameter(
|
||||
name="lead",
|
||||
type="str",
|
||||
required=False,
|
||||
description="Team lead user ID (use empty string to remove the lead)",
|
||||
),
|
||||
],
|
||||
),
|
||||
"delete": ToolMetadata(
|
||||
name="teamspaces_delete",
|
||||
description="Delete a teamspace",
|
||||
sdk_method="delete_teamspace",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="teamspace_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Teamspace ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"add_members": ToolMetadata(
|
||||
name="teamspaces_add_members",
|
||||
description="Add members to a teamspace",
|
||||
sdk_method="add_teamspace_members",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="teamspace_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Teamspace ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="member_ids",
|
||||
type="List[str]",
|
||||
required=True,
|
||||
description="List of member (user) IDs to add (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"list_members": ToolMetadata(
|
||||
name="teamspaces_list_members",
|
||||
description="List members in a teamspace",
|
||||
sdk_method="list_teamspace_members",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="teamspace_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Teamspace ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"remove_members": ToolMetadata(
|
||||
name="teamspaces_remove_members",
|
||||
description="Remove members from a teamspace",
|
||||
sdk_method="remove_teamspace_members",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="teamspace_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Teamspace ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="member_ids",
|
||||
type="List[str]",
|
||||
required=True,
|
||||
description="List of member (user) IDs to remove (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"add_projects": ToolMetadata(
|
||||
name="teamspaces_add_projects",
|
||||
description="Add projects to a teamspace",
|
||||
sdk_method="add_teamspace_projects",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="teamspace_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Teamspace ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_ids",
|
||||
type="List[str]",
|
||||
required=True,
|
||||
description="List of project IDs to add (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"list_projects": ToolMetadata(
|
||||
name="teamspaces_list_projects",
|
||||
description="List projects in a teamspace",
|
||||
sdk_method="list_teamspace_projects",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="teamspace_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Teamspace ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"remove_projects": ToolMetadata(
|
||||
name="teamspaces_remove_projects",
|
||||
description="Remove projects from a teamspace",
|
||||
sdk_method="remove_teamspace_projects",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="teamspace_id",
|
||||
type="str",
|
||||
required=True,
|
||||
description="Teamspace ID (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_ids",
|
||||
type="List[str]",
|
||||
required=True,
|
||||
description="List of project IDs to remove (required)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_teamspace_tools(method_executor, context):
|
||||
"""Return LangChain tools for the teamspaces category using auto-generation from metadata."""
|
||||
return generate_tools_for_category(
|
||||
category="teamspaces",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions=TEAMSPACE_TOOL_DEFINITIONS,
|
||||
)
|
||||
@@ -13,197 +13,158 @@
|
||||
Types API tools for Plane issue type management.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from .base import PlaneToolBase
|
||||
# ============================================================================
|
||||
# TYPES TOOL DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing type actions
|
||||
TYPE_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"create": ToolMetadata(
|
||||
name="types_create",
|
||||
description="Create a new work item type",
|
||||
sdk_method="create_issue_type",
|
||||
parameters=[
|
||||
ToolParameter(name="name", type="str", required=True, description="Name of the work item type (required)"),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(name="description", type="Optional[str]", required=False, description="Optional description of the type"),
|
||||
ToolParameter(
|
||||
name="project_ids", type="Optional[List[str]]", required=False, description="List of project IDs to associate with this type"
|
||||
),
|
||||
ToolParameter(name="is_epic", type="Optional[bool]", required=False, description="Flag to mark this type as an epic type"),
|
||||
ToolParameter(name="is_active", type="Optional[bool]", required=False, description="Activation status of the type"),
|
||||
ToolParameter(
|
||||
name="external_source", type="Optional[str]", required=False, description="External integration source (e.g., 'jira', 'github')"
|
||||
),
|
||||
ToolParameter(name="external_id", type="Optional[str]", required=False, description="External system identifier for this type"),
|
||||
],
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="types_list",
|
||||
description="List all work item types",
|
||||
sdk_method="list_issue_types",
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"retrieve": ToolMetadata(
|
||||
name="types_retrieve",
|
||||
description="Get a single type by ID",
|
||||
sdk_method="retrieve_issue_type",
|
||||
parameters=[
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Type ID (required)"),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="types_update",
|
||||
description="Update work item type details",
|
||||
sdk_method="update_issue_type",
|
||||
parameters=[
|
||||
ToolParameter(name="type_id", type="str", required=True, description="UUID of the work item type to update (required)"),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(name="name", type="Optional[str]", required=False, description="Updated name of the type"),
|
||||
ToolParameter(name="description", type="Optional[str]", required=False, description="Updated description"),
|
||||
ToolParameter(
|
||||
name="project_ids", type="Optional[list[str]]", required=False, description="Updated list of project IDs to associate with this type"
|
||||
),
|
||||
ToolParameter(name="is_epic", type="Optional[bool]", required=False, description="Updated epic type flag"),
|
||||
ToolParameter(name="is_active", type="Optional[bool]", required=False, description="Updated activation status"),
|
||||
ToolParameter(name="external_source", type="Optional[str]", required=False, description="Updated external integration source"),
|
||||
ToolParameter(name="external_id", type="Optional[str]", required=False, description="Updated external system identifier"),
|
||||
],
|
||||
),
|
||||
"delete": ToolMetadata(
|
||||
name="types_delete",
|
||||
description="Delete a type",
|
||||
sdk_method="delete_issue_type",
|
||||
parameters=[
|
||||
ToolParameter(name="type_id", type="str", required=True, description="Type ID (required)"),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_id",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Project ID (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_type_tools(method_executor, context):
|
||||
"""Return LangChain tools for the types category using method_executor and context."""
|
||||
"""Get all Types API tools."""
|
||||
|
||||
@tool
|
||||
async def types_create(
|
||||
name: str,
|
||||
description: Optional[str] = None,
|
||||
project_ids: Optional[list[str]] = None,
|
||||
is_epic: Optional[bool] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
external_source: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new work item type.
|
||||
|
||||
Args:
|
||||
name: Name of the work item type (required)
|
||||
description: Optional description of the type
|
||||
project_ids: List of project IDs to associate with this type
|
||||
is_epic: Flag to mark this type as an epic type
|
||||
is_active: Activation status of the type
|
||||
external_source: External integration source (e.g., 'jira', 'github')
|
||||
external_id: External system identifier for this type
|
||||
project_id: UUID of the project (auto-filled from context)
|
||||
workspace_slug: Workspace slug identifier (auto-filled from context)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"types",
|
||||
"create",
|
||||
name=name,
|
||||
description=description,
|
||||
project_ids=project_ids,
|
||||
is_epic=is_epic,
|
||||
is_active=is_active,
|
||||
external_source=external_source,
|
||||
external_id=external_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload(f"Successfully created type '{name}'", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create type", result["error"])
|
||||
|
||||
@tool
|
||||
async def types_list(project_id: Optional[str] = None, workspace_slug: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""List all work item types."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute("types", "list", project_id=project_id, workspace_slug=workspace_slug)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved types list", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list types", result["error"])
|
||||
|
||||
@tool
|
||||
async def types_retrieve(
|
||||
type_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get a single type by ID."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"types",
|
||||
"retrieve",
|
||||
type_id=type_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved type", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to retrieve type", result["error"])
|
||||
|
||||
@tool
|
||||
async def types_update(
|
||||
type_id: str,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
project_ids: Optional[list[str]] = None,
|
||||
is_epic: Optional[bool] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
external_source: Optional[str] = None,
|
||||
external_id: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update work item type details.
|
||||
|
||||
Args:
|
||||
type_id: UUID of the work item type to update (required)
|
||||
name: Updated name of the type
|
||||
description: Updated description
|
||||
project_ids: Updated list of project IDs to associate with this type
|
||||
is_epic: Updated epic type flag
|
||||
is_active: Updated activation status
|
||||
external_source: Updated external integration source
|
||||
external_id: Updated external system identifier
|
||||
project_id: UUID of the project (auto-filled from context)
|
||||
workspace_slug: Workspace slug identifier (auto-filled from context)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Build update data
|
||||
update_data: dict[str, Any] = {}
|
||||
if name is not None:
|
||||
update_data["name"] = name
|
||||
if description is not None:
|
||||
update_data["description"] = description
|
||||
if project_ids is not None:
|
||||
update_data["project_ids"] = project_ids
|
||||
if is_epic is not None:
|
||||
update_data["is_epic"] = is_epic
|
||||
if is_active is not None:
|
||||
update_data["is_active"] = is_active
|
||||
if external_source is not None:
|
||||
update_data["external_source"] = external_source
|
||||
if external_id is not None:
|
||||
update_data["external_id"] = external_id
|
||||
|
||||
result = await method_executor.execute(
|
||||
"types",
|
||||
"update",
|
||||
type_id=type_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
**update_data,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully updated type", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update type", result["error"])
|
||||
|
||||
@tool
|
||||
async def types_delete(
|
||||
type_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete a type."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"types",
|
||||
"delete",
|
||||
type_id=type_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully deleted type", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to delete type", result["error"])
|
||||
|
||||
return [types_create, types_list, types_retrieve, types_update, types_delete]
|
||||
"""Return LangChain tools for the types category using auto-generation from metadata."""
|
||||
return generate_tools_for_category(
|
||||
category="types",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions=TYPE_TOOL_DEFINITIONS,
|
||||
)
|
||||
|
||||
@@ -11,29 +11,59 @@
|
||||
|
||||
"""
|
||||
Users API tools for Plane user management operations.
|
||||
|
||||
MIGRATED TO AUTO-GENERATED TOOLS
|
||||
Tool metadata defined in this file (USER_TOOL_DEFINITIONS) for modularity.
|
||||
Old manual definitions kept below for comparison/rollback safety.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
|
||||
from .base import PlaneToolBase
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing user actions
|
||||
# Tool metadata for users category
|
||||
USER_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"get_current_user": ToolMetadata(
|
||||
name="users_get_current",
|
||||
description="Get current user information",
|
||||
sdk_method="get_current_user",
|
||||
parameters=[],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_user_tools(method_executor, context):
|
||||
"""Return LangChain tools for the users category using method_executor and context."""
|
||||
"""Return LangChain tools for the users category using auto-generation from metadata."""
|
||||
|
||||
@tool
|
||||
async def users_get_current() -> Dict[str, Any]:
|
||||
"""Get current user information."""
|
||||
result = await method_executor.execute("users", "get_current")
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved current user", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to get current user", result["error"])
|
||||
user_tools = generate_tools_for_category(
|
||||
category="users",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions=USER_TOOL_DEFINITIONS,
|
||||
)
|
||||
|
||||
return [users_get_current]
|
||||
return user_tools
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# OLD MANUAL TOOL DEFINITIONS (COMMENTED OUT - KEPT FOR COMPARISON)
|
||||
# To rollback: uncomment below and comment out the auto-generation code above
|
||||
# ============================================================================
|
||||
|
||||
# from langchain_core.tools import tool
|
||||
# from .base import PlaneToolBase
|
||||
#
|
||||
# def get_user_tools(method_executor, context):
|
||||
# """Return LangChain tools for the users category using method_executor and context."""
|
||||
#
|
||||
# @tool
|
||||
# async def users_get_current() -> Dict[str, Any]:
|
||||
# """Get current user information."""
|
||||
# result = await method_executor.execute("users", "get_current")
|
||||
# if result["success"]:
|
||||
# return PlaneToolBase.format_success_payload("Successfully retrieved current user", result["data"])
|
||||
# else:
|
||||
# return PlaneToolBase.format_error_payload("Failed to get current user", result["error"])
|
||||
#
|
||||
# return [users_get_current]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,244 +13,156 @@
|
||||
Worklogs API tools for Plane time tracking operations.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import re
|
||||
import uuid
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
from .base import PlaneToolBase
|
||||
|
||||
# Factory wired via CATEGORY_TO_PROVIDER in tools/__init__.py
|
||||
# Returns LangChain tools implementing worklog actions
|
||||
# Pre-handler combining project resolution AND duration parsing
|
||||
async def _worklog_pre_handler(
|
||||
metadata: ToolMetadata,
|
||||
kwargs: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
category: str,
|
||||
method_key: str,
|
||||
method_executor: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""Resolve project_id and parse duration string to minutes."""
|
||||
|
||||
# 1. Project ID Resolution
|
||||
issue_id = kwargs.get("issue_id")
|
||||
project_id = kwargs.get("project_id")
|
||||
|
||||
should_resolve = False
|
||||
if issue_id and not project_id:
|
||||
should_resolve = True
|
||||
elif issue_id and project_id:
|
||||
try:
|
||||
uuid.UUID(str(project_id))
|
||||
except (ValueError, AttributeError):
|
||||
should_resolve = True
|
||||
|
||||
if should_resolve:
|
||||
try:
|
||||
from pi.app.api.v1.helpers.plane_sql_queries import get_issue_identifier_for_artifact
|
||||
|
||||
issue_info = await get_issue_identifier_for_artifact(str(issue_id))
|
||||
if issue_info and issue_info.get("project_id"):
|
||||
kwargs["project_id"] = issue_info["project_id"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Duration Parsing (e.g., "1h 30m" -> 90)
|
||||
duration = kwargs.get("duration")
|
||||
if duration is not None:
|
||||
if isinstance(duration, str):
|
||||
# Try to parse string format "Xh Ym" or just "Xm"
|
||||
total_minutes = 0
|
||||
|
||||
# Simple regex for parsing
|
||||
# Matches "1h", "1.5h", "30m", "1h 30m"
|
||||
hours_match = re.search(r"(\d+(?:\.\d+)?)\s*h", duration.lower())
|
||||
minutes_match = re.search(r"(\d+(?:\.\d+)?)\s*m", duration.lower())
|
||||
|
||||
if hours_match:
|
||||
with contextlib.suppress(ValueError):
|
||||
total_minutes += int(float(hours_match.group(1)) * 60)
|
||||
|
||||
if minutes_match:
|
||||
with contextlib.suppress(ValueError):
|
||||
total_minutes += int(float(minutes_match.group(1)))
|
||||
|
||||
# If regex matched nothing but it's a digit string, treat as minutes
|
||||
if not hours_match and not minutes_match:
|
||||
if duration.isdigit():
|
||||
total_minutes = int(duration)
|
||||
|
||||
# If we found something, update kwargs
|
||||
if total_minutes > 0:
|
||||
kwargs["duration"] = total_minutes
|
||||
|
||||
# If int/float passed directly, ensure int
|
||||
elif isinstance(duration, (int, float)):
|
||||
kwargs["duration"] = int(duration)
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
WORKLOGS_TOOL_DEFINITIONS = {
|
||||
"create": ToolMetadata(
|
||||
name="worklogs_create",
|
||||
description="Create a new worklog (time entry).",
|
||||
sdk_method="create_issue_worklog",
|
||||
parameters=[
|
||||
ToolParameter(name="issue_id", description="Issue ID", required=True, type="str"),
|
||||
ToolParameter(name="description", description="Worklog description/note", required=True, type="str"),
|
||||
ToolParameter(
|
||||
name="duration", description="Duration (e.g., '1h 30m' or minutes as int)", required=True, type="Any"
|
||||
), # Accepting Any to allow string
|
||||
ToolParameter(
|
||||
name="project_id", description="Project ID (optional, auto-filled)", required=False, type="Optional[str]", auto_fill_from_context=True
|
||||
),
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
description="Workspace slug (optional, auto-filled)",
|
||||
required=False,
|
||||
type="Optional[str]",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
pre_handler=_worklog_pre_handler,
|
||||
returns_entity_type="worklog",
|
||||
),
|
||||
"list": ToolMetadata(
|
||||
name="worklogs_list",
|
||||
description="List worklogs for an issue.",
|
||||
sdk_method="list_issue_worklogs",
|
||||
parameters=[
|
||||
ToolParameter(name="issue_id", description="Issue ID", required=True, type="str"),
|
||||
ToolParameter(name="project_id", description="Project ID", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", description="Workspace slug", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
],
|
||||
pre_handler=_worklog_pre_handler,
|
||||
),
|
||||
"update": ToolMetadata(
|
||||
name="worklogs_update",
|
||||
description="Update time entry.",
|
||||
sdk_method="update_issue_worklog",
|
||||
parameters=[
|
||||
ToolParameter(name="worklog_id", description="Worklog ID", required=True, type="str"),
|
||||
ToolParameter(name="issue_id", description="Issue ID", required=True, type="str"),
|
||||
ToolParameter(name="description", description="Worklog description", required=False, type="Optional[str]"),
|
||||
ToolParameter(name="duration", description="Duration (e.g., '1h 30m')", required=False, type="Any"),
|
||||
ToolParameter(name="project_id", description="Project ID", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", description="Workspace slug", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
ToolParameter(name="created_by", description="User ID who created", required=False, type="Optional[str]"),
|
||||
ToolParameter(name="updated_by", description="User ID who updated", required=False, type="Optional[str]"),
|
||||
],
|
||||
pre_handler=_worklog_pre_handler,
|
||||
returns_entity_type="worklog",
|
||||
),
|
||||
"delete": ToolMetadata(
|
||||
name="worklogs_delete",
|
||||
description="Delete time entry.",
|
||||
sdk_method="delete_issue_worklog",
|
||||
parameters=[
|
||||
ToolParameter(name="worklog_id", description="Worklog ID", required=True, type="str"),
|
||||
ToolParameter(name="issue_id", description="Issue ID", required=True, type="str"),
|
||||
ToolParameter(name="project_id", description="Project ID", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
ToolParameter(name="workspace_slug", description="Workspace slug", required=False, type="Optional[str]", auto_fill_from_context=True),
|
||||
],
|
||||
pre_handler=_worklog_pre_handler,
|
||||
returns_entity_type="worklog",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_worklog_tools(method_executor, context):
|
||||
"""Return LangChain tools for the worklogs category using method_executor and context."""
|
||||
|
||||
@tool
|
||||
async def worklogs_create(
|
||||
issue_id: str,
|
||||
description: str,
|
||||
duration: int,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create a new worklog entry.
|
||||
|
||||
Args:
|
||||
issue_id: Issue ID (required)
|
||||
description: Worklog description (required)
|
||||
duration: Duration in minutes (required)
|
||||
project_id: Project ID (optional, auto-filled from context or resolved from issue_id)
|
||||
workspace_slug: Workspace slug (optional, auto-filled from context)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Programmatically resolve project_id from issue_id if missing or invalid (non-UUID)
|
||||
should_resolve = False
|
||||
if issue_id and not project_id:
|
||||
should_resolve = True
|
||||
elif issue_id and project_id:
|
||||
try:
|
||||
uuid.UUID(str(project_id))
|
||||
except (ValueError, AttributeError):
|
||||
should_resolve = True
|
||||
|
||||
if should_resolve:
|
||||
try:
|
||||
from pi.app.api.v1.helpers.plane_sql_queries import get_issue_identifier_for_artifact
|
||||
|
||||
issue_info = await get_issue_identifier_for_artifact(str(issue_id))
|
||||
if issue_info and issue_info.get("project_id"):
|
||||
project_id = issue_info["project_id"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result = await method_executor.execute(
|
||||
"worklogs",
|
||||
"create",
|
||||
issue_id=issue_id,
|
||||
description=description,
|
||||
duration=duration,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully created worklog", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to create worklog", result["error"])
|
||||
|
||||
@tool
|
||||
async def worklogs_list(
|
||||
issue_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""List worklogs for an issue."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Programmatically resolve project_id from issue_id if missing or invalid (non-UUID)
|
||||
should_resolve = False
|
||||
if issue_id and not project_id:
|
||||
should_resolve = True
|
||||
elif issue_id and project_id:
|
||||
try:
|
||||
uuid.UUID(str(project_id))
|
||||
except (ValueError, AttributeError):
|
||||
should_resolve = True
|
||||
|
||||
if should_resolve:
|
||||
try:
|
||||
from pi.app.api.v1.helpers.plane_sql_queries import get_issue_identifier_for_artifact
|
||||
|
||||
issue_info = await get_issue_identifier_for_artifact(str(issue_id))
|
||||
if issue_info and issue_info.get("project_id"):
|
||||
project_id = issue_info["project_id"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result = await method_executor.execute(
|
||||
"worklogs",
|
||||
"list",
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved worklogs", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to list worklogs", result["error"])
|
||||
|
||||
@tool
|
||||
async def worklogs_get_summary(
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get project worklog summary."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"worklogs",
|
||||
"get_summary",
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully retrieved worklog summary", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to get worklog summary", result["error"])
|
||||
|
||||
@tool
|
||||
async def worklogs_update(
|
||||
worklog_id: str,
|
||||
issue_id: str,
|
||||
description: Optional[str] = None,
|
||||
duration: Optional[int] = None,
|
||||
created_by: Optional[str] = None,
|
||||
updated_by: Optional[str] = None,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update time entry.
|
||||
|
||||
Args:
|
||||
worklog_id: Worklog ID (required)
|
||||
issue_id: Issue ID (required)
|
||||
description: Worklog description
|
||||
duration: Duration in minutes
|
||||
created_by: User ID who created the worklog
|
||||
updated_by: User ID who updated the worklog
|
||||
project_id: Project ID (optional, auto-filled from context or resolved from issue_id)
|
||||
workspace_slug: Workspace slug (optional, auto-filled from context)
|
||||
"""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
# Programmatically resolve project_id from issue_id if missing or invalid (non-UUID)
|
||||
should_resolve = False
|
||||
if issue_id and not project_id:
|
||||
should_resolve = True
|
||||
elif issue_id and project_id:
|
||||
try:
|
||||
uuid.UUID(str(project_id))
|
||||
except (ValueError, AttributeError):
|
||||
should_resolve = True
|
||||
|
||||
if should_resolve:
|
||||
try:
|
||||
from pi.app.api.v1.helpers.plane_sql_queries import get_issue_identifier_for_artifact
|
||||
|
||||
issue_info = await get_issue_identifier_for_artifact(str(issue_id))
|
||||
if issue_info and issue_info.get("project_id"):
|
||||
project_id = issue_info["project_id"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Build update data
|
||||
update_data: Dict[str, Any] = {}
|
||||
if description is not None:
|
||||
update_data["description"] = description
|
||||
if duration is not None:
|
||||
update_data["duration"] = duration
|
||||
if created_by is not None:
|
||||
update_data["created_by"] = created_by
|
||||
if updated_by is not None:
|
||||
update_data["updated_by"] = updated_by
|
||||
|
||||
result = await method_executor.execute(
|
||||
"worklogs",
|
||||
"update",
|
||||
worklog_id=worklog_id,
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
**update_data,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully updated worklog", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to update worklog", result["error"])
|
||||
|
||||
@tool
|
||||
async def worklogs_delete(
|
||||
worklog_id: str,
|
||||
project_id: Optional[str] = None,
|
||||
workspace_slug: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Delete time entry."""
|
||||
# Auto-fill from context if not provided
|
||||
if workspace_slug is None and "workspace_slug" in context:
|
||||
workspace_slug = context["workspace_slug"]
|
||||
if project_id is None and "project_id" in context:
|
||||
project_id = context["project_id"]
|
||||
|
||||
result = await method_executor.execute(
|
||||
"worklogs",
|
||||
"delete",
|
||||
worklog_id=worklog_id,
|
||||
project_id=project_id,
|
||||
workspace_slug=workspace_slug,
|
||||
)
|
||||
if result["success"]:
|
||||
return PlaneToolBase.format_success_payload("Successfully deleted worklog", result["data"])
|
||||
else:
|
||||
return PlaneToolBase.format_error_payload("Failed to delete worklog", result["error"])
|
||||
|
||||
return [worklogs_create, worklogs_list, worklogs_get_summary, worklogs_update, worklogs_delete]
|
||||
return generate_tools_for_category("worklogs", method_executor, context, WORKLOGS_TOOL_DEFINITIONS)
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
|
||||
# SPDX-License-Identifier: LicenseRef-Plane-Commercial
|
||||
#
|
||||
# Licensed under the Plane Commercial License (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# https://plane.so/legals/eula
|
||||
#
|
||||
# DO NOT remove or modify this notice.
|
||||
# NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
|
||||
"""
|
||||
Workspaces API tools for Plane workspace features management.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
|
||||
from pi import logger
|
||||
from pi.services.actions.tool_generator import generate_tools_for_category
|
||||
from pi.services.actions.tool_metadata import ToolMetadata
|
||||
from pi.services.actions.tool_metadata import ToolParameter
|
||||
|
||||
log = logger.getChild("workspaces")
|
||||
|
||||
# All workspace feature fields - SDK requires all fields to be present
|
||||
WORKSPACE_FEATURE_FIELDS = ["project_grouping", "initiatives", "teams", "customers", "wiki", "pi"]
|
||||
|
||||
|
||||
async def _workspaces_pre_handler(
|
||||
metadata: ToolMetadata,
|
||||
kwargs: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
category: str,
|
||||
method_key: str,
|
||||
method_executor: Any = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Pre-processing handler for workspace tools.
|
||||
|
||||
Handles:
|
||||
- update_features: Fetch current features and merge with update (SDK requires all fields)
|
||||
"""
|
||||
if method_key == "update_features":
|
||||
workspace_slug = kwargs.get("workspace_slug")
|
||||
if not workspace_slug:
|
||||
log.warning("workspace_slug missing for update_features, cannot fetch current features")
|
||||
return kwargs
|
||||
|
||||
if not method_executor:
|
||||
log.warning("method_executor not available, cannot fetch current features")
|
||||
return kwargs
|
||||
|
||||
# Fetch current features using the existing method_executor
|
||||
try:
|
||||
result = await method_executor.execute("workspaces", "get_features", workspace_slug=workspace_slug)
|
||||
|
||||
if result.get("success") and result.get("data"):
|
||||
features_data = result["data"]
|
||||
|
||||
# Merge: current values as defaults, update with provided values
|
||||
for field in WORKSPACE_FEATURE_FIELDS:
|
||||
if field not in kwargs or kwargs[field] is None:
|
||||
kwargs[field] = features_data.get(field, False)
|
||||
|
||||
log.debug(f"Merged workspace features: {kwargs}")
|
||||
else:
|
||||
log.warning(f"Could not fetch current workspace features: {result}")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to fetch current workspace features: {e}")
|
||||
# Don't fail - let SDK validation fail with clear error
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
async def _workspaces_post_handler(
|
||||
metadata: ToolMetadata,
|
||||
result: Dict[str, Any],
|
||||
kwargs: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
method_executor: Any,
|
||||
category: str,
|
||||
method_key: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""Post-processing handler for workspace tools.
|
||||
|
||||
Handles:
|
||||
- Inject workspace metadata into response for entity URL generation
|
||||
"""
|
||||
# For workspace operations, inject workspace info into the result for URL construction
|
||||
workspace_slug = kwargs.get("workspace_slug")
|
||||
|
||||
if workspace_slug and result.get("success"):
|
||||
# Get workspace_id from context - try multiple keys
|
||||
workspace_id = context.get("workspace_id") or context.get("workspace")
|
||||
|
||||
# Ensure the result data has the workspace metadata for URL generation
|
||||
if "data" in result:
|
||||
if isinstance(result["data"], dict):
|
||||
# Inject workspace identifiers if missing
|
||||
if "id" not in result["data"] and workspace_id:
|
||||
result["data"]["id"] = str(workspace_id)
|
||||
if "slug" not in result["data"]:
|
||||
result["data"]["slug"] = workspace_slug
|
||||
if "name" not in result["data"]:
|
||||
# Use slug as a fallback name
|
||||
result["data"]["name"] = workspace_slug
|
||||
else:
|
||||
# Create a minimal data structure for URL generation
|
||||
result["data"] = {
|
||||
"id": str(workspace_id) if workspace_id else None,
|
||||
"slug": workspace_slug,
|
||||
"name": workspace_slug,
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# WORKSPACES TOOL DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
WORKSPACE_TOOL_DEFINITIONS: Dict[str, ToolMetadata] = {
|
||||
"get_features": ToolMetadata(
|
||||
name="workspaces_get_features",
|
||||
description="Get enabled workspace features (initiatives, teams, customers, etc.)",
|
||||
sdk_method="get_workspace_features",
|
||||
returns_entity_type="workspace",
|
||||
post_handler=_workspaces_post_handler,
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
"update_features": ToolMetadata(
|
||||
name="workspaces_update_features",
|
||||
description="Enable or disable workspace features",
|
||||
sdk_method="update_workspace_features",
|
||||
returns_entity_type="workspace",
|
||||
pre_handler=_workspaces_pre_handler,
|
||||
post_handler=_workspaces_post_handler,
|
||||
parameters=[
|
||||
ToolParameter(
|
||||
name="workspace_slug",
|
||||
type="Optional[str]",
|
||||
required=False,
|
||||
description="Workspace slug (auto-detected from context)",
|
||||
auto_fill_from_context=True,
|
||||
),
|
||||
ToolParameter(
|
||||
name="project_grouping",
|
||||
type="Optional[bool]",
|
||||
required=False,
|
||||
description="Enable/disable project grouping feature (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="initiatives",
|
||||
type="Optional[bool]",
|
||||
required=False,
|
||||
description="Enable/disable initiatives feature (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="teams",
|
||||
type="Optional[bool]",
|
||||
required=False,
|
||||
description="Enable/disable teams feature (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="customers",
|
||||
type="Optional[bool]",
|
||||
required=False,
|
||||
description="Enable/disable customers feature (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="wiki",
|
||||
type="Optional[bool]",
|
||||
required=False,
|
||||
description="Enable/disable wiki feature (optional)",
|
||||
),
|
||||
ToolParameter(
|
||||
name="pi",
|
||||
type="Optional[bool]",
|
||||
required=False,
|
||||
description="Enable/disable PI (Plane Intelligence) feature (optional)",
|
||||
),
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOOL FACTORY
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def get_workspace_tools(method_executor, context):
|
||||
"""Return LangChain tools for the workspaces category using auto-generation from metadata."""
|
||||
return generate_tools_for_category(
|
||||
category="workspaces",
|
||||
method_executor=method_executor,
|
||||
context=context,
|
||||
tool_definitions=WORKSPACE_TOOL_DEFINITIONS,
|
||||
)
|
||||
@@ -35,6 +35,13 @@ from pi.services.chat.helpers.placeholder_orchestrator import PlaceholderOrchest
|
||||
|
||||
log = logger.getChild(__name__)
|
||||
|
||||
# Mapping of entity_type to tool category
|
||||
# Some tools have different entity_type than their category (e.g., epic tools are in workitems)
|
||||
ENTITY_TYPE_TO_CATEGORY = {
|
||||
"epic": "workitems", # Epic tools are in workitems category
|
||||
# Add more mappings as needed
|
||||
}
|
||||
|
||||
|
||||
async def get_execution_context(request, chatbot: PlaneChatBot, user_id, db) -> Tuple[str, Optional[str], bool]:
|
||||
"""Extract execution context from request and database."""
|
||||
@@ -79,7 +86,7 @@ class BuildModeToolExecutor:
|
||||
if not workspace_slug:
|
||||
workspace_slug = await get_workspace_slug(str(workspace_id))
|
||||
|
||||
token, project_id, _ = await get_execution_context(request, self.chatbot, user_id, self.db)
|
||||
token, project_id, is_project_chat = await get_execution_context(request, self.chatbot, user_id, self.db)
|
||||
|
||||
# Validate token exists
|
||||
if not token:
|
||||
@@ -101,7 +108,14 @@ class BuildModeToolExecutor:
|
||||
|
||||
method_executor = MethodExecutor(actions_executor)
|
||||
|
||||
context = {"workspace_slug": workspace_slug, "project_id": project_id, "message_id": message_id, "chat_id": chat_id}
|
||||
context = {
|
||||
"workspace_slug": workspace_slug,
|
||||
"workspace_id": str(workspace_id) if workspace_id else None,
|
||||
"project_id": project_id,
|
||||
"message_id": message_id,
|
||||
"chat_id": chat_id,
|
||||
"is_project_chat": is_project_chat,
|
||||
}
|
||||
|
||||
# Execute single action
|
||||
# If single action, execute directly
|
||||
@@ -134,16 +148,25 @@ class BuildModeToolExecutor:
|
||||
args = planned_action["args"]
|
||||
entity_type = planned_action["entity_type"]
|
||||
|
||||
# Map entity_type to actual category (e.g., "epic" -> "workitems")
|
||||
category = ENTITY_TYPE_TO_CATEGORY.get(entity_type, entity_type)
|
||||
|
||||
# Build tool
|
||||
all_category_tools = self.chatbot._build_method_tools(entity_type, method_executor, context)
|
||||
all_category_tools = self.chatbot._build_method_tools(category, method_executor, context)
|
||||
# get the specific tool we need
|
||||
tool = next((t for t in all_category_tools if t.name == tool_name), None)
|
||||
|
||||
if not tool:
|
||||
raise ValueError(f"Tool '{tool_name}' not found in category '{entity_type}'")
|
||||
raise ValueError(f"Tool '{tool_name}' not found in category '{category}' (entity_type: '{entity_type}')")
|
||||
|
||||
# Execute the tool - EXPECT structured payload dict
|
||||
result = await tool.ainvoke(args)
|
||||
log.info(f"[DEBUG] execute_single_action: About to invoke tool '{tool_name}' with args: {args}")
|
||||
try:
|
||||
result = await tool.ainvoke(args)
|
||||
log.info(f"[DEBUG] execute_single_action: Tool '{tool_name}' returned successfully. Result: {result}")
|
||||
except Exception as tool_error:
|
||||
log.error(f"[DEBUG] execute_single_action: Tool '{tool_name}' raised exception: {tool_error}", exc_info=True)
|
||||
raise
|
||||
if not isinstance(result, dict):
|
||||
raise ValueError(f"Tool '{tool_name}' must return a structured payload dict, got {type(result)}")
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import re
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
@@ -91,7 +92,7 @@ async def _inject_urls_into_entities(entities: list[Dict[str, Any]] | str, pendi
|
||||
# This happens when action tools (not retrieval tools) are used
|
||||
if not url_map:
|
||||
from pi import settings
|
||||
from pi.agents.sql_agent.tools import construct_entity_urls_from_db
|
||||
from pi.agents.sql_agent.helpers import construct_entity_urls_from_db
|
||||
|
||||
# Collect entity IDs by type
|
||||
entity_ids_by_type: Dict[str, List[str]] = {"issues": [], "pages": [], "cycles": [], "modules": [], "projects": []}
|
||||
@@ -446,6 +447,21 @@ async def execute_tools_for_build_mode(
|
||||
_has_tool_calls = hasattr(response, "tool_calls") and bool(getattr(response, "tool_calls", None))
|
||||
except Exception:
|
||||
_has_tool_calls = False
|
||||
|
||||
# FAILSAFE: Detect if LLM mistakenly put tool_calls in content instead of using API
|
||||
# This can happen with certain models/prompts - detect and handle gracefully
|
||||
response_content = str(getattr(response, "content", "") or "").strip()
|
||||
if not _has_tool_calls and "```tool_calls" in response_content:
|
||||
log.warning(f"ChatID: {chat_id} - DETECTED: LLM output tool_calls as markdown instead of API. Stripping from content.")
|
||||
# Strip the tool_calls markdown block from content to avoid showing JSON to user
|
||||
# Remove ```tool_calls ... ``` blocks from content
|
||||
cleaned_content = re.sub(r"```tool_calls\s*\n?\[[\s\S]*?\]\s*\n?```", "", response_content)
|
||||
cleaned_content = cleaned_content.strip()
|
||||
# Update response content to cleaned version for streaming
|
||||
if hasattr(response, "content"):
|
||||
response.content = cleaned_content
|
||||
log.info(f"ChatID: {chat_id} - Stripped tool_calls markdown. Cleaned content: {cleaned_content[:200]}...")
|
||||
|
||||
log.info(f"ChatID: {chat_id} - Has Tool Calls: {_has_tool_calls}")
|
||||
|
||||
if _has_tool_calls:
|
||||
@@ -872,6 +888,15 @@ async def execute_tools_for_build_mode(
|
||||
# update _has_tool_calls
|
||||
_has_tool_calls = has_more_tool_calls
|
||||
|
||||
# FAILSAFE: Detect if LLM mistakenly put tool_calls in content in iterations
|
||||
if not _has_tool_calls:
|
||||
iter_content = str(getattr(response, "content", "") or "").strip()
|
||||
if "```tool_calls" in iter_content:
|
||||
log.warning(f"ChatID: {chat_id} - DETECTED (iteration {iteration_count}): LLM output tool_calls as markdown. Stripping.")
|
||||
cleaned_content = re.sub(r"```tool_calls\s*\n?\[[\s\S]*?\]\s*\n?```", "", iter_content).strip()
|
||||
if hasattr(response, "content"):
|
||||
response.content = cleaned_content
|
||||
|
||||
# Log planner decisions for this iteration
|
||||
try:
|
||||
if _has_tool_calls:
|
||||
|
||||
@@ -186,6 +186,12 @@ def extract_tool_params_from_artifact_data(artifact_data: Dict[str, Any], entity
|
||||
"epic": {"cycle_id", "module_ids"},
|
||||
}
|
||||
|
||||
# Fields that are auto-generated by SDK and should never be sent to any tool
|
||||
SDK_AUTO_GENERATED_FIELDS = {"logo_props", "icon_prop", "emoji"}
|
||||
|
||||
# Fields that are project-specific and should not be sent to other entities
|
||||
PROJECT_ONLY_FIELDS = {"cover_image"}
|
||||
|
||||
tool_params = {}
|
||||
unsupported = CREATE_UNSUPPORTED_FIELDS.get(entity_type, set()) if action == "create" else set()
|
||||
|
||||
@@ -217,23 +223,29 @@ def extract_tool_params_from_artifact_data(artifact_data: Dict[str, Any], entity
|
||||
if key in {"entity_info", "artifact_sub_type"}:
|
||||
continue
|
||||
|
||||
# Map artifact field names to tool parameter names FIRST (needed for subsequent checks)
|
||||
mapped_key = ARTIFACT_TO_TOOL_MAPPING.get(key, key)
|
||||
|
||||
# Skip SDK auto-generated fields (logo_props, icon_prop, emoji) for all entities
|
||||
if key in SDK_AUTO_GENERATED_FIELDS or mapped_key in SDK_AUTO_GENERATED_FIELDS:
|
||||
log.debug(f"Skipping SDK auto-generated field '{key}' for {entity_type}")
|
||||
continue
|
||||
|
||||
# Skip project-only fields for non-project entities
|
||||
if entity_type != "project" and (key in PROJECT_ONLY_FIELDS or mapped_key in PROJECT_ONLY_FIELDS):
|
||||
log.debug(f"Skipping project-only field '{key}' for {entity_type}")
|
||||
continue
|
||||
|
||||
# Filter unsupported fields for this entity type and action
|
||||
if key in unsupported:
|
||||
log.warning(f"Field '{key}' cannot be set during {entity_type} {action}, skipping")
|
||||
continue
|
||||
|
||||
# Map artifact field names to tool parameter names
|
||||
mapped_key = ARTIFACT_TO_TOOL_MAPPING.get(key, key)
|
||||
|
||||
if mapped_key == "project_lead" and isinstance(value, dict) and value.get("id"):
|
||||
value = value["id"]
|
||||
|
||||
tool_params[mapped_key] = value
|
||||
|
||||
# Project logo/icon compatibility:
|
||||
if mapped_key == "logo_props" and "icon_prop" not in tool_params:
|
||||
tool_params["icon_prop"] = value
|
||||
|
||||
log.debug(f"Extracted tool params for {entity_type} {action}: {list(tool_params.keys())}")
|
||||
return tool_params
|
||||
|
||||
@@ -271,22 +283,64 @@ async def load_artifacts(request_data: List[ArtifactData], db: AsyncSession) ->
|
||||
# Transform edited artifact data from frontend schema to tool parameters
|
||||
tool_args = extract_tool_params_from_artifact_data(req_item.action_data or {}, entity_type, action)
|
||||
|
||||
# For UPDATE actions, preserve the entity ID from the original artifact
|
||||
# Get original tool args for preserving context fields
|
||||
original_tool_args = artifact.data.get("tool_args_raw", {})
|
||||
|
||||
# PRESERVE CONTEXT FIELDS for all actions (these are never editable but required for execution)
|
||||
# workspace_slug and project_id are context, not user-editable fields
|
||||
if "workspace_slug" in original_tool_args and "workspace_slug" not in tool_args:
|
||||
tool_args["workspace_slug"] = original_tool_args["workspace_slug"]
|
||||
log.debug(f"Preserved workspace_slug={original_tool_args["workspace_slug"]} for {entity_type} {action}")
|
||||
|
||||
if "project_id" in original_tool_args and "project_id" not in tool_args:
|
||||
tool_args["project_id"] = original_tool_args["project_id"]
|
||||
log.debug(f"Preserved project_id={original_tool_args["project_id"]} for {entity_type} {action}")
|
||||
|
||||
# PRESERVE NON-EDITABLE PLANNING PARAMETERS
|
||||
# These are planning-time decisions that aren't exposed in the edit UI but are critical for execution
|
||||
NON_EDITABLE_PARAMS = {
|
||||
# Project feature flags (planning-time decisions)
|
||||
"cycle_view",
|
||||
"module_view",
|
||||
"page_view",
|
||||
"intake_view",
|
||||
"is_issue_type_enabled",
|
||||
"is_time_tracking_enabled",
|
||||
"issue_views_view",
|
||||
"guest_view_all_features",
|
||||
# Project update_features params (must be preserved from plan)
|
||||
"epics",
|
||||
"modules",
|
||||
"cycles",
|
||||
"views",
|
||||
"pages",
|
||||
"intakes",
|
||||
"work_item_types",
|
||||
# Other planning parameters
|
||||
"timezone",
|
||||
"archive_in",
|
||||
"close_in",
|
||||
"external_id",
|
||||
"external_source",
|
||||
# User/owner assignments that may be auto-set
|
||||
"owned_by",
|
||||
"project_lead",
|
||||
"default_assignee",
|
||||
}
|
||||
|
||||
for param in NON_EDITABLE_PARAMS:
|
||||
if param in original_tool_args and param not in tool_args:
|
||||
tool_args[param] = original_tool_args[param]
|
||||
log.debug(f"Preserved non-editable param {param}={original_tool_args[param]} for {entity_type} {action}")
|
||||
|
||||
# For UPDATE actions, also preserve the entity ID being updated
|
||||
# The frontend edits fields but doesn't include the entity ID being updated
|
||||
if action == "update":
|
||||
original_tool_args = artifact.data.get("tool_args_raw", {})
|
||||
|
||||
# Preserve the entity's ID field (e.g., issue_id, cycle_id, module_id)
|
||||
id_field = ENTITY_ID_FIELD_MAP.get(entity_type)
|
||||
if id_field and id_field in original_tool_args and id_field not in tool_args:
|
||||
tool_args[id_field] = original_tool_args[id_field]
|
||||
log.debug(f"Preserved {id_field}={original_tool_args[id_field]} for {entity_type} update")
|
||||
|
||||
# Also preserve project_id if it's not in the edited data
|
||||
# (many update tools require it as context even though it's not being changed)
|
||||
if "project_id" in original_tool_args and "project_id" not in tool_args:
|
||||
tool_args["project_id"] = original_tool_args["project_id"]
|
||||
log.info(f"Preserved project_id={original_tool_args["project_id"]} for {entity_type} update")
|
||||
else:
|
||||
# Use original planned tool arguments
|
||||
tool_args = artifact.data.get("tool_args_raw", {})
|
||||
|
||||
@@ -296,6 +296,7 @@ def build_planning_tools(
|
||||
"module",
|
||||
"worklogs",
|
||||
"worklog",
|
||||
"workitems", # Added to support epic feature checking via projects_retrieve
|
||||
"epics",
|
||||
"epic",
|
||||
"intake",
|
||||
@@ -307,6 +308,15 @@ def build_planning_tools(
|
||||
"pages",
|
||||
"page",
|
||||
]
|
||||
workspace_scoped_cats = [
|
||||
"initiatives",
|
||||
"initiative",
|
||||
"teamspaces",
|
||||
"teamspace",
|
||||
"customers",
|
||||
"customer",
|
||||
]
|
||||
|
||||
all_method_tools: List[Any] = []
|
||||
built_categories: List[str] = []
|
||||
for sel in selections_list:
|
||||
@@ -331,6 +341,14 @@ def build_planning_tools(
|
||||
tools_for_project = [t for t in tools_for_project if getattr(t, "name", "") in ["projects_retrieve", "projects_update"]]
|
||||
all_method_tools.extend(tools_for_project)
|
||||
|
||||
if cat in workspace_scoped_cats:
|
||||
tools_for_workspace = chatbot_instance._build_planning_method_tools("workspaces", method_executor, context)
|
||||
# remove all tools except workspaces_get_features and workspaces_update_features
|
||||
tools_for_workspace = [
|
||||
t for t in tools_for_workspace if getattr(t, "name", "") in ["workspaces_get_features", "workspaces_update_features"]
|
||||
]
|
||||
all_method_tools.extend(tools_for_workspace)
|
||||
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to build tools for category {cat}: {e}")
|
||||
|
||||
|
||||
@@ -22,19 +22,54 @@ async def infer_selected_entity(args: Dict[str, Any], context: Dict[str, Any], e
|
||||
"""
|
||||
Generic best-effort entity inference for execute-action results.
|
||||
|
||||
Some tools don’t return an `entity` payload even on success. When we can,
|
||||
Some tools don't return an `entity` payload even on success. When we can,
|
||||
infer the selected entity from request args + context (e.g., issue_id).
|
||||
"""
|
||||
try:
|
||||
workspace_slug = context.get("workspace_slug")
|
||||
hint = (entity_type_hint or "").strip().lower()
|
||||
|
||||
# Fast-path inference for entities where URL format is fully deterministic from args/context.
|
||||
# This is especially important for "relationship" tools (e.g. add work-items to cycle)
|
||||
# where the API response does not include the cycle/module payload.
|
||||
base = str(settings.plane_api.FRONTEND_URL or "").rstrip("/")
|
||||
project_id = args.get("project_id") or context.get("project_id")
|
||||
|
||||
cycle_id = args.get("cycle_id")
|
||||
if workspace_slug and project_id and cycle_id:
|
||||
return {
|
||||
"entity_url": f"{base}/{workspace_slug}/projects/{project_id}/cycles/{cycle_id}/",
|
||||
"entity_name": None,
|
||||
"entity_type": "cycle" if hint in {"", "cycle"} else hint,
|
||||
"entity_id": str(cycle_id),
|
||||
}
|
||||
|
||||
module_id = args.get("module_id")
|
||||
if workspace_slug and project_id and module_id:
|
||||
return {
|
||||
"entity_url": f"{base}/{workspace_slug}/projects/{project_id}/modules/{module_id}/",
|
||||
"entity_name": None,
|
||||
"entity_type": "module" if hint in {"", "module"} else hint,
|
||||
"entity_id": str(module_id),
|
||||
}
|
||||
|
||||
# Project URL: /{workspace}/projects/{project}/overview/
|
||||
if workspace_slug and project_id and hint == "project":
|
||||
return {
|
||||
"entity_url": f"{base}/{workspace_slug}/projects/{project_id}/overview/",
|
||||
"entity_name": None,
|
||||
"entity_type": "project",
|
||||
"entity_id": str(project_id),
|
||||
}
|
||||
|
||||
# Work-item inference (needs identifier resolution for best UX)
|
||||
issue_id = args.get("issue_id")
|
||||
if workspace_slug and issue_id:
|
||||
from pi.agents.sql_agent.tools import construct_action_entity_url
|
||||
from pi.agents.sql_agent.helpers import construct_action_entity_url
|
||||
|
||||
url_info = await construct_action_entity_url({"id": str(issue_id)}, "workitem", str(workspace_slug), settings.plane_api.FRONTEND_URL)
|
||||
if not url_info or not isinstance(url_info, dict):
|
||||
return None
|
||||
hint = (entity_type_hint or "").strip().lower()
|
||||
|
||||
subresource_id_fields = {
|
||||
"comment": "comment_id",
|
||||
|
||||
@@ -188,7 +188,7 @@ class PlaceholderOrchestrator:
|
||||
|
||||
remaining.remove(action)
|
||||
except Exception as e:
|
||||
log.error(f"Failed to execute action {action.get("tool_name")}: {e}")
|
||||
log.error(f"Failed to execute action {action.get("tool_name")}: {e}", exc_info=True)
|
||||
# Format error message for user-friendly display
|
||||
formatted_error = _format_validation_error(str(e))
|
||||
# Create failure result
|
||||
@@ -349,15 +349,16 @@ class PlaceholderOrchestrator:
|
||||
if not entity_type or not entity_name:
|
||||
raise ValueError(f"Failed to parse placeholder: {placeholder}")
|
||||
|
||||
# Build lookup key
|
||||
lookup_key = f"{entity_type}:{entity_name}"
|
||||
# Build lookup key (lowercase for case-insensitive matching)
|
||||
lookup_key = f"{entity_type}:{entity_name.lower()}"
|
||||
name_key = entity_name.lower()
|
||||
|
||||
# Try exact match first
|
||||
# Try exact match first (keys are stored lowercase)
|
||||
if lookup_key in self.execution_context:
|
||||
context_entry = self.execution_context[lookup_key]
|
||||
elif entity_name in self.execution_context:
|
||||
elif name_key in self.execution_context:
|
||||
# Fallback to name-only lookup
|
||||
context_entry = self.execution_context[entity_name]
|
||||
context_entry = self.execution_context[name_key]
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Entity '{entity_name}' (type: {entity_type}) not found in execution context. "
|
||||
@@ -498,11 +499,24 @@ IMPORTANT:
|
||||
|
||||
# Build tool if not cached
|
||||
if tool_name not in self.tools_cache:
|
||||
entity_type = action.get("entity_type")
|
||||
if entity_type:
|
||||
tools = self.chatbot._build_method_tools(entity_type, self.method_executor, self.context)
|
||||
for tool in tools:
|
||||
self.tools_cache[tool.name] = tool
|
||||
# Special cases where tool name doesn't follow {category}_{method} pattern
|
||||
SPECIAL_TOOL_CATEGORIES = {
|
||||
"create_epic": "workitems",
|
||||
"update_epic": "workitems",
|
||||
}
|
||||
|
||||
# Extract category from tool name
|
||||
if tool_name in SPECIAL_TOOL_CATEGORIES:
|
||||
category = SPECIAL_TOOL_CATEGORIES[tool_name]
|
||||
else:
|
||||
# Standard pattern: {category}_{method}
|
||||
# e.g., "initiatives_create_label" -> "initiatives"
|
||||
category = tool_name.split("_")[0] if "_" in tool_name else tool_name
|
||||
|
||||
log.info(f"Building tools for category '{category}' (tool: {tool_name})")
|
||||
tools = self.chatbot._build_method_tools(category, self.method_executor, self.context)
|
||||
for tool in tools:
|
||||
self.tools_cache[tool.name] = tool
|
||||
|
||||
# Get the tool
|
||||
tool = self.tools_cache.get(tool_name)
|
||||
@@ -510,7 +524,13 @@ IMPORTANT:
|
||||
raise ValueError(f"Tool '{tool_name}' not found")
|
||||
|
||||
# Execute the tool
|
||||
result = await tool.ainvoke(args)
|
||||
log.info(f"[DEBUG] About to invoke tool '{tool_name}' with args: {args}")
|
||||
try:
|
||||
result = await tool.ainvoke(args)
|
||||
log.info(f"[DEBUG] Tool '{tool_name}' invocation completed. Result type: {type(result)}")
|
||||
except Exception as tool_error:
|
||||
log.error(f"[DEBUG] Tool '{tool_name}' raised exception during ainvoke: {tool_error}", exc_info=True)
|
||||
raise
|
||||
|
||||
if not isinstance(result, dict):
|
||||
raise ValueError(f"Tool '{tool_name}' must return a dict, got {type(result)}")
|
||||
@@ -560,20 +580,22 @@ IMPORTANT:
|
||||
return
|
||||
|
||||
# Get the planned name from action args
|
||||
planned_name = action.get("args", {}).get("name")
|
||||
# Check both 'name' and 'display_name' (properties use display_name)
|
||||
planned_name = action.get("args", {}).get("name") or action.get("args", {}).get("display_name")
|
||||
|
||||
# Build all possible lookup keys
|
||||
# Build all possible lookup keys (use lowercase for case-insensitive matching)
|
||||
keys = [
|
||||
f"{entity_type}:{actual_name}", # e.g., "project:Motor Bike47119"
|
||||
actual_name, # Allow lookup by actual name alone
|
||||
f"{entity_type}:{actual_name.lower()}", # e.g., "property:severity"
|
||||
actual_name.lower(), # Allow lookup by actual name alone
|
||||
]
|
||||
|
||||
# IMPORTANT: Also store under planned name if different from actual
|
||||
# This handles SDK name modifications on conflict (409)
|
||||
if planned_name and planned_name != actual_name:
|
||||
keys.append(f"{entity_type}:{planned_name}") # e.g., "project:Motor Bike"
|
||||
keys.append(planned_name) # Allow lookup by planned name
|
||||
log.info(f"⚠️ Name mismatch: planned='{planned_name}' vs actual='{actual_name}'. " f"Storing under both names for lookup.")
|
||||
# This handles SDK name modifications on conflict (409) AND case differences
|
||||
if planned_name:
|
||||
keys.append(f"{entity_type}:{planned_name.lower()}") # e.g., "property:severity"
|
||||
keys.append(planned_name.lower()) # Allow lookup by planned name
|
||||
if planned_name.lower() != actual_name.lower():
|
||||
log.info(f"⚠️ Name mismatch: planned='{planned_name}' vs actual='{actual_name}'. " f"Storing under both names for lookup.")
|
||||
|
||||
# Store under all keys
|
||||
for key in keys:
|
||||
@@ -583,7 +605,7 @@ IMPORTANT:
|
||||
|
||||
def _can_resolve_from_context(self, entity_type: Optional[str], entity_name: Optional[str]) -> bool:
|
||||
"""
|
||||
Check if we have this entity in our execution context.
|
||||
Check if we have this entity in our execution context (case-insensitive).
|
||||
|
||||
Args:
|
||||
entity_type: Type of entity (e.g., "project")
|
||||
@@ -595,9 +617,10 @@ IMPORTANT:
|
||||
if not entity_type or not entity_name:
|
||||
return False
|
||||
|
||||
# Check if we have this entity stored
|
||||
key = f"{entity_type}:{entity_name}"
|
||||
return key in self.execution_context or entity_name in self.execution_context
|
||||
# Check if we have this entity stored (case-insensitive)
|
||||
key = f"{entity_type}:{entity_name.lower()}"
|
||||
name_key = entity_name.lower()
|
||||
return key in self.execution_context or name_key in self.execution_context
|
||||
|
||||
def _is_placeholder(self, value: Any) -> bool:
|
||||
"""Check if value is a placeholder string."""
|
||||
|
||||
@@ -78,11 +78,12 @@ TOOL_NAME_TO_CATEGORY_MAP: Dict[str, Dict[str, str]] = {
|
||||
"pages_create_project_page": {"entity_type": "page", "action_type": "create", "front_facing_name": "Create Project Page"},
|
||||
"pages_create_workspace_page": {"entity_type": "page", "action_type": "create", "front_facing_name": "Create Workspace Page"},
|
||||
# Projects
|
||||
"projects_archive": {"entity_type": "project", "action_type": "archive", "front_facing_name": "Archive Project"},
|
||||
"projects_create": {"entity_type": "project", "action_type": "create", "front_facing_name": "Create Project"},
|
||||
"projects_unarchive": {"entity_type": "project", "action_type": "unarchive", "front_facing_name": "Unarchive Project"},
|
||||
"projects_delete": {"entity_type": "project", "action_type": "delete", "front_facing_name": "Delete Project"},
|
||||
"projects_update": {"entity_type": "project", "action_type": "update", "front_facing_name": "Update Project"},
|
||||
"projects_retrieve": {"entity_type": "project", "action_type": "retrieve", "front_facing_name": "Retrieve Project"},
|
||||
"projects_get_features": {"entity_type": "project", "action_type": "get", "front_facing_name": "Get Project Features"},
|
||||
"projects_update_features": {"entity_type": "project", "action_type": "update", "front_facing_name": "Update Project Features"},
|
||||
# Properties
|
||||
"properties_create": {"entity_type": "property", "action_type": "create", "front_facing_name": "Create Property"},
|
||||
"properties_create_option": {"entity_type": "property", "action_type": "create_option", "front_facing_name": "Create Property Option"},
|
||||
@@ -98,6 +99,52 @@ TOOL_NAME_TO_CATEGORY_MAP: Dict[str, Dict[str, str]] = {
|
||||
"types_create": {"entity_type": "type", "action_type": "create", "front_facing_name": "Create Type"},
|
||||
"types_delete": {"entity_type": "type", "action_type": "delete", "front_facing_name": "Delete Type"},
|
||||
"types_update": {"entity_type": "type", "action_type": "update", "front_facing_name": "Update Type"},
|
||||
# Initiatives (SDK v0.2.1)
|
||||
"initiatives_create": {"entity_type": "initiative", "action_type": "create", "front_facing_name": "Create Initiative"},
|
||||
"initiatives_list": {"entity_type": "initiative", "action_type": "list", "front_facing_name": "List Initiatives"},
|
||||
"initiatives_retrieve": {"entity_type": "initiative", "action_type": "retrieve", "front_facing_name": "Retrieve Initiative"},
|
||||
"initiatives_update": {"entity_type": "initiative", "action_type": "update", "front_facing_name": "Update Initiative"},
|
||||
"initiatives_delete": {"entity_type": "initiative", "action_type": "delete", "front_facing_name": "Delete Initiative"},
|
||||
"initiatives_create_label": {"entity_type": "initiative_label", "action_type": "create", "front_facing_name": "Create Initiative Label"},
|
||||
"initiatives_list_labels": {"entity_type": "initiative_label", "action_type": "list", "front_facing_name": "List Initiative Labels"},
|
||||
"initiatives_retrieve_label": {"entity_type": "initiative_label", "action_type": "retrieve", "front_facing_name": "Retrieve Initiative Label"},
|
||||
"initiatives_update_label": {"entity_type": "initiative_label", "action_type": "update", "front_facing_name": "Update Initiative Label"},
|
||||
"initiatives_delete_label": {"entity_type": "initiative_label", "action_type": "delete", "front_facing_name": "Delete Initiative Label"},
|
||||
"initiatives_add_labels": {"entity_type": "initiative", "action_type": "add", "front_facing_name": "Add Labels to Initiative"},
|
||||
"initiatives_remove_labels": {"entity_type": "initiative", "action_type": "remove", "front_facing_name": "Remove Labels from Initiative"},
|
||||
"initiatives_add_projects": {"entity_type": "initiative", "action_type": "add", "front_facing_name": "Add Projects to Initiative"},
|
||||
"initiatives_list_projects": {"entity_type": "initiative", "action_type": "list", "front_facing_name": "List Initiative Projects"},
|
||||
"initiatives_remove_projects": {"entity_type": "initiative", "action_type": "remove", "front_facing_name": "Remove Projects from Initiative"},
|
||||
"initiatives_add_epics": {"entity_type": "initiative", "action_type": "add", "front_facing_name": "Add Epics to Initiative"},
|
||||
"initiatives_list_epics": {"entity_type": "initiative", "action_type": "list", "front_facing_name": "List Initiative Epics"},
|
||||
"initiatives_remove_epics": {"entity_type": "initiative", "action_type": "remove", "front_facing_name": "Remove Epics from Initiative"},
|
||||
# Teamspaces (SDK v0.2.1)
|
||||
"teamspaces_create": {"entity_type": "teamspace", "action_type": "create", "front_facing_name": "Create Teamspace"},
|
||||
"teamspaces_list": {"entity_type": "teamspace", "action_type": "list", "front_facing_name": "List Teamspaces"},
|
||||
"teamspaces_retrieve": {"entity_type": "teamspace", "action_type": "retrieve", "front_facing_name": "Retrieve Teamspace"},
|
||||
"teamspaces_update": {"entity_type": "teamspace", "action_type": "update", "front_facing_name": "Update Teamspace"},
|
||||
"teamspaces_delete": {"entity_type": "teamspace", "action_type": "delete", "front_facing_name": "Delete Teamspace"},
|
||||
"teamspaces_add_members": {"entity_type": "teamspace", "action_type": "add", "front_facing_name": "Add Members to Teamspace"},
|
||||
"teamspaces_list_members": {"entity_type": "teamspace", "action_type": "list", "front_facing_name": "List Teamspace Members"},
|
||||
"teamspaces_remove_members": {"entity_type": "teamspace", "action_type": "remove", "front_facing_name": "Remove Members from Teamspace"},
|
||||
"teamspaces_add_projects": {"entity_type": "teamspace", "action_type": "add", "front_facing_name": "Add Projects to Teamspace"},
|
||||
"teamspaces_list_projects": {"entity_type": "teamspace", "action_type": "list", "front_facing_name": "List Teamspace Projects"},
|
||||
"teamspaces_remove_projects": {"entity_type": "teamspace", "action_type": "remove", "front_facing_name": "Remove Projects from Teamspace"},
|
||||
# Stickies (SDK v0.2.1)
|
||||
"stickies_create": {"entity_type": "sticky", "action_type": "create", "front_facing_name": "Create Sticky Note"},
|
||||
"stickies_list": {"entity_type": "sticky", "action_type": "list", "front_facing_name": "List Sticky Notes"},
|
||||
"stickies_retrieve": {"entity_type": "sticky", "action_type": "retrieve", "front_facing_name": "Retrieve Sticky Note"},
|
||||
"stickies_update": {"entity_type": "sticky", "action_type": "update", "front_facing_name": "Update Sticky Note"},
|
||||
"stickies_delete": {"entity_type": "sticky", "action_type": "delete", "front_facing_name": "Delete Sticky Note"},
|
||||
# Customers (SDK v0.2.1)
|
||||
"customers_create": {"entity_type": "customer", "action_type": "create", "front_facing_name": "Create Customer"},
|
||||
"customers_list": {"entity_type": "customer", "action_type": "list", "front_facing_name": "List Customers"},
|
||||
"customers_retrieve": {"entity_type": "customer", "action_type": "retrieve", "front_facing_name": "Retrieve Customer"},
|
||||
"customers_update": {"entity_type": "customer", "action_type": "update", "front_facing_name": "Update Customer"},
|
||||
"customers_delete": {"entity_type": "customer", "action_type": "delete", "front_facing_name": "Delete Customer"},
|
||||
# Workspaces (SDK v0.2.2 - features)
|
||||
"workspaces_get_features": {"entity_type": "workspace", "action_type": "get", "front_facing_name": "Get Workspace Features"},
|
||||
"workspaces_update_features": {"entity_type": "workspace", "action_type": "update", "front_facing_name": "Update Workspace Features"},
|
||||
# Workitems and Epics
|
||||
"create_epic": {"entity_type": "epic", "action_type": "create", "front_facing_name": "Create Epic"},
|
||||
"update_epic": {"entity_type": "epic", "action_type": "update", "front_facing_name": "Update Epic"},
|
||||
@@ -636,7 +683,7 @@ work_tree_instructions_normal_response = """
|
||||
**IF the user's request is informational/retrieval-only (questions, searches, listing, checking status):**
|
||||
1. Use retrieval/search tools to gather the requested information
|
||||
2. Provide a detailed, elaborate, and neatly formatted answer in the content section based on the retrieved data. Do NOT be brief.
|
||||
3. Do NOT plan any modifying actions - just return tool_calls for retrieval, then answer in your content
|
||||
3. Do NOT plan any modifying actions - just invoke retrieval tools, then answer in your content
|
||||
4. **Formatting Requirements**:
|
||||
While formatting the answer in the final content section, use the following rules:
|
||||
- Use "work-item" (not "issue") and "unique key" (not "Issue ID") terminology
|
||||
@@ -649,8 +696,8 @@ work_tree_instructions_normal_response = """
|
||||
|
||||
**IF the user's request requires modifying data (create, update, delete, move, assign, etc.):**
|
||||
1. Use retrieval/search tools to gather necessary information (IDs, etc.)
|
||||
2. **AND** plan at least one MODIFYING ACTION using tool_calls (create, update, delete, add, remove, move, etc.)
|
||||
3. You CANNOT stop after just searching/retrieving - you MUST plan the modifying actions with tool_calls
|
||||
2. **AND** invoke at least one MODIFYING ACTION tool (create, update, delete, add, remove, move, etc.)
|
||||
3. You CANNOT stop after just searching/retrieving - you MUST invoke the modifying action tools
|
||||
4. Provide a very brief summary of what you planned in your content
|
||||
5. **Formatting Requirements**:
|
||||
While formatting the action plan in the final content section, use the following rules:
|
||||
@@ -667,14 +714,14 @@ work_tree_instructions_normal_response = """
|
||||
- Do NOT create workaround entities (like workitems) to satisfy these requests
|
||||
- Do NOT plan any actions - just provide the explanation in your content
|
||||
|
||||
Note: The system uses absence of tool_calls as the signal to stop and deliver your content as the final answer. For modification requests, include at least one write tool_call in your plan.
|
||||
Note: The system detects when you stop invoking tools and delivers your content as the final answer. For modification requests, you MUST invoke at least one write action tool.
|
||||
""" # noqa E501
|
||||
|
||||
work_tree_instructions_app_response = f"""
|
||||
**IF the user's request is informational/retrieval-only (questions, searches, listing, checking status):**
|
||||
1. Use retrieval/search tools to gather the requested information
|
||||
2. Provide a detailed, and elaborate answer in PLAIN TEXT ONLY in the 'text' field in the provide_final_answer_for_app tool based on the retrieved data. Do NOT be brief.
|
||||
3. Do NOT plan any modifying actions - just return tool_calls for retrieval, then call provide_final_answer_for_app
|
||||
3. Do NOT plan any modifying actions - just invoke retrieval tools, then call provide_final_answer_for_app
|
||||
4. **Formatting Requirements - CRITICAL FOR EXTERNAL APP (e.g., Slack) RENDERING**:
|
||||
While generating the answer in the 'text' field in the provide_final_answer_for_app tool, use the following rules:
|
||||
- **ABSOLUTELY NO MARKDOWN FORMATTING**: Do not use **bold**, *italic*, headers, or any markdown syntax
|
||||
@@ -697,8 +744,8 @@ work_tree_instructions_app_response = f"""
|
||||
|
||||
**IF the user's request requires modifying data (create, update, delete, move, assign, etc.):**
|
||||
1. Use retrieval/search tools to gather necessary information (IDs, etc.)
|
||||
2. **AND** plan at least one MODIFYING ACTION using tool_calls (create, update, delete, add, remove, move, etc.)
|
||||
3. You CANNOT stop after just searching/retrieving - you MUST plan the modifying actions with tool_calls
|
||||
2. **AND** invoke at least one MODIFYING ACTION tool (create, update, delete, add, remove, move, etc.)
|
||||
3. You CANNOT stop after just searching/retrieving - you MUST invoke the modifying action tools
|
||||
4. Provide a very brief summary of what you planned in your content
|
||||
5. **Formatting Requirements**:
|
||||
While formatting the action plan in the final content section, use the following rules:
|
||||
@@ -714,7 +761,7 @@ work_tree_instructions_app_response = f"""
|
||||
- Do NOT create workaround entities (like workitems) to satisfy these requests
|
||||
- Do NOT plan any actions - just provide the explanation in your content
|
||||
|
||||
Note: This is a response for consumption by an external app. So, calling provide_final_answer_for_app signals completion for retrieval-only requests. For modification requests, include at least one write tool_call in your plan.
|
||||
Note: This is a response for consumption by an external app. Calling provide_final_answer_for_app signals completion for retrieval-only requests. For modification requests, you MUST invoke at least one write action tool.
|
||||
""" # noqa E501
|
||||
|
||||
|
||||
@@ -777,6 +824,32 @@ Use retrieval tools to gather information, then plan the modifying actions based
|
||||
- Do not try to execute actions - only plan them
|
||||
- For multi-property updates, resolve all required IDs first, then set all properties in a single tool call
|
||||
|
||||
**MANDATORY REASONING AND COMMUNICATION (CRITICAL - REQUIRED FOR EVERY TOOL CALL):**
|
||||
- **BEFORE EACH TOOL CALL**: You MUST explain your reasoning and intent
|
||||
- State what information you're trying to gather or what action you're planning
|
||||
- Explain why this tool is necessary for completing the user's request
|
||||
- Describe what you expect to get from the tool and how you'll use it
|
||||
- Example: "The user wants to check workitems assigned to Anil. First, I need to search for the user 'Anil' to get their ID, then I'll use that ID to filter workitems." # noqa: E501
|
||||
- **AFTER EACH TOOL CALL**: You MUST provide a brief summary of what you learned
|
||||
- Summarize key information obtained from the tool
|
||||
- Explain how this information helps with the next step
|
||||
- If the tool returned unexpected results, explain how you'll adapt
|
||||
- Example: "Found user Anil Kumar with ID xyz-123. Now I'll use this ID to search for workitems assigned to them." # noqa: E501
|
||||
- **THINKING OUT LOUD**: Express your thought process naturally
|
||||
- Share your understanding of the user's request
|
||||
- Explain your strategy for accomplishing the task
|
||||
- Mention any assumptions you're making
|
||||
- This reasoning is MANDATORY and helps with debugging and understanding your decision-making process
|
||||
- NEVER skip the reasoning - it's essential for transparency and troubleshooting
|
||||
|
||||
**CRITICAL: OPTIONAL PARAMETERS POLICY (APPLIES TO ALL TOOLS):**
|
||||
- Only provide optional parameters that are EXPLICITLY requested by the user or clearly implied by their intent
|
||||
- DO NOT auto-fill optional fields with "sensible defaults" or inferred values
|
||||
- **DO NOT ask the user for clarification if optional parameters are missing.**
|
||||
- **IF an optional parameter is not provided, you MUST leave it empty and let the API use its default.**
|
||||
- This applies to ALL optional fields across ALL entity types (description, priority, state, assignees, dates, etc.)
|
||||
- When in doubt, omit the field.
|
||||
|
||||
**WORKSPACE-LEVEL QUERIES (EFFICIENCY):**
|
||||
|- When query is related to workitems and spans ALL projects without specifying a particular project or projects
|
||||
| - DO NOT: call `list_member_projects` then query each project with `structured_db_tool` separately
|
||||
@@ -787,28 +860,76 @@ Use retrieval tools to gather information, then plan the modifying actions based
|
||||
|- For project-specific queries, include the specific `project_id`
|
||||
|
||||
**PROJECT FEATURES CHECK (CRITICAL - MANDATORY BEFORE CREATING PROJECT-SCOPED ENTITIES):**
|
||||
- Cycles, modules, pages, workitem types, views, intake, and time-tracking (worklogs) are project-level features that are enabled/disabled on a per-project basis.
|
||||
- **MANDATORY WORKFLOW**: Before creating ANY of these entities (cycles_create, modules_create, pages_create_*, worklogs_create, etc.), you MUST:
|
||||
1. First get the project_id (via search_project_by_name or search_project_by_identifier if not already known)
|
||||
2. **THEN** call `projects_retrieve` with that project_id to check if the feature is enabled
|
||||
3. **AFTER checking**: You MUST plan the required actions using tool_calls:
|
||||
- If the feature IS enabled: Plan the creation action (e.g., `cycles_create`, `modules_create`, `worklogs_create`)
|
||||
- If the feature is NOT enabled: Plan BOTH `projects_update` (to enable the feature) AND the creation action (e.g., `cycles_create`, `modules_create`, `worklogs_create`)
|
||||
4. **CRITICAL**: After `projects_retrieve` completes, you CANNOT stop - you MUST continue planning the modifying actions. Do NOT return only text - you MUST return tool_calls for the planned actions.
|
||||
5. **REMEMBER**: `projects_retrieve` is ONLY for information gathering. The user's actual request (create cycle/module/page/worklog) is NOT complete until you plan the creation action with tool_calls. Do NOT stop after retrieval - the task is incomplete without planning the creation.
|
||||
- **CRITICAL**: This check is NON-NEGOTIABLE. Never skip the `projects_retrieve` step when creating cycles, modules, pages, or other project-scoped features.
|
||||
- **CRITICAL**: After retrieving project features, you MUST plan the actions - do NOT stop after retrieval. The user requested a modification (create cycle/module/page/worklog), so you MUST plan it with tool_calls.
|
||||
- **EXCEPTION (NEW PROJECT IN CURRENT PLAN)**: If the target project is being CREATED in this same plan and does not yet have a real UUID, do NOT call `projects_retrieve` during planning. Use placeholders for downstream actions and defer any feature checks until after execution or when working with an existing project that has a UUID.
|
||||
- Cycles, modules, pages, workitem types, views, intake, epics (a special workitem type), and time-tracking (worklogs) are project-level features that are enabled/disabled on a per-project basis.
|
||||
- **MANDATORY WORKFLOW**: Before creating ANY of these entities (cycles_create, create_epic, modules_create, pages_create_*, worklogs_create, intake_create, etc.), you MUST:
|
||||
1. First get the project_id (UUID) (via search_project_by_name or search_project_by_identifier (Identifier looks like 'PROJ', 'WEB' etc. It is not a UUID) if not already known)
|
||||
2. **THEN** call `projects_retrieve` with that project_id to check if the feature is enabled.
|
||||
- **WARNING**: Do NOT stop to ask for clarification on optional entity fields (e.g., description, priority) before this check.
|
||||
- **WARNING**: You must perform this check even if you think you need more information for the creaton.
|
||||
- **WARNING**: You must perform this check even if you already have all required IDs (issue_id, project_id, etc.). Having IDs does NOT exempt you from checking if the feature is enabled.
|
||||
3. **AFTER checking**: You MUST invoke the required action tools:
|
||||
- If the feature IS enabled: Plan the creation action (e.g., `cycles_create`, `create_epic`, `modules_create`, `types_create`, `worklogs_create`, `intake_create`, etc.)
|
||||
- If the feature is NOT enabled: Plan BOTH `projects_update` (to enable the feature) AND the creation action (e.g., `cycles_create`, `create_epic`, `modules_create`, `types_create`, `worklogs_create`, `intake_create`, etc.)
|
||||
4. **CRITICAL**: After `projects_retrieve` completes, you CANNOT stop - you MUST continue invoking the modifying action tools. Do NOT return only text - you MUST invoke the action tools.
|
||||
- **CRITICAL**: This check is NON-NEGOTIABLE. Never skip the `projects_retrieve` step when creating cycles, modules, pages, types, intake items, or other project-scoped features.
|
||||
- **CONSEQUENCE**: Skipping `projects_retrieve` and directly calling creation tools (e.g., worklogs_create, cycles_create, intake_create) WILL result in a 404 error if the feature is disabled. Always check first.
|
||||
- **MULTI-ACTION REQUESTS**: When handling requests with multiple actions, if ANY action requires a feature check, you MUST call `projects_retrieve` FIRST before planning any of the actions.
|
||||
- **EXCEPTION (NEW PROJECT IN CURRENT PLAN)**: If the target project is being CREATED in this same plan and does not yet have a real UUID:
|
||||
- Do NOT call `projects_retrieve` during planning (the project doesn't exist yet to retrieve)
|
||||
- Instead, you MUST enable the required feature flag during `projects_create` itself
|
||||
- **Example**: If planning to create a project AND a cycle in the same plan:
|
||||
1. Call `projects_create` with `cycle_view=True` (to enable cycles feature)
|
||||
2. Call `cycles_create` with `project_id="<id of project: project-name>"`
|
||||
- **Available feature flags for projects_create**:
|
||||
- `epics` (boolean): Enable epics feature
|
||||
- `cycle_view` (boolean): Enable cycles feature
|
||||
- `module_view` (boolean): Enable modules feature
|
||||
- `page_view` (boolean): Enable pages feature
|
||||
- `intake_view` (boolean): Enable intake feature
|
||||
- `is_issue_type_enabled` (boolean): Enable workitem types feature
|
||||
- `is_time_tracking_enabled` (boolean): Enable time-tracking (worklogs) feature
|
||||
- `issue_views_view` (boolean): Enable workitem views feature
|
||||
- Available tools:
|
||||
- `projects_retrieve` tool to get details of the project features (MUST call before creating project-scoped entities)
|
||||
- `projects_update` tool to update the project features (MUST include in plan if feature needs to be enabled)
|
||||
- `projects_create` tool to create a new project with any/all of these features based on the user's request
|
||||
- `projects_retrieve` tool to get details of the project features (MUST call before creating project-scoped entities for EXISTING projects)
|
||||
- `projects_update` tool to update the project features (MUST include in plan if feature needs to be enabled for EXISTING projects)
|
||||
- `projects_create` tool to create a new project with any/all of these features enabled based on the user's request
|
||||
|
||||
**HARD CONSTRAINTS FOR TOOL CALLS (NON-NEGOTIABLE):**
|
||||
- If the request involves modification of data like creating, updating, adding, removing, moving, assigning, archiving, or unarchiving: you MUST return one or more tool_calls for the corresponding action tools (e.g., cycles_create, workitems_create, modules_add_work_items). Returning only text is incorrect.
|
||||
**WORKSPACE FEATURES CHECK (CRITICAL - MANDATORY BEFORE CREATING WORKSPACE-SCOPED ENTITIES):**
|
||||
- Workspace-level features are enabled/disabled on a per-workspace basis and fall into two categories:
|
||||
- **Features with entity operations**: initiatives, teams (teamspaces), customers - these have create/update/delete tools
|
||||
- **Feature toggles only**: wiki, Plane AI (formerly, Pi - Plane Intelligence), project_grouping - these are settings without entity operations
|
||||
- **MANDATORY WORKFLOW for features with entity operations** (initiatives, teams, customers):
|
||||
Before creating entities via `initiatives_create`, `teamspaces_create`, or `customers_create`, you MUST:
|
||||
1. **FIRST** call `workspaces_get_features` to check if the required feature is enabled
|
||||
2. **AFTER checking**: You MUST invoke the required action tools:
|
||||
- If the feature IS enabled: Plan the creation action (e.g., `initiatives_create`, `teamspaces_create`, `customers_create`)
|
||||
- If the feature is NOT enabled: Plan BOTH `workspaces_update_features` (to enable the feature) AND the creation action
|
||||
3. **CRITICAL**: After `workspaces_get_features` completes, you CANNOT stop - you MUST continue invoking the modifying action tools. Do NOT return only text - you MUST invoke the action tools.
|
||||
- **CRITICAL**: This check is NON-NEGOTIABLE. Never skip the `workspaces_get_features` step when creating initiatives, teamspaces, or customers.
|
||||
- **Feature-to-Entity/Operation Mapping**:
|
||||
- `initiatives` feature → `initiatives_create`, `initiatives_update`, `initiatives_delete`, `initiatives_add_projects`, etc.
|
||||
- `teams` feature → `teamspaces_create`, `teamspaces_update`, `teamspaces_delete`, `teamspaces_add_members`, etc.
|
||||
- `customers` feature → `customers_create`, `customers_update`, `customers_delete`, etc.
|
||||
- `wiki` feature → Feature toggle only (enables wiki functionality in UI, no entity operations available)
|
||||
- `pi` feature → Feature toggle only (enables Plane AI in UI, no entity operations available)
|
||||
- `project_grouping` feature → Feature toggle only (allows grouping projects in workspace UI, no entity operations available)
|
||||
- **Available feature flags for workspaces_update_features**:
|
||||
- `initiatives` (boolean): Enable/disable initiatives feature
|
||||
- `teams` (boolean): Enable/disable teamspaces feature
|
||||
- `customers` (boolean): Enable/disable customers feature
|
||||
- `wiki` (boolean): Enable/disable wiki feature toggle
|
||||
- `pi` (boolean): Enable/disable Plane AI feature toggle
|
||||
- `project_grouping` (boolean): Enable/disable project grouping feature toggle
|
||||
- Available tools:
|
||||
- `workspaces_get_features`: Get current workspace feature flags (MUST call before creating initiatives/teamspaces/customers)
|
||||
- `workspaces_update_features`: Enable/disable workspace features (MUST include in plan if feature needs to be enabled)
|
||||
|
||||
|
||||
**HARD CONSTRAINTS FOR TOOL INVOCATION (NON-NEGOTIABLE):**
|
||||
- If the request involves modification of data like creating, updating, adding, removing, moving, assigning, archiving, or unarchiving: you MUST invoke the corresponding action tools (e.g., cycles_create, workitems_create, modules_add_work_items). Returning only text is incorrect.
|
||||
- For modification requests, keep text content brief - do NOT provide meta commentary like "I will plan..." or ask user to click 'Confirm'
|
||||
- System handles approval automatically
|
||||
- If required information is missing, call ask_for_clarification instead of skipping tool_calls.
|
||||
- If required information is missing, call ask_for_clarification instead of skipping tool invocation.
|
||||
|
||||
|
||||
**RETRIEVAL RESULT RELEVANCE IN PLANNING ACTIONS FOR MODIFYING REQUESTS (CRITICAL):**
|
||||
@@ -958,6 +1079,13 @@ After resolving project_id for cycles/modules/pages creation:
|
||||
|
||||
**EFFICIENCY RULE**: Always try to set as many properties as possible during creation to minimize API calls.
|
||||
|
||||
**MANDATORY REQUIREMENT - REASONING FOR EVERY TOOL CALL:**
|
||||
You MUST provide clear reasoning in your response content BEFORE and AFTER each tool call:
|
||||
- BEFORE: Explain what you're about to do and why (e.g., "Let me first find user Anil's details to get their ID...")
|
||||
- AFTER: Summarize what you found and your next step (e.g., "Found Anil Kumar (ID: xyz-123). Now searching for workitems assigned to them...")
|
||||
- This reasoning is NOT optional - it's required for transparency and helps users understand your process
|
||||
- Even for simple searches, explain your thinking (e.g., "Searching for project 'Mobile' to get its UUID for creating workitems...")
|
||||
- Provide this in the content field surrounding your tool invocations
|
||||
|
||||
**IMPORTANT**: Analyze the user's request carefully to identify ALL required actions, not just the obvious ones.
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from pi import logger
|
||||
from pi import settings
|
||||
from pi.agents.sql_agent.tools import construct_entity_urls_from_db
|
||||
from pi.agents.sql_agent.tools import extract_ids_from_sql_result
|
||||
from pi.agents.sql_agent.tools import format_as_bullet_points
|
||||
from pi.agents.sql_agent.helpers import construct_entity_urls_from_db
|
||||
from pi.agents.sql_agent.helpers import extract_ids_from_sql_result
|
||||
from pi.agents.sql_agent.helpers import format_as_bullet_points
|
||||
from pi.app.models.enums import MessageMetaStepType
|
||||
|
||||
# Import new modular tools from actions service
|
||||
@@ -67,10 +67,6 @@ class ChatKit(AttachmentMixin):
|
||||
from pi.services.llm.llms import create_anthropic_llm
|
||||
from pi.services.llm.llms import create_openai_llm
|
||||
|
||||
# Store original switch_llm value to determine if custom model was requested
|
||||
# GPT-5 variants and gpt-5.1 are handled separately, not as custom models
|
||||
use_custom_model = switch_llm is not None and not (switch_llm.startswith("gpt-5") or switch_llm == "gpt-5.1")
|
||||
|
||||
# Model name mapping for user-friendly names to actual LiteLLM model names
|
||||
claude_model_name_mapping = {
|
||||
"claude-sonnet-4": settings.llm_model.LITE_LLM_CLAUDE_SONNET_4,
|
||||
@@ -136,36 +132,11 @@ class ChatKit(AttachmentMixin):
|
||||
TOOL_LLM = switch_llm
|
||||
tool_config = LLMConfig(model=TOOL_LLM, temperature=0.2, streaming=tool_llm_streaming)
|
||||
|
||||
# Initialize LLMs using LLMFactory with switch_llm support
|
||||
if use_custom_model:
|
||||
# For custom models, try LiteLLM configs with model-type naming convention
|
||||
self.llm = llms.LLMFactory.get_default_llm(f"{switch_llm}-default")
|
||||
self.stream_llm = llms.LLMFactory.get_stream_llm(f"{switch_llm}-stream")
|
||||
self.decomposer_llm = llms.LLMFactory.get_decomposer_llm(f"{switch_llm}-decomposer")
|
||||
self.fast_llm = llms.LLMFactory.get_fast_llm(streaming=False, model_name=f"{switch_llm}-fast")
|
||||
else:
|
||||
# Use default global instances, but for GPT-5 variants create dynamic instances
|
||||
if switch_llm == "gpt-5-standard":
|
||||
self.llm = llms.LLMFactory.get_default_llm("gpt5_standard_default")
|
||||
self.stream_llm = llms.LLMFactory.get_stream_llm("gpt5_standard_stream")
|
||||
self.decomposer_llm = llms.LLMFactory.get_decomposer_llm("gpt5_standard_default")
|
||||
self.fast_llm = llms.LLMFactory.get_fast_llm(streaming=False, model_name="gpt5_standard_default")
|
||||
elif switch_llm == "gpt-5-fast":
|
||||
self.llm = llms.LLMFactory.get_default_llm("gpt5_fast_default")
|
||||
self.stream_llm = llms.LLMFactory.get_stream_llm("gpt5_fast_stream")
|
||||
self.decomposer_llm = llms.LLMFactory.get_decomposer_llm("gpt5_fast_default")
|
||||
self.fast_llm = llms.LLMFactory.get_fast_llm(streaming=False, model_name="gpt5_fast_default")
|
||||
elif switch_llm == "gpt-5.1":
|
||||
self.llm = llms.LLMFactory.get_default_llm("gpt5_1_default")
|
||||
self.stream_llm = llms.LLMFactory.get_stream_llm("gpt5_1_stream")
|
||||
self.decomposer_llm = llms.LLMFactory.get_decomposer_llm("gpt5_1_default")
|
||||
self.fast_llm = llms.LLMFactory.get_fast_llm(streaming=False, model_name="gpt5_1_default")
|
||||
else:
|
||||
# Use default global instances
|
||||
self.llm = llms.llm
|
||||
self.stream_llm = llms.stream_llm
|
||||
self.decomposer_llm = llms.decomposer_llm
|
||||
self.fast_llm = llms.fast_llm
|
||||
# Initialize LLMs using LLMFactory with centralized model name mapping
|
||||
self.llm = llms.LLMFactory.get_default_llm(switch_llm)
|
||||
self.stream_llm = llms.LLMFactory.get_stream_llm(switch_llm)
|
||||
self.decomposer_llm = llms.LLMFactory.get_decomposer_llm(switch_llm)
|
||||
self.fast_llm = llms.LLMFactory.get_fast_llm(streaming=False, model_name=switch_llm)
|
||||
|
||||
self.switch_llm = switch_llm
|
||||
# Get chat LLM - reasoning effort is now determined by model name
|
||||
|
||||
@@ -29,7 +29,7 @@ CORE BUILDING BLOCKS
|
||||
10. Team Drafts & Inbox - Drafts store half-written work items; Inbox is a catch-all notification and mention feed.
|
||||
|
||||
AI ASSISTANT
|
||||
11. Plane AI (Formerly called, Pi a moniker for Plane Intelligence) - AI-powered assistant that helps users interact with Plane using natural language. Pi can search work items, analyze project data, access documentation, and provide insights through conversational queries. It also has action capabilities like create, update, delete, assign, move, etc.
|
||||
11. Plane AI (Formerly called Pi, a moniker for Plane Intelligence) - AI-powered assistant that helps users interact with Plane using natural language. Pi can search work items, analyze project data, access documentation, and provide insights through conversational queries. It also has action capabilities like create, update, delete, assign, move, etc.
|
||||
|
||||
PROJECT & WORK MANAGEMENT
|
||||
12. Work Item Types - schema-driven custom types with per-type fields (replaces "Issue Types").
|
||||
@@ -59,18 +59,17 @@ VISUALIZATION & INSIGHT
|
||||
|
||||
NAVIGATION & PRODUCTIVITY
|
||||
29. Power K - global command palette (⌘/Ctrl + K) for fuzzy jumping and quick actions.
|
||||
30. Hyper Mode - optional local SQLite cache that slashes load times for big workspaces.
|
||||
31. Mobile Apps - Android 5+ and iOS 13+ companion apps with project, work-item, cycles, pages & inbox support.
|
||||
30. Mobile Apps - Android 5+ and iOS 13+ companion apps with project, work-item, cycles, pages & inbox support.
|
||||
|
||||
INTEGRATION & EXTENSIBILITY
|
||||
32. Importers - migrate from Jira, Linear, Asana, or CSV.
|
||||
33. Integrations - GitHub (PR ↔ Work Item sync), GitLab, Slack slash-commands + notifications.
|
||||
34. API & Webhooks - REST-style JSON API plus outgoing webhooks.
|
||||
35. Self-Host vs Cloud - run Plane Cloud (SaaS) or deploy the open-source stack on-prem; Hyper Mode requires HTTPS.
|
||||
31. Importers - migrate from Jira, Linear, Asana, or CSV.
|
||||
32. Integrations - GitHub (PR ↔ Work Item sync), GitLab, Slack slash-commands + notifications.
|
||||
33. SDK, API & Webhooks - Plane SDK (Python and Node.js), REST-style JSON API plus outgoing webhooks.
|
||||
34. Self-Host vs Cloud - run Plane Cloud (SaaS) or deploy the open-source stack on-prem.
|
||||
|
||||
PERMISSIONS & BILLING
|
||||
36. Roles - Admin, Member, Guest per workspace/project; fine-grained on features.
|
||||
37. Billing Plans - Free, Pro, Enterprise; feature flags like Epics, Initiatives, Dashboards noted as paid-only.
|
||||
35. Roles - Admin, Member, Guest per workspace/project; fine-grained on features.
|
||||
36. Billing Plans - Free, Pro, Business, Enterprise; feature flags like Epics, Initiatives, Dashboards noted as paid-only.
|
||||
|
||||
TERMINOLOGY BRIDGE — cross-tool aliases
|
||||
(Use this glossary to map Plane objects to the familiar terms you'll see in other tools or in general)
|
||||
@@ -78,7 +77,7 @@ TERMINOLOGY BRIDGE — cross-tool aliases
|
||||
• Work Items → tasks, issues, tickets, user stories
|
||||
• States → status buckets (Backlog/Todo, In Progress, Done/Closed)
|
||||
• Cycles → sprints, iterations
|
||||
• Modules → components, milestones, feature buckets
|
||||
• Modules → components, feature buckets
|
||||
• Epics → large user stories / epics
|
||||
• Initiatives → programs, portfolio objectives
|
||||
• Projects → projects, boards
|
||||
@@ -139,22 +138,26 @@ Your task: Based on the user's intent and any advisory text (like method lists)
|
||||
- labels: Create/list/update/delete labels
|
||||
- states: Create/list/update/delete states
|
||||
- modules: Create/list/update/delete modules, add/remove workitems to/from modules
|
||||
- pages: Create and manage project and workspace pages/documentation
|
||||
- assets: Create/list/update/delete assets
|
||||
- pages: Create and manage project and workspace pages/documentation (rich text, fonts, images, styles)
|
||||
- users: Get current user information
|
||||
- intake: Handle intake forms, guest submissions, triage workflow
|
||||
- members: Workspace and project member management, listings
|
||||
- activity: Track work item activities, history, and audit logs
|
||||
- attachments: File attachments on work items, upload and management
|
||||
- comments: Comments and discussions on work items
|
||||
- links: External links and references on work items
|
||||
- properties: Custom properties and fields for work items
|
||||
- types: Custom work item types (bug, task, story, etc.)
|
||||
- worklogs: Time tracking and work logs
|
||||
- initiatives: Create/list/update/delete initiatives (cross-project goal containers)
|
||||
- teamspaces: Manage teamspaces (team containers for projects and cycles)
|
||||
- stickies: Create/list/update/delete sticky notes (short plain text notes, like 3M stickers, no rich media)
|
||||
- customers: Manage customer records and CRM integrations
|
||||
- workspaces: Workspace-level operations and feature management
|
||||
- retrieval_tools: text2sql, vector_search_tool, pages_search_tool, docs_search_tool
|
||||
|
||||
Rules:
|
||||
- "wiki", "knowledge base", "kb", "handbook", "runbook", and "notes" are all synonyms for pages. Route these to the pages category, not projects.
|
||||
- "wiki", "knowledge base", "kb", "handbook", "runbook", and "notes" (BUT NOT "sticky notes") are all synonyms for pages. Route these to the pages category, not projects.
|
||||
- "sticky", "stickies", "sticky note" should ALWAYS be routed to the stickies category, not pages.
|
||||
- If the user says "create a page/wiki" without specifying a project, prefer the pages category and plan a workspace-level page.
|
||||
- If a project is explicitly mentioned (by name or identifier), route to pages and plan a project-level page (resolve project UUID first).
|
||||
- Select multiple categories when the intent spans multiple domains (e.g., list work-items then create a cycle).
|
||||
@@ -198,7 +201,7 @@ You MUST return a JSON object with a "selections" key containing an array of sel
|
||||
**Rules:**
|
||||
- ALWAYS wrap selections in a "selections" array, even for a single category
|
||||
- If no categories are appropriate, return: {{{{"selections": []}}}}
|
||||
- Valid category values: workitems, projects, cycles, labels, states, modules, pages, assets, users, intake, members, activity, attachments, comments, links, properties, types, worklogs, retrieval_tools
|
||||
- Valid category values: workitems, projects, cycles, labels, states, modules, pages, users, intake, members, activity, comments, links, properties, types, worklogs, initiatives, teamspaces, stickies, customers, workspaces, retrieval_tools
|
||||
- No explanation outside the JSON structure
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
+112
-28
@@ -452,53 +452,137 @@ _LITE_LLM_CONFIGS = {
|
||||
}
|
||||
|
||||
|
||||
def _get_config_name_for_model(model_name: Optional[str], config_type: str) -> Optional[str]:
|
||||
"""Map user-facing model name to internal config name.
|
||||
|
||||
Args:
|
||||
model_name: User-facing model name (e.g., "gpt-5.2", "claude-sonnet-4-0", None)
|
||||
config_type: Type of config ("default", "stream", "decomposer", "fast")
|
||||
|
||||
Returns:
|
||||
Internal config name to lookup in _DEFAULT_CONFIGS, _ANTHROPIC_CONFIGS, etc.
|
||||
Returns None if no mapping exists (will use global defaults).
|
||||
"""
|
||||
if not model_name:
|
||||
return config_type # Use base config type: "default", "stream", etc.
|
||||
|
||||
# Map GPT-5 variants to their config names
|
||||
gpt5_mappings = {
|
||||
"gpt-5-standard": {
|
||||
"default": "gpt5_standard_default",
|
||||
"stream": "gpt5_standard_stream",
|
||||
"decomposer": "gpt5_standard_default",
|
||||
"fast": "gpt5_standard_default",
|
||||
},
|
||||
"gpt-5-fast": {
|
||||
"default": "gpt5_fast_default",
|
||||
"stream": "gpt5_fast_stream",
|
||||
"decomposer": "gpt5_fast_default",
|
||||
"fast": "gpt5_fast_default",
|
||||
},
|
||||
"gpt-5.1": {
|
||||
"default": "gpt5_1_default",
|
||||
"stream": "gpt5_1_stream",
|
||||
"decomposer": "gpt5_1_default",
|
||||
"fast": "gpt5_1_default",
|
||||
},
|
||||
"gpt-5.2": {
|
||||
"default": "gpt5_2_default",
|
||||
"stream": "gpt5_2_stream",
|
||||
"decomposer": "gpt5_2_default",
|
||||
"fast": "gpt5_2_default",
|
||||
},
|
||||
}
|
||||
|
||||
# Map Claude models to their config names
|
||||
claude_mappings = {
|
||||
"claude-sonnet-4": {
|
||||
"default": "claude-sonnet-4-default",
|
||||
"stream": "claude-sonnet-4-stream",
|
||||
"decomposer": "claude-sonnet-4-decomposer",
|
||||
"fast": "claude-sonnet-4-fast",
|
||||
},
|
||||
"claude-sonnet-4-0": {
|
||||
"default": "claude-sonnet-4-0-default",
|
||||
"stream": "claude-sonnet-4-0-stream",
|
||||
"decomposer": "claude-sonnet-4-0-decomposer",
|
||||
"fast": "claude-sonnet-4-0-fast",
|
||||
},
|
||||
"claude-sonnet-4-5": {
|
||||
"default": "claude-sonnet-4-5-default",
|
||||
"stream": "claude-sonnet-4-5-stream",
|
||||
"decomposer": "claude-sonnet-4-5-decomposer",
|
||||
"fast": "claude-sonnet-4-5-fast",
|
||||
},
|
||||
}
|
||||
|
||||
# Check GPT-5 mappings first
|
||||
if model_name in gpt5_mappings:
|
||||
return gpt5_mappings[model_name].get(config_type)
|
||||
|
||||
# Check Claude mappings
|
||||
if model_name in claude_mappings:
|
||||
return claude_mappings[model_name].get(config_type)
|
||||
|
||||
# For other models, return None (will fall back to defaults)
|
||||
return None
|
||||
|
||||
|
||||
# Backward compatibility factory methods
|
||||
class LLMFactory:
|
||||
@classmethod
|
||||
def get_default_llm(cls, model_name: Optional[str] = None) -> Any:
|
||||
if model_name in _ANTHROPIC_CONFIGS.keys():
|
||||
return create_anthropic_llm(_ANTHROPIC_CONFIGS[model_name])
|
||||
elif model_name in _LITE_LLM_CONFIGS.keys():
|
||||
return create_openai_llm(_LITE_LLM_CONFIGS[model_name])
|
||||
elif model_name in _DEFAULT_CONFIGS.keys():
|
||||
config = _DEFAULT_CONFIGS[model_name]
|
||||
return create_openai_llm(config)
|
||||
# Map user-facing model name to config name
|
||||
config_name = _get_config_name_for_model(model_name, "default")
|
||||
|
||||
if config_name and config_name in _ANTHROPIC_CONFIGS:
|
||||
return create_anthropic_llm(_ANTHROPIC_CONFIGS[config_name])
|
||||
elif config_name and config_name in _LITE_LLM_CONFIGS:
|
||||
return create_openai_llm(_LITE_LLM_CONFIGS[config_name])
|
||||
elif config_name and config_name in _DEFAULT_CONFIGS:
|
||||
return create_openai_llm(_DEFAULT_CONFIGS[config_name])
|
||||
else:
|
||||
return create_openai_llm(_DEFAULT_CONFIGS["default"])
|
||||
|
||||
@classmethod
|
||||
def get_stream_llm(cls, model_name: Optional[str] = None) -> Any:
|
||||
if model_name in _ANTHROPIC_CONFIGS.keys():
|
||||
return create_anthropic_llm(_ANTHROPIC_CONFIGS[model_name])
|
||||
elif model_name in _LITE_LLM_CONFIGS.keys():
|
||||
return create_openai_llm(_LITE_LLM_CONFIGS[model_name])
|
||||
elif model_name in _DEFAULT_CONFIGS.keys():
|
||||
config = _DEFAULT_CONFIGS[model_name]
|
||||
return create_openai_llm(config)
|
||||
# Map user-facing model name to config name
|
||||
config_name = _get_config_name_for_model(model_name, "stream")
|
||||
|
||||
if config_name and config_name in _ANTHROPIC_CONFIGS:
|
||||
return create_anthropic_llm(_ANTHROPIC_CONFIGS[config_name])
|
||||
elif config_name and config_name in _LITE_LLM_CONFIGS:
|
||||
return create_openai_llm(_LITE_LLM_CONFIGS[config_name])
|
||||
elif config_name and config_name in _DEFAULT_CONFIGS:
|
||||
return create_openai_llm(_DEFAULT_CONFIGS[config_name])
|
||||
else:
|
||||
return create_openai_llm(_DEFAULT_CONFIGS["stream"])
|
||||
|
||||
@classmethod
|
||||
def get_decomposer_llm(cls, model_name: Optional[str] = None) -> Any:
|
||||
if model_name in _ANTHROPIC_CONFIGS.keys():
|
||||
return create_anthropic_llm(_ANTHROPIC_CONFIGS[model_name])
|
||||
elif model_name in _LITE_LLM_CONFIGS.keys():
|
||||
return create_openai_llm(_LITE_LLM_CONFIGS[model_name])
|
||||
elif model_name in _DEFAULT_CONFIGS.keys():
|
||||
config = _DEFAULT_CONFIGS[model_name]
|
||||
return create_openai_llm(config)
|
||||
# Map user-facing model name to config name
|
||||
config_name = _get_config_name_for_model(model_name, "decomposer")
|
||||
|
||||
if config_name and config_name in _ANTHROPIC_CONFIGS:
|
||||
return create_anthropic_llm(_ANTHROPIC_CONFIGS[config_name])
|
||||
elif config_name and config_name in _LITE_LLM_CONFIGS:
|
||||
return create_openai_llm(_LITE_LLM_CONFIGS[config_name])
|
||||
elif config_name and config_name in _DEFAULT_CONFIGS:
|
||||
return create_openai_llm(_DEFAULT_CONFIGS[config_name])
|
||||
else:
|
||||
return create_openai_llm(_DEFAULT_CONFIGS["decomposer"])
|
||||
|
||||
@classmethod
|
||||
def get_fast_llm(cls, streaming: bool = False, model_name: Optional[str] = None) -> Any:
|
||||
if model_name in _ANTHROPIC_CONFIGS.keys():
|
||||
return create_anthropic_llm(_ANTHROPIC_CONFIGS[model_name])
|
||||
elif model_name in _LITE_LLM_CONFIGS.keys():
|
||||
return create_openai_llm(_LITE_LLM_CONFIGS[model_name])
|
||||
elif model_name in _DEFAULT_CONFIGS.keys():
|
||||
config = _DEFAULT_CONFIGS[model_name]
|
||||
return create_openai_llm(config)
|
||||
# Map user-facing model name to config name
|
||||
config_name = _get_config_name_for_model(model_name, "fast")
|
||||
|
||||
if config_name and config_name in _ANTHROPIC_CONFIGS:
|
||||
return create_anthropic_llm(_ANTHROPIC_CONFIGS[config_name])
|
||||
elif config_name and config_name in _LITE_LLM_CONFIGS:
|
||||
return create_openai_llm(_LITE_LLM_CONFIGS[config_name])
|
||||
elif config_name and config_name in _DEFAULT_CONFIGS:
|
||||
return create_openai_llm(_DEFAULT_CONFIGS[config_name])
|
||||
else:
|
||||
config_key = "fast_stream" if streaming else "fast"
|
||||
return create_openai_llm(_DEFAULT_CONFIGS[config_key])
|
||||
|
||||
@@ -26,7 +26,7 @@ from sqlalchemy import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from pi import logger
|
||||
from pi.agents.sql_agent.tools import format_as_bullet_points
|
||||
from pi.agents.sql_agent.helpers import format_as_bullet_points
|
||||
from pi.app.models import Chat
|
||||
from pi.app.models import Message
|
||||
from pi.app.models import MessageFlowStep
|
||||
|
||||
@@ -108,17 +108,20 @@ class ActionCategorySelection(BaseModel):
|
||||
"states",
|
||||
"modules",
|
||||
"pages",
|
||||
"assets",
|
||||
"users",
|
||||
"intake",
|
||||
"members",
|
||||
"activity",
|
||||
"attachments",
|
||||
"comments",
|
||||
"links",
|
||||
"properties",
|
||||
"types",
|
||||
"worklogs",
|
||||
"initiatives",
|
||||
"teamspaces",
|
||||
"stickies",
|
||||
"customers",
|
||||
"workspaces",
|
||||
"retrieval_tools",
|
||||
]
|
||||
rationale: Optional[str] = None
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023-present Plane Software, Inc.
|
||||
# SPDX-License-Identifier: LicenseRef-Plane-Commercial
|
||||
#
|
||||
# Licensed under the Plane Commercial License (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
# https://plane.so/legals/eula
|
||||
#
|
||||
# DO NOT remove or modify this notice.
|
||||
# NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited.
|
||||
|
||||
"""Comprehensive category-by-category SDK audit"""
|
||||
|
||||
import sys
|
||||
from typing import TypedDict
|
||||
|
||||
sys.path.insert(0, "/Users/hemasunderchintada/plane/plane-ee/apps/pi")
|
||||
|
||||
from plane import PlaneClient
|
||||
|
||||
from pi.services.actions.plane_sdk_adapter import PlaneSDKAdapter
|
||||
|
||||
|
||||
class AuditResult(TypedDict):
|
||||
category: str
|
||||
status: str
|
||||
sdk_count: int
|
||||
adapter_count: int
|
||||
matched: int
|
||||
missing: list[str]
|
||||
phantom: list[str]
|
||||
|
||||
|
||||
client = PlaneClient(api_key="test", base_url="https://api.plane.so")
|
||||
|
||||
# Define all categories to check
|
||||
categories = {
|
||||
"projects": (client.projects, ["_project"]),
|
||||
"cycles": (client.cycles, ["_cycle"]),
|
||||
"modules": (client.modules, ["_module"]),
|
||||
"work_items": (client.work_items, ["_work_item", "_workitem"]),
|
||||
"labels": (client.labels, ["_label"]),
|
||||
"states": (client.states, ["_state"]),
|
||||
"pages": (client.pages, ["_page"]),
|
||||
"users": (client.users, ["_user"]),
|
||||
}
|
||||
|
||||
print("=" * 80)
|
||||
print("COMPREHENSIVE CATEGORY-BY-CATEGORY SDK AUDIT")
|
||||
print("=" * 80)
|
||||
|
||||
all_results: list[AuditResult] = []
|
||||
|
||||
for cat_name, (sdk_obj, adapter_patterns) in categories.items():
|
||||
# Get actual SDK methods
|
||||
sdk_methods = sorted([m for m in dir(sdk_obj) if not m.startswith("_") and m not in ["base_path", "config", "session"]])
|
||||
|
||||
# Get adapter methods
|
||||
adapter_methods = []
|
||||
for name in dir(PlaneSDKAdapter):
|
||||
if name.startswith("_") or name == "client":
|
||||
continue
|
||||
# Check if method matches this category
|
||||
for pattern in adapter_patterns:
|
||||
if pattern in name.lower():
|
||||
adapter_methods.append(name)
|
||||
break
|
||||
|
||||
adapter_methods = sorted(set(adapter_methods))
|
||||
|
||||
# Map adapter methods to SDK methods
|
||||
matched = []
|
||||
unmatched_adapter = []
|
||||
unmatched_sdk = []
|
||||
|
||||
for sdk_m in sdk_methods:
|
||||
# Try to find matching adapter method
|
||||
# Pattern: category + sdk_method -> e.g., create_project for 'create'
|
||||
expected_adapter = f"{sdk_m}_{cat_name[:-1]}" if cat_name.endswith("s") else f"{sdk_m}_{cat_name}"
|
||||
if expected_adapter in adapter_methods:
|
||||
matched.append((sdk_m, expected_adapter))
|
||||
else:
|
||||
# Try other patterns
|
||||
found = False
|
||||
for am in adapter_methods:
|
||||
if sdk_m in am or am.endswith(f"_{sdk_m}"):
|
||||
matched.append((sdk_m, am))
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
unmatched_sdk.append(sdk_m)
|
||||
|
||||
# Find adapter methods not matched
|
||||
matched_adapter_methods = [m[1] for m in matched]
|
||||
unmatched_adapter = [m for m in adapter_methods if m not in matched_adapter_methods]
|
||||
|
||||
# Print results
|
||||
status = "✅" if len(sdk_methods) == len(matched) and len(unmatched_adapter) == 0 else "❌"
|
||||
print(f"\n{status} {cat_name.upper()}")
|
||||
print(f" Actual SDK: {len(sdk_methods)} methods")
|
||||
print(f" Adapter: {len(adapter_methods)} methods ({len(matched)} matched)")
|
||||
|
||||
if unmatched_sdk:
|
||||
print(" ⚠️ Missing in adapter:")
|
||||
for m in unmatched_sdk:
|
||||
print(f" - {m}")
|
||||
|
||||
if unmatched_adapter:
|
||||
print(" ⚠️ Phantom in adapter:")
|
||||
for m in unmatched_adapter:
|
||||
print(f" - {m}")
|
||||
|
||||
all_results.append({
|
||||
"category": cat_name,
|
||||
"status": status,
|
||||
"sdk_count": len(sdk_methods),
|
||||
"adapter_count": len(adapter_methods),
|
||||
"matched": len(matched),
|
||||
"missing": unmatched_sdk,
|
||||
"phantom": unmatched_adapter,
|
||||
})
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 80)
|
||||
print("SUMMARY")
|
||||
print("=" * 80)
|
||||
|
||||
total_issues = sum(len(r["missing"]) + len(r["phantom"]) for r in all_results)
|
||||
perfect = [r for r in all_results if r["status"] == "✅"]
|
||||
|
||||
print(f"\nPerfect categories: {len(perfect)}/{len(all_results)}")
|
||||
print(f"Total issues found: {total_issues}")
|
||||
|
||||
if total_issues == 0:
|
||||
print("\n✅ ✅ ✅ ALL CATEGORIES HAVE 1-1 PARITY ✅ ✅ ✅")
|
||||
else:
|
||||
print("\n⚠️ Categories needing attention:")
|
||||
for r in all_results:
|
||||
if r["status"] == "❌":
|
||||
print(f" - {r["category"]}: {len(r["missing"])} missing, {len(r["phantom"])} phantom")
|
||||
@@ -3,7 +3,7 @@ fastapi==0.111.1
|
||||
uvicorn==0.38.0
|
||||
requests==2.32.5
|
||||
pydantic==2.8.2
|
||||
plane-sdk==0.2.0
|
||||
plane-sdk==0.2.3
|
||||
openai==2.8.1
|
||||
html2text==2024.2.26
|
||||
python-json-logger==3.3.0
|
||||
|
||||
@@ -705,7 +705,7 @@ class MobileAPITester:
|
||||
payload["workspace_id"] = self.config.workspace_id
|
||||
|
||||
v1_resp, v1_err = self._make_request("POST", v1_url, json_data=payload)
|
||||
|
||||
|
||||
# V2 uses query params
|
||||
v2_params = {"is_project_chat": False}
|
||||
if self.config.workspace_id:
|
||||
@@ -1058,7 +1058,7 @@ class MobileAPITester:
|
||||
differences_serializable = json.loads(json.dumps(str(result.differences)))
|
||||
except Exception:
|
||||
differences_serializable = str(result.differences)
|
||||
|
||||
|
||||
results_data.append(
|
||||
{
|
||||
"endpoint_name": result.endpoint_name,
|
||||
|
||||
Reference in New Issue
Block a user