feat(F): F-WORK agent_workstream + F-EMAIL/CONTACT/FOLLOW/REPORT prebuilt agents
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:
Agent Zero
2026-08-17 18:33:45 +02:00
parent 7ed79d3c1f
commit 8ed6d27885
6 changed files with 407 additions and 0 deletions
+201
View File
@@ -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,
)