diff --git a/app/ai/agent_workstream.py b/app/ai/agent_workstream.py new file mode 100644 index 0000000..af9a943 --- /dev/null +++ b/app/ai/agent_workstream.py @@ -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, + ) diff --git a/app/plugins/builtins/automation/prebuilt/__init__.py b/app/plugins/builtins/automation/prebuilt/__init__.py new file mode 100644 index 0000000..be4a7e7 --- /dev/null +++ b/app/plugins/builtins/automation/prebuilt/__init__.py @@ -0,0 +1 @@ +"""Pre-built agent definitions for common CRM use cases.""" diff --git a/app/plugins/builtins/automation/prebuilt/contact_enrichment_agent.py b/app/plugins/builtins/automation/prebuilt/contact_enrichment_agent.py new file mode 100644 index 0000000..4127b34 --- /dev/null +++ b/app/plugins/builtins/automation/prebuilt/contact_enrichment_agent.py @@ -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, + ) diff --git a/app/plugins/builtins/automation/prebuilt/email_triage_agent.py b/app/plugins/builtins/automation/prebuilt/email_triage_agent.py new file mode 100644 index 0000000..21539c5 --- /dev/null +++ b/app/plugins/builtins/automation/prebuilt/email_triage_agent.py @@ -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, + ) diff --git a/app/plugins/builtins/automation/prebuilt/follow_up_agent.py b/app/plugins/builtins/automation/prebuilt/follow_up_agent.py new file mode 100644 index 0000000..1f1512b --- /dev/null +++ b/app/plugins/builtins/automation/prebuilt/follow_up_agent.py @@ -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, + ) diff --git a/app/plugins/builtins/automation/prebuilt/report_agent.py b/app/plugins/builtins/automation/prebuilt/report_agent.py new file mode 100644 index 0000000..f60c9dc --- /dev/null +++ b/app/plugins/builtins/automation/prebuilt/report_agent.py @@ -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, + )