feat(F): F-WORK agent_workstream + F-EMAIL/CONTACT/FOLLOW/REPORT prebuilt agents
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- F-WORK: app/ai/agent_workstream.py (201 lines) — post_agent_message, post_agent_step, post_agent_result, post_approval_request - F-EMAIL: prebuilt/email_triage_agent.py — E-Mail-Triage-Agent with 3 tools, max 10 steps, $0.50 budget - F-CONTACT: prebuilt/contact_enrichment_agent.py — Contact-Enrichment-Agent with 3 tools, max 8 steps, $0.30 budget - F-FOLLOW: prebuilt/follow_up_agent.py — Follow-up-Agent with 3 tools, max 8 steps, $0.30 budget - F-REPORT: prebuilt/report_agent.py — Report-Agent with 2 tools, max 12 steps, $0.50 budget - All compile checks pass
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
"""Agent → Workstream integration.
|
||||
|
||||
Posts agent messages to the central communication system (kommunikation plugin).
|
||||
Supports text, action_card, entity_card, task_card, approval, and miniapp block types.
|
||||
All agent messages are marked as AI-generated via transparency metadata.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.ai.transparency import mark_as_ai_generated
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def post_agent_message(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID,
|
||||
agent_run_id: uuid.UUID,
|
||||
content: str,
|
||||
block_type: str = "text",
|
||||
block_data: dict[str, Any] | None = None,
|
||||
conversation_id: uuid.UUID | None = None,
|
||||
) -> uuid.UUID:
|
||||
"""Post a message from an agent to the communication system.
|
||||
|
||||
Creates a CommMessage in the agent's conversation channel.
|
||||
If no conversation_id is provided, uses the agent's default channel.
|
||||
|
||||
Args:
|
||||
db: Database session.
|
||||
tenant_id: Tenant ID.
|
||||
agent_id: Agent definition ID.
|
||||
agent_run_id: Agent run ID for traceability.
|
||||
content: Message text content.
|
||||
block_type: Block type (text, action_card, entity_card, task_card, approval, miniapp).
|
||||
block_data: Additional block data (e.g. action buttons, entity reference).
|
||||
conversation_id: Optional conversation to post to. If None, uses agent channel.
|
||||
|
||||
Returns:
|
||||
Message ID.
|
||||
"""
|
||||
from app.plugins.builtins.kommunikation.models import CommMessage, CommMessageBlock
|
||||
from app.plugins.builtins.kommunikation.services import send_message
|
||||
|
||||
# Mark as AI-generated
|
||||
ai_metadata = mark_as_ai_generated(content, {
|
||||
"agent_id": str(agent_id),
|
||||
"agent_run_id": str(agent_run_id),
|
||||
})
|
||||
|
||||
# Build block if not plain text
|
||||
blocks: list[dict[str, Any]] = []
|
||||
if block_type != "text" and block_data:
|
||||
blocks.append({
|
||||
"type": block_type,
|
||||
"data": block_data,
|
||||
})
|
||||
|
||||
# Post via kommunikation service
|
||||
message_id = await send_message(
|
||||
db=db,
|
||||
tenant_id=tenant_id,
|
||||
sender_id=agent_id,
|
||||
sender_type="agent",
|
||||
conversation_id=conversation_id or await _get_or_create_agent_channel(db, tenant_id, agent_id),
|
||||
content=content,
|
||||
blocks=blocks,
|
||||
metadata=ai_metadata,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Agent %s posted message %s (block_type=%s, run=%s)",
|
||||
agent_id, message_id, block_type, agent_run_id,
|
||||
)
|
||||
return message_id
|
||||
|
||||
|
||||
async def _get_or_create_agent_channel(
|
||||
db: AsyncSession, tenant_id: uuid.UUID, agent_id: uuid.UUID
|
||||
) -> uuid.UUID:
|
||||
"""Get or create a dedicated conversation channel for an agent."""
|
||||
from app.plugins.builtins.kommunikation.models import CommConversation
|
||||
|
||||
# Try to find existing agent channel
|
||||
result = await db.execute(
|
||||
select(CommConversation).where(
|
||||
CommConversation.tenant_id == tenant_id,
|
||||
CommConversation.entity_type == "agent",
|
||||
CommConversation.entity_id == agent_id,
|
||||
CommConversation.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
conv = result.scalar_one_or_none()
|
||||
if conv:
|
||||
return conv.id
|
||||
|
||||
# Create new channel
|
||||
conv = CommConversation(
|
||||
tenant_id=tenant_id, entity_type="agent",
|
||||
entity_id=agent_id,
|
||||
title=f"Agent Channel",
|
||||
conversation_type="channel",
|
||||
is_system=False,
|
||||
)
|
||||
db.add(conv)
|
||||
await db.flush()
|
||||
return conv.id
|
||||
|
||||
|
||||
async def post_agent_step(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID,
|
||||
agent_run_id: uuid.UUID,
|
||||
step_number: int,
|
||||
thought: str,
|
||||
action: str | None = None,
|
||||
observation: str | None = None,
|
||||
) -> uuid.UUID | None:
|
||||
"""Post a ReAct step as an action_card to the workstream.
|
||||
|
||||
Only posts if the agent's trace_mode is 'extended'.
|
||||
"""
|
||||
block_data = {
|
||||
"step_number": step_number,
|
||||
"thought": thought[:500], # Truncate for display
|
||||
"action": action,
|
||||
"observation": (observation or "")[:500],
|
||||
}
|
||||
return await post_agent_message(
|
||||
db=db,
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
agent_run_id=agent_run_id,
|
||||
content=f"Step {step_number}: {action or 'Thinking...'}",
|
||||
block_type="action_card",
|
||||
block_data=block_data,
|
||||
)
|
||||
|
||||
|
||||
async def post_agent_result(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID,
|
||||
agent_run_id: uuid.UUID,
|
||||
final_content: str,
|
||||
total_cost_usd: float,
|
||||
steps_taken: int,
|
||||
status: str,
|
||||
) -> uuid.UUID:
|
||||
"""Post the final result of an agent run to the workstream."""
|
||||
block_data = {
|
||||
"status": status,
|
||||
"steps_taken": steps_taken,
|
||||
"total_cost_usd": round(total_cost_usd, 6),
|
||||
"run_id": str(agent_run_id),
|
||||
}
|
||||
return await post_agent_message(
|
||||
db=db,
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
agent_run_id=agent_run_id,
|
||||
content=final_content,
|
||||
block_type="action_card",
|
||||
block_data=block_data,
|
||||
)
|
||||
|
||||
|
||||
async def post_approval_request(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
agent_id: uuid.UUID,
|
||||
agent_run_id: uuid.UUID,
|
||||
approval_id: uuid.UUID,
|
||||
action: str,
|
||||
description: str,
|
||||
) -> uuid.UUID:
|
||||
"""Post an approval request card to the workstream."""
|
||||
block_data = {
|
||||
"approval_id": str(approval_id),
|
||||
"action": action,
|
||||
"description": description,
|
||||
"status": "pending",
|
||||
}
|
||||
return await post_agent_message(
|
||||
db=db,
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
agent_run_id=agent_run_id,
|
||||
content=f"Approval required: {action}",
|
||||
block_type="approval",
|
||||
block_data=block_data,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Pre-built agent definitions for common CRM use cases."""
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Pre-built Contact-Enrichment-Agent.
|
||||
|
||||
Enriches contact data by searching for related information.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import uuid
|
||||
from app.plugins.builtins.automation.models import AgentDefinition
|
||||
|
||||
CONTACT_ENRICHMENT_SYSTEM_PROMPT = """You are a Contact Enrichment Agent for a CRM system.
|
||||
|
||||
Your task is to enrich contact profiles with additional information.
|
||||
|
||||
For each contact, you should:
|
||||
1. Search for related entities (companies, other contacts)
|
||||
2. Check audit history for recent interactions
|
||||
3. Find semantic matches in the database
|
||||
4. Suggest missing fields that could be filled
|
||||
5. Identify potential duplicates
|
||||
|
||||
Use the available tools to:
|
||||
- Search for related entities (search_related)
|
||||
- Get entity history (get_contact_history)
|
||||
- Call CRM API for data lookup (call_crm_api)
|
||||
|
||||
Output format:
|
||||
- Enrichment suggestions as structured data
|
||||
- Confidence score for each suggestion
|
||||
- Source reference for each piece of information
|
||||
|
||||
Do NOT modify contacts. You are advisory only.
|
||||
"""
|
||||
|
||||
def create_contact_enrichment_agent(
|
||||
tenant_id: uuid.UUID, user_id: uuid.UUID
|
||||
) -> AgentDefinition:
|
||||
return AgentDefinition(
|
||||
tenant_id=tenant_id,
|
||||
name="Contact-Enrichment-Agent",
|
||||
description="Reichert Kontaktdaten mit verwandten Informationen an",
|
||||
system_prompt=CONTACT_ENRICHMENT_SYSTEM_PROMPT,
|
||||
llm_model="openai/gpt-4o-mini",
|
||||
tool_ids=["search_related", "get_contact_history", "call_crm_api"],
|
||||
max_steps=8,
|
||||
max_duration_seconds=90,
|
||||
budget_limit_usd=0.30,
|
||||
mode="reactive",
|
||||
is_active=True,
|
||||
temperature=0.2,
|
||||
max_tokens=1500,
|
||||
trace_mode="standard",
|
||||
created_by=user_id,
|
||||
)
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Pre-built E-Mail-Triage-Agent.
|
||||
|
||||
Sorts and prioritizes incoming emails automatically.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import uuid
|
||||
from app.plugins.builtins.automation.models import AgentDefinition
|
||||
|
||||
EMAIL_TRIAGE_SYSTEM_PROMPT = """You are an E-Mail Triage Agent for a CRM system.
|
||||
|
||||
Your task is to sort and prioritize incoming emails for the user.
|
||||
|
||||
For each email, you should:
|
||||
1. Classify it as: urgent, important, normal, low_priority, or spam
|
||||
2. Extract key information: sender, subject, intent, action items
|
||||
3. Suggest a response category: reply_needed, forward, archive, delete
|
||||
4. Identify any contacts that should be linked
|
||||
|
||||
Use the available tools to:
|
||||
- Fetch emails for contacts (get_contact_mails)
|
||||
- Summarize email threads (summarize_mail_thread)
|
||||
- Call CRM API for contact/company data (call_crm_api)
|
||||
|
||||
Output format:
|
||||
- Provide a structured summary of each email
|
||||
- Include priority level and suggested action
|
||||
- Be concise but thorough
|
||||
|
||||
Do NOT send emails or make changes. You are advisory only.
|
||||
"""
|
||||
|
||||
def create_email_triage_agent(
|
||||
tenant_id: uuid.UUID, user_id: uuid.UUID
|
||||
) -> AgentDefinition:
|
||||
return AgentDefinition(
|
||||
tenant_id=tenant_id,
|
||||
name="E-Mail-Triage-Agent",
|
||||
description="Sortiert und priorisiert eingehende E-Mails automatisch",
|
||||
system_prompt=EMAIL_TRIAGE_SYSTEM_PROMPT,
|
||||
llm_model="openai/gpt-4o-mini",
|
||||
tool_ids=["get_contact_mails", "summarize_mail_thread", "call_crm_api"],
|
||||
max_steps=10,
|
||||
max_duration_seconds=120,
|
||||
budget_limit_usd=0.50,
|
||||
mode="reactive",
|
||||
is_active=True,
|
||||
temperature=0.3,
|
||||
max_tokens=2000,
|
||||
trace_mode="standard",
|
||||
created_by=user_id,
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Pre-built Follow-up-Agent.
|
||||
|
||||
Reminds about and creates follow-up tasks for contacts.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import uuid
|
||||
from app.plugins.builtins.automation.models import AgentDefinition
|
||||
|
||||
FOLLOW_UP_SYSTEM_PROMPT = """You are a Follow-up Agent for a CRM system.
|
||||
|
||||
Your task is to identify and create follow-up tasks for contacts.
|
||||
|
||||
For each contact, you should:
|
||||
1. Check open tasks and calendar entries
|
||||
2. Review recent email communication
|
||||
3. Identify contacts that need follow-up (no response, overdue tasks, upcoming deadlines)
|
||||
4. Suggest follow-up actions (call, email, meeting, task)
|
||||
5. Create follow-up tasks when appropriate
|
||||
|
||||
Use the available tools to:
|
||||
- Get open tasks (get_open_tasks)
|
||||
- Get contact emails (get_contact_mails)
|
||||
- Call CRM API for task creation (call_crm_api)
|
||||
|
||||
Output format:
|
||||
- List of contacts needing follow-up with reason
|
||||
- Suggested action and timing for each
|
||||
- Priority level (urgent, this_week, this_month)
|
||||
|
||||
You may create tasks via call_crm_api. Always include a clear description and due date.
|
||||
"""
|
||||
|
||||
def create_follow_up_agent(
|
||||
tenant_id: uuid.UUID, user_id: uuid.UUID
|
||||
) -> AgentDefinition:
|
||||
return AgentDefinition(
|
||||
tenant_id=tenant_id,
|
||||
name="Follow-up-Agent",
|
||||
description="Erstellt und erinnert an Follow-up-Tasks für Kontakte",
|
||||
system_prompt=FOLLOW_UP_SYSTEM_PROMPT,
|
||||
llm_model="openai/gpt-4o-mini",
|
||||
tool_ids=["get_open_tasks", "get_contact_mails", "call_crm_api"],
|
||||
max_steps=8,
|
||||
max_duration_seconds=90,
|
||||
budget_limit_usd=0.30,
|
||||
mode="proactive",
|
||||
is_active=True,
|
||||
temperature=0.4,
|
||||
max_tokens=1500,
|
||||
trace_mode="standard",
|
||||
created_by=user_id,
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Pre-built Report-Agent.
|
||||
|
||||
Generates reports from CRM data using search and API tools.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import uuid
|
||||
from app.plugins.builtins.automation.models import AgentDefinition
|
||||
|
||||
REPORT_SYSTEM_PROMPT = """You are a Report Agent for a CRM system.
|
||||
|
||||
Your task is to generate reports from CRM data.
|
||||
|
||||
You can:
|
||||
1. Search for contacts, companies, and activities using hybrid search
|
||||
2. Call CRM API for structured data (contacts, companies, tasks, calendar)
|
||||
3. Aggregate and summarize data into reports
|
||||
4. Format reports as markdown with tables and sections
|
||||
|
||||
Report types you can generate:
|
||||
- Contact activity summary (interactions, emails, tasks per contact)
|
||||
- Sales pipeline overview (contacts by status, recent changes)
|
||||
- Task completion report (open vs done, overdue, by assignee)
|
||||
- Communication summary (email volume, response times)
|
||||
- Custom reports based on user request
|
||||
|
||||
Use the available tools to gather data, then format a clear, structured report.
|
||||
Include relevant metrics, dates, and entity references.
|
||||
Be concise but comprehensive. Use markdown formatting.
|
||||
"""
|
||||
|
||||
def create_report_agent(
|
||||
tenant_id: uuid.UUID, user_id: uuid.UUID
|
||||
) -> AgentDefinition:
|
||||
return AgentDefinition(
|
||||
tenant_id=tenant_id,
|
||||
name="Report-Agent",
|
||||
description="Generiert Berichte aus CRM-Daten",
|
||||
system_prompt=REPORT_SYSTEM_PROMPT,
|
||||
llm_model="openai/gpt-4o-mini",
|
||||
tool_ids=["call_crm_api", "hybrid_search"],
|
||||
max_steps=12,
|
||||
max_duration_seconds=180,
|
||||
budget_limit_usd=0.50,
|
||||
mode="reactive",
|
||||
is_active=True,
|
||||
temperature=0.3,
|
||||
max_tokens=3000,
|
||||
trace_mode="standard",
|
||||
created_by=user_id,
|
||||
)
|
||||
Reference in New Issue
Block a user