feat: add team-aware CRM queries and project tracking
This commit is contained in:
+187
-9
@@ -210,7 +210,7 @@ def create_server(
|
||||
] = None,
|
||||
owner: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional owner name, username, email, or English name to filter customers and follow-ups."),
|
||||
Field(description="Optional user to filter customer ownership and follow-up participation."),
|
||||
] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return CRM totals, common customer distributions, and recent follow-ups."""
|
||||
@@ -233,6 +233,73 @@ def create_server(
|
||||
"""Return candidate NocoBase users for an owner filter before querying CRM data."""
|
||||
return _safe_tool_result(lambda: service.resolve_owner(query))
|
||||
|
||||
@server.tool()
|
||||
def list_teams(
|
||||
query: Annotated[str | None, Field(description="Optional team name search.")] = None,
|
||||
include_members: Annotated[bool, Field(description="Include resolved team members.")] = False,
|
||||
formal_members_only: Annotated[
|
||||
bool,
|
||||
Field(description="When including members, keep only formal employees."),
|
||||
] = True,
|
||||
limit: Annotated[int, Field(description="Maximum teams to return.")] = 20,
|
||||
offset: Annotated[int, Field(description="Zero-based result offset.")] = 0,
|
||||
) -> dict[str, Any]:
|
||||
"""List configured CRM teams and optionally their formal members."""
|
||||
return _safe_tool_result(
|
||||
lambda: service.list_teams(
|
||||
query,
|
||||
include_members=include_members,
|
||||
formal_members_only=formal_members_only,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool()
|
||||
def resolve_team(
|
||||
query: Annotated[str, Field(description="Team name to resolve.")],
|
||||
) -> dict[str, Any]:
|
||||
"""Resolve a team name before using it as a CRM filter."""
|
||||
return _safe_tool_result(lambda: service.resolve_team(query))
|
||||
|
||||
@server.tool()
|
||||
def get_team(
|
||||
team_id: Annotated[Any, Field(description="NocoBase team ID.")],
|
||||
include_members: Annotated[bool, Field(description="Include team members.")] = True,
|
||||
formal_members_only: Annotated[
|
||||
bool,
|
||||
Field(description="Return only formal employees when members are included."),
|
||||
] = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Get one CRM team, its managers, and optionally its members."""
|
||||
return _safe_tool_result(
|
||||
lambda: service.get_team(
|
||||
team_id,
|
||||
include_members=include_members,
|
||||
formal_members_only=formal_members_only,
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool()
|
||||
def team_crm_overview(
|
||||
team: Annotated[str, Field(description="Exact or uniquely matching CRM team name.")],
|
||||
date_from: Annotated[str | None, Field(description="Optional lower follow-up date bound.")] = None,
|
||||
date_to: Annotated[str | None, Field(description="Optional upper follow-up date bound.")] = None,
|
||||
include_member_breakdown: Annotated[
|
||||
bool,
|
||||
Field(description="Include per-member customer ownership and follow-up contribution."),
|
||||
] = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Return team totals and member contributions using formal CRM team membership."""
|
||||
return _safe_tool_result(
|
||||
lambda: service.team_crm_overview(
|
||||
team,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
include_member_breakdown=include_member_breakdown,
|
||||
)
|
||||
)
|
||||
|
||||
if os.environ.get(ENABLE_CONFIG_TOOLS_ENV, "").strip() == "1":
|
||||
|
||||
@server.tool()
|
||||
@@ -245,17 +312,24 @@ def create_server(
|
||||
query: Annotated[str | None, Field(description="Optional customer keyword search.")] = None,
|
||||
status: Annotated[str | None, Field(description="Optional exact customer status.")] = None,
|
||||
owner: Annotated[str | None, Field(description="Optional customer owner name, username, email, or English name.")] = None,
|
||||
team: Annotated[str | None, Field(description="Optional CRM team name.")] = None,
|
||||
source: Annotated[str | None, Field(description="Optional exact customer source.")] = None,
|
||||
limit: Annotated[int, Field(description="Maximum rows to return. Defaults to 20.")] = 20,
|
||||
offset: Annotated[int, Field(description="Zero-based result offset. Defaults to 0.")] = 0,
|
||||
include_raw: Annotated[bool, Field(description="When true, include the original NocoBase row under raw. Defaults to false.")] = False,
|
||||
) -> dict[str, Any]:
|
||||
"""List CRM customers with compact rows and pagination metadata."""
|
||||
"""List CRM customers with compact rows and pagination metadata.
|
||||
|
||||
Rows are {id, name, english_name, territorial_name, status, source,
|
||||
owners: [{id, name}], managers, createdAt, updatedAt}. Empty values are
|
||||
omitted. A name shown as "#123" means that customer has no name recorded.
|
||||
"""
|
||||
return _safe_tool_result(
|
||||
lambda: service.list_customers(
|
||||
query=query,
|
||||
status=status,
|
||||
owner=owner,
|
||||
team=team,
|
||||
source=source,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
@@ -269,19 +343,27 @@ def create_server(
|
||||
include_contacts: Annotated[bool, Field(description="Include related contacts.")] = True,
|
||||
include_recent_followups: Annotated[bool, Field(description="Include recent related follow-ups.")] = True,
|
||||
followup_limit: Annotated[int, Field(description="Maximum related follow-ups to return.")] = 10,
|
||||
include_raw_followups: Annotated[
|
||||
include_raw: Annotated[
|
||||
bool,
|
||||
Field(description="When true, include original NocoBase follow-up rows under raw. Defaults to false."),
|
||||
Field(description="When true, include original NocoBase rows under raw. Defaults to false."),
|
||||
] = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Get one customer and optionally include contacts and recent follow-ups."""
|
||||
"""Get one customer with its contacts and recent follow-ups.
|
||||
|
||||
Returns {customer, contacts, recent_followups, timezone}. The customer is
|
||||
{id, name, english_name, territorial_name, status, source, owners, managers,
|
||||
address, intro, tags, ...} using readable keys, never NocoBase field codes.
|
||||
Contacts are {id, name, title} without phone or email -- call list_contacts
|
||||
for those. Follow-ups use the same row shape as list_followups. Empty values
|
||||
are omitted, so a missing key means empty rather than unknown.
|
||||
"""
|
||||
return _safe_tool_result(
|
||||
lambda: service.get_customer(
|
||||
customer_id,
|
||||
include_contacts=include_contacts,
|
||||
include_recent_followups=include_recent_followups,
|
||||
followup_limit=followup_limit,
|
||||
include_raw_followups=include_raw_followups,
|
||||
include_raw=include_raw,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -291,15 +373,25 @@ def create_server(
|
||||
query: Annotated[str | None, Field(description="Optional contact keyword search.")] = None,
|
||||
limit: Annotated[int, Field(description="Maximum rows to return. Defaults to 20.")] = 20,
|
||||
offset: Annotated[int, Field(description="Zero-based result offset. Defaults to 0.")] = 0,
|
||||
include_details: Annotated[
|
||||
bool,
|
||||
Field(description="Include contact phone and email. Defaults to true."),
|
||||
] = True,
|
||||
include_raw: Annotated[bool, Field(description="When true, include the original NocoBase row under raw. Defaults to false.")] = False,
|
||||
) -> dict[str, Any]:
|
||||
"""List CRM contacts as compact rows, optionally filtered by customer."""
|
||||
"""List CRM contacts as compact rows, optionally filtered by customer.
|
||||
|
||||
Rows are {id, name, title, phone, email, customers: [{id, name}],
|
||||
createdAt, updatedAt}. Set include_details=false to omit phone and email.
|
||||
Empty values are omitted.
|
||||
"""
|
||||
return _safe_tool_result(
|
||||
lambda: service.list_contacts(
|
||||
customer_id=customer_id,
|
||||
query=query,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
include_details=include_details,
|
||||
include_raw=include_raw,
|
||||
)
|
||||
)
|
||||
@@ -308,19 +400,35 @@ def create_server(
|
||||
def list_followups(
|
||||
customer_id: Annotated[Any | None, Field(description="Optional customer primary key for relation filtering.")] = None,
|
||||
contact_id: Annotated[Any | None, Field(description="Optional contact primary key for relation filtering.")] = None,
|
||||
owner: Annotated[str | None, Field(description="Optional follow-up owner name, username, email, or English name.")] = None,
|
||||
owner: Annotated[str | None, Field(description="Optional follow-up participant name, username, email, or English name.")] = None,
|
||||
team: Annotated[str | None, Field(description="Optional CRM team name.")] = None,
|
||||
project_id: Annotated[Any | None, Field(description="Optional CRM project ID.")] = None,
|
||||
followup_type: Annotated[str | None, Field(description="Optional exact follow-up type value.")] = None,
|
||||
record_status: Annotated[str | None, Field(description="Optional exact follow-up record status value.")] = None,
|
||||
date_from: Annotated[str | None, Field(description="Optional lower follow-up date bound.")] = None,
|
||||
date_to: Annotated[str | None, Field(description="Optional upper follow-up date bound.")] = None,
|
||||
limit: Annotated[int, Field(description="Maximum rows to return. Defaults to 20.")] = 20,
|
||||
offset: Annotated[int, Field(description="Zero-based result offset. Defaults to 0.")] = 0,
|
||||
include_raw: Annotated[bool, Field(description="When true, include the original NocoBase row under raw. Defaults to false.")] = False,
|
||||
) -> dict[str, Any]:
|
||||
"""List customer follow-up records as compact rows with pagination metadata."""
|
||||
"""List customer follow-up records as compact rows with pagination metadata.
|
||||
|
||||
Rows are {id, followedAt, customers: [{id, name}], contacts, participants,
|
||||
projects, type, status, location, content}. Content is the full follow-up
|
||||
text, never truncated. type and status are the display labels; use
|
||||
crm_filter_options for the codes the followup_type and record_status
|
||||
filters expect. creator appears only when the submitter is not also a
|
||||
participant. Empty values are omitted.
|
||||
"""
|
||||
return _safe_tool_result(
|
||||
lambda: service.list_followups(
|
||||
customer_id=customer_id,
|
||||
contact_id=contact_id,
|
||||
owner=owner,
|
||||
team=team,
|
||||
project_id=project_id,
|
||||
followup_type=followup_type,
|
||||
record_status=record_status,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
limit=limit,
|
||||
@@ -329,6 +437,76 @@ def create_server(
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool()
|
||||
def list_projects(
|
||||
customer_id: Annotated[Any | None, Field(description="Optional customer ID.")] = None,
|
||||
team: Annotated[str | None, Field(description="Optional CRM team name.")] = None,
|
||||
stage: Annotated[str | None, Field(description="Optional project stage label or configured value.")] = None,
|
||||
limit: Annotated[int, Field(description="Maximum projects to return.")] = 20,
|
||||
offset: Annotated[int, Field(description="Zero-based result offset.")] = 0,
|
||||
include_raw: Annotated[bool, Field(description="Include original NocoBase project rows.")] = False,
|
||||
) -> dict[str, Any]:
|
||||
"""List CRM projects linked to customers, teams, and project stages."""
|
||||
return _safe_tool_result(
|
||||
lambda: service.list_projects(
|
||||
customer_id=customer_id,
|
||||
team=team,
|
||||
stage=stage,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
include_raw=include_raw,
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool()
|
||||
def get_project(
|
||||
project_id: Annotated[Any, Field(description="NocoBase CRM project ID.")],
|
||||
include_contacts: Annotated[bool, Field(description="Include related contacts.")] = True,
|
||||
include_followups: Annotated[bool, Field(description="Include related follow-ups.")] = True,
|
||||
include_updates: Annotated[bool, Field(description="Include project update records.")] = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Get a CRM project with its existing NocoBase relations."""
|
||||
return _safe_tool_result(
|
||||
lambda: service.get_project(
|
||||
project_id,
|
||||
include_contacts=include_contacts,
|
||||
include_followups=include_followups,
|
||||
include_updates=include_updates,
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool()
|
||||
def get_project_timeline(
|
||||
project_id: Annotated[Any, Field(description="NocoBase CRM project ID.")],
|
||||
date_from: Annotated[str | None, Field(description="Optional lower event date bound.")] = None,
|
||||
date_to: Annotated[str | None, Field(description="Optional upper event date bound.")] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return a chronological CRM project timeline from follow-ups and project updates."""
|
||||
return _safe_tool_result(
|
||||
lambda: service.get_project_timeline(project_id, date_from=date_from, date_to=date_to)
|
||||
)
|
||||
|
||||
@server.tool()
|
||||
def list_stale_customers(
|
||||
team: Annotated[str | None, Field(description="Optional CRM team name.")] = None,
|
||||
owner: Annotated[str | None, Field(description="Optional customer owner.")] = None,
|
||||
inactive_days: Annotated[int, Field(description="Days without a valid follow-up.")] = 30,
|
||||
as_of: Annotated[str | None, Field(description="Reference date in YYYY-MM-DD format.")] = None,
|
||||
limit: Annotated[int, Field(description="Maximum customers to return.")] = 20,
|
||||
offset: Annotated[int, Field(description="Zero-based result offset.")] = 0,
|
||||
) -> dict[str, Any]:
|
||||
"""List customers whose last valid follow-up is older than the configured threshold."""
|
||||
return _safe_tool_result(
|
||||
lambda: service.list_stale_customers(
|
||||
team=team,
|
||||
owner=owner,
|
||||
inactive_days=inactive_days,
|
||||
as_of=as_of,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
)
|
||||
|
||||
if not readonly:
|
||||
|
||||
@server.tool()
|
||||
|
||||
Reference in New Issue
Block a user