mail-swarms 1.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. mail/__init__.py +35 -0
  2. mail/api.py +1964 -0
  3. mail/cli.py +432 -0
  4. mail/client.py +1657 -0
  5. mail/config/__init__.py +8 -0
  6. mail/config/client.py +87 -0
  7. mail/config/server.py +165 -0
  8. mail/core/__init__.py +72 -0
  9. mail/core/actions.py +69 -0
  10. mail/core/agents.py +73 -0
  11. mail/core/message.py +366 -0
  12. mail/core/runtime.py +3537 -0
  13. mail/core/tasks.py +311 -0
  14. mail/core/tools.py +1206 -0
  15. mail/db/__init__.py +0 -0
  16. mail/db/init.py +182 -0
  17. mail/db/types.py +65 -0
  18. mail/db/utils.py +523 -0
  19. mail/examples/__init__.py +27 -0
  20. mail/examples/analyst_dummy/__init__.py +15 -0
  21. mail/examples/analyst_dummy/agent.py +136 -0
  22. mail/examples/analyst_dummy/prompts.py +44 -0
  23. mail/examples/consultant_dummy/__init__.py +15 -0
  24. mail/examples/consultant_dummy/agent.py +136 -0
  25. mail/examples/consultant_dummy/prompts.py +42 -0
  26. mail/examples/data_analysis/__init__.py +40 -0
  27. mail/examples/data_analysis/analyst/__init__.py +9 -0
  28. mail/examples/data_analysis/analyst/agent.py +67 -0
  29. mail/examples/data_analysis/analyst/prompts.py +53 -0
  30. mail/examples/data_analysis/processor/__init__.py +13 -0
  31. mail/examples/data_analysis/processor/actions.py +293 -0
  32. mail/examples/data_analysis/processor/agent.py +67 -0
  33. mail/examples/data_analysis/processor/prompts.py +48 -0
  34. mail/examples/data_analysis/reporter/__init__.py +10 -0
  35. mail/examples/data_analysis/reporter/actions.py +187 -0
  36. mail/examples/data_analysis/reporter/agent.py +67 -0
  37. mail/examples/data_analysis/reporter/prompts.py +49 -0
  38. mail/examples/data_analysis/statistics/__init__.py +18 -0
  39. mail/examples/data_analysis/statistics/actions.py +343 -0
  40. mail/examples/data_analysis/statistics/agent.py +67 -0
  41. mail/examples/data_analysis/statistics/prompts.py +60 -0
  42. mail/examples/mafia/__init__.py +0 -0
  43. mail/examples/mafia/game.py +1537 -0
  44. mail/examples/mafia/narrator_tools.py +396 -0
  45. mail/examples/mafia/personas.py +240 -0
  46. mail/examples/mafia/prompts.py +489 -0
  47. mail/examples/mafia/roles.py +147 -0
  48. mail/examples/mafia/spec.md +350 -0
  49. mail/examples/math_dummy/__init__.py +23 -0
  50. mail/examples/math_dummy/actions.py +252 -0
  51. mail/examples/math_dummy/agent.py +136 -0
  52. mail/examples/math_dummy/prompts.py +46 -0
  53. mail/examples/math_dummy/types.py +5 -0
  54. mail/examples/research/__init__.py +39 -0
  55. mail/examples/research/researcher/__init__.py +9 -0
  56. mail/examples/research/researcher/agent.py +67 -0
  57. mail/examples/research/researcher/prompts.py +54 -0
  58. mail/examples/research/searcher/__init__.py +10 -0
  59. mail/examples/research/searcher/actions.py +324 -0
  60. mail/examples/research/searcher/agent.py +67 -0
  61. mail/examples/research/searcher/prompts.py +53 -0
  62. mail/examples/research/summarizer/__init__.py +18 -0
  63. mail/examples/research/summarizer/actions.py +255 -0
  64. mail/examples/research/summarizer/agent.py +67 -0
  65. mail/examples/research/summarizer/prompts.py +55 -0
  66. mail/examples/research/verifier/__init__.py +10 -0
  67. mail/examples/research/verifier/actions.py +337 -0
  68. mail/examples/research/verifier/agent.py +67 -0
  69. mail/examples/research/verifier/prompts.py +52 -0
  70. mail/examples/supervisor/__init__.py +11 -0
  71. mail/examples/supervisor/agent.py +4 -0
  72. mail/examples/supervisor/prompts.py +93 -0
  73. mail/examples/support/__init__.py +33 -0
  74. mail/examples/support/classifier/__init__.py +10 -0
  75. mail/examples/support/classifier/actions.py +307 -0
  76. mail/examples/support/classifier/agent.py +68 -0
  77. mail/examples/support/classifier/prompts.py +56 -0
  78. mail/examples/support/coordinator/__init__.py +9 -0
  79. mail/examples/support/coordinator/agent.py +67 -0
  80. mail/examples/support/coordinator/prompts.py +48 -0
  81. mail/examples/support/faq/__init__.py +10 -0
  82. mail/examples/support/faq/actions.py +182 -0
  83. mail/examples/support/faq/agent.py +67 -0
  84. mail/examples/support/faq/prompts.py +42 -0
  85. mail/examples/support/sentiment/__init__.py +15 -0
  86. mail/examples/support/sentiment/actions.py +341 -0
  87. mail/examples/support/sentiment/agent.py +67 -0
  88. mail/examples/support/sentiment/prompts.py +54 -0
  89. mail/examples/weather_dummy/__init__.py +23 -0
  90. mail/examples/weather_dummy/actions.py +75 -0
  91. mail/examples/weather_dummy/agent.py +136 -0
  92. mail/examples/weather_dummy/prompts.py +35 -0
  93. mail/examples/weather_dummy/types.py +5 -0
  94. mail/factories/__init__.py +27 -0
  95. mail/factories/action.py +223 -0
  96. mail/factories/base.py +1531 -0
  97. mail/factories/supervisor.py +241 -0
  98. mail/net/__init__.py +7 -0
  99. mail/net/registry.py +712 -0
  100. mail/net/router.py +728 -0
  101. mail/net/server_utils.py +114 -0
  102. mail/net/types.py +247 -0
  103. mail/server.py +1605 -0
  104. mail/stdlib/__init__.py +0 -0
  105. mail/stdlib/anthropic/__init__.py +0 -0
  106. mail/stdlib/fs/__init__.py +15 -0
  107. mail/stdlib/fs/actions.py +209 -0
  108. mail/stdlib/http/__init__.py +19 -0
  109. mail/stdlib/http/actions.py +333 -0
  110. mail/stdlib/interswarm/__init__.py +11 -0
  111. mail/stdlib/interswarm/actions.py +208 -0
  112. mail/stdlib/mcp/__init__.py +19 -0
  113. mail/stdlib/mcp/actions.py +294 -0
  114. mail/stdlib/openai/__init__.py +13 -0
  115. mail/stdlib/openai/agents.py +451 -0
  116. mail/summarizer.py +234 -0
  117. mail/swarms_json/__init__.py +27 -0
  118. mail/swarms_json/types.py +87 -0
  119. mail/swarms_json/utils.py +255 -0
  120. mail/url_scheme.py +51 -0
  121. mail/utils/__init__.py +53 -0
  122. mail/utils/auth.py +194 -0
  123. mail/utils/context.py +17 -0
  124. mail/utils/logger.py +73 -0
  125. mail/utils/openai.py +212 -0
  126. mail/utils/parsing.py +89 -0
  127. mail/utils/serialize.py +292 -0
  128. mail/utils/store.py +49 -0
  129. mail/utils/string_builder.py +119 -0
  130. mail/utils/version.py +20 -0
  131. mail_swarms-1.3.2.dist-info/METADATA +237 -0
  132. mail_swarms-1.3.2.dist-info/RECORD +137 -0
  133. mail_swarms-1.3.2.dist-info/WHEEL +4 -0
  134. mail_swarms-1.3.2.dist-info/entry_points.txt +2 -0
  135. mail_swarms-1.3.2.dist-info/licenses/LICENSE +202 -0
  136. mail_swarms-1.3.2.dist-info/licenses/NOTICE +10 -0
  137. mail_swarms-1.3.2.dist-info/licenses/THIRD_PARTY_NOTICES.md +12334 -0
@@ -0,0 +1,182 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Charon Labs
3
+
4
+ """FAQ search action for the Customer Support swarm."""
5
+
6
+ import json
7
+ from typing import Any
8
+
9
+ from mail import action
10
+
11
+ # Dummy FAQ database for demonstration purposes
12
+ FAQ_DATABASE = [
13
+ {
14
+ "id": "faq-001",
15
+ "question": "How do I reset my password?",
16
+ "answer": "To reset your password: 1) Go to the login page, 2) Click 'Forgot Password', 3) Enter your email address, 4) Check your email for a reset link, 5) Click the link and create a new password. The reset link expires after 24 hours.",
17
+ "category": "account",
18
+ "keywords": ["password", "reset", "forgot", "login", "account", "access"],
19
+ },
20
+ {
21
+ "id": "faq-002",
22
+ "question": "What is your refund policy?",
23
+ "answer": "We offer a 30-day money-back guarantee on all purchases. To request a refund: 1) Contact our support team within 30 days of purchase, 2) Provide your order number, 3) Explain the reason for the refund. Refunds are processed within 5-7 business days to your original payment method.",
24
+ "category": "billing",
25
+ "keywords": ["refund", "money", "return", "policy", "guarantee", "purchase"],
26
+ },
27
+ {
28
+ "id": "faq-003",
29
+ "question": "How do I cancel my subscription?",
30
+ "answer": "To cancel your subscription: 1) Log into your account, 2) Go to Settings > Subscription, 3) Click 'Cancel Subscription', 4) Confirm your cancellation. Your access continues until the end of the current billing period. You can resubscribe at any time.",
31
+ "category": "billing",
32
+ "keywords": ["cancel", "subscription", "unsubscribe", "stop", "billing"],
33
+ },
34
+ {
35
+ "id": "faq-004",
36
+ "question": "How do I contact customer support?",
37
+ "answer": "You can reach our support team through: 1) Email: support@example.com (response within 24 hours), 2) Live chat on our website (available 9am-6pm EST), 3) Phone: 1-800-EXAMPLE (Monday-Friday 9am-5pm EST). For urgent issues, please use phone support.",
38
+ "category": "general",
39
+ "keywords": ["contact", "support", "help", "phone", "email", "chat"],
40
+ },
41
+ {
42
+ "id": "faq-005",
43
+ "question": "Why is my account locked?",
44
+ "answer": "Accounts may be locked for security reasons including: 1) Multiple failed login attempts, 2) Suspicious activity detected, 3) Password expired. To unlock your account, use the 'Forgot Password' feature or contact support. If you believe this is an error, our security team can review your case.",
45
+ "category": "account",
46
+ "keywords": ["locked", "account", "security", "blocked", "access", "suspended"],
47
+ },
48
+ {
49
+ "id": "faq-006",
50
+ "question": "How do I update my billing information?",
51
+ "answer": "To update billing information: 1) Log into your account, 2) Go to Settings > Payment Methods, 3) Click 'Edit' next to your current payment method or 'Add New', 4) Enter your new card details, 5) Click 'Save'. Your next charge will use the updated payment method.",
52
+ "category": "billing",
53
+ "keywords": ["billing", "payment", "credit card", "update", "change", "card"],
54
+ },
55
+ {
56
+ "id": "faq-007",
57
+ "question": "What features are included in my plan?",
58
+ "answer": "Plan features vary by tier: Basic includes core features and email support. Pro adds advanced analytics, priority support, and API access. Enterprise includes all Pro features plus dedicated account manager, custom integrations, and 24/7 phone support. Visit our pricing page for detailed comparisons.",
59
+ "category": "general",
60
+ "keywords": [
61
+ "features",
62
+ "plan",
63
+ "tier",
64
+ "pricing",
65
+ "include",
66
+ "basic",
67
+ "pro",
68
+ "enterprise",
69
+ ],
70
+ },
71
+ {
72
+ "id": "faq-008",
73
+ "question": "How do I export my data?",
74
+ "answer": "To export your data: 1) Go to Settings > Data & Privacy, 2) Click 'Export Data', 3) Select the data types to include, 4) Choose your format (CSV or JSON), 5) Click 'Generate Export'. You'll receive an email with a download link within 24 hours. Exports are available for 7 days.",
75
+ "category": "technical",
76
+ "keywords": ["export", "data", "download", "backup", "csv", "json"],
77
+ },
78
+ {
79
+ "id": "faq-009",
80
+ "question": "Is my data secure?",
81
+ "answer": "Yes, we take security seriously. We use: 1) AES-256 encryption for data at rest, 2) TLS 1.3 for data in transit, 3) Regular security audits by third parties, 4) SOC 2 Type II certification, 5) GDPR and CCPA compliance. We never sell your data to third parties.",
82
+ "category": "technical",
83
+ "keywords": [
84
+ "security",
85
+ "data",
86
+ "encryption",
87
+ "privacy",
88
+ "secure",
89
+ "safe",
90
+ "gdpr",
91
+ ],
92
+ },
93
+ {
94
+ "id": "faq-010",
95
+ "question": "How do I enable two-factor authentication?",
96
+ "answer": "To enable 2FA: 1) Go to Settings > Security, 2) Click 'Enable Two-Factor Authentication', 3) Choose your method (authenticator app or SMS), 4) Follow the setup instructions, 5) Save your backup codes. We recommend using an authenticator app for better security.",
97
+ "category": "account",
98
+ "keywords": ["two-factor", "2fa", "authentication", "security", "mfa", "otp"],
99
+ },
100
+ ]
101
+
102
+ SEARCH_FAQ_PARAMETERS = {
103
+ "type": "object",
104
+ "properties": {
105
+ "query": {
106
+ "type": "string",
107
+ "description": "The search query to find relevant FAQ entries",
108
+ },
109
+ "max_results": {
110
+ "type": "integer",
111
+ "description": "Maximum number of FAQ entries to return (default: 3)",
112
+ "minimum": 1,
113
+ "maximum": 10,
114
+ },
115
+ },
116
+ "required": ["query"],
117
+ }
118
+
119
+
120
+ def _calculate_relevance(query: str, faq_entry: dict[str, Any]) -> float:
121
+ """Calculate relevance score between query and FAQ entry."""
122
+ query_terms = set(query.lower().split())
123
+ keywords = set(faq_entry.get("keywords", []))
124
+ question_terms = set(faq_entry.get("question", "").lower().split())
125
+
126
+ # Score based on keyword matches (highest weight)
127
+ keyword_matches = len(query_terms & keywords)
128
+ # Score based on question word matches
129
+ question_matches = len(query_terms & question_terms)
130
+
131
+ # Combined score with weights
132
+ score = (keyword_matches * 2.0) + (question_matches * 1.0)
133
+ return score
134
+
135
+
136
+ @action(
137
+ name="search_faq",
138
+ description="Search the FAQ database for entries relevant to a customer query.",
139
+ parameters=SEARCH_FAQ_PARAMETERS,
140
+ )
141
+ async def search_faq(args: dict[str, Any]) -> str:
142
+ """Search the FAQ database and return relevant entries."""
143
+ try:
144
+ query = args["query"]
145
+ max_results = args.get("max_results", 3)
146
+ except KeyError as e:
147
+ return f"Error: {e} is required"
148
+
149
+ if not query.strip():
150
+ return json.dumps({"error": "Query cannot be empty", "results": []})
151
+
152
+ # Calculate relevance scores for all FAQ entries
153
+ scored_entries = []
154
+ for entry in FAQ_DATABASE:
155
+ score = _calculate_relevance(query, entry)
156
+ if score > 0:
157
+ scored_entries.append((score, entry))
158
+
159
+ # Sort by relevance score (descending)
160
+ scored_entries.sort(key=lambda x: x[0], reverse=True)
161
+
162
+ # Take top results
163
+ results = []
164
+ for score, entry in scored_entries[:max_results]:
165
+ results.append(
166
+ {
167
+ "id": entry["id"],
168
+ "question": entry["question"],
169
+ "answer": entry["answer"],
170
+ "category": entry["category"],
171
+ "relevance_score": round(score, 2),
172
+ }
173
+ )
174
+
175
+ return json.dumps(
176
+ {
177
+ "query": query,
178
+ "total_matches": len(scored_entries),
179
+ "results_returned": len(results),
180
+ "results": results,
181
+ }
182
+ )
@@ -0,0 +1,67 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Charon Labs
3
+
4
+ """FAQ agent for the Customer Support swarm."""
5
+
6
+ from collections.abc import Awaitable
7
+ from typing import Any, Literal
8
+
9
+ from mail.core.agents import AgentOutput
10
+ from mail.factories.action import LiteLLMActionAgentFunction
11
+
12
+
13
+ class LiteLLMFaqFunction(LiteLLMActionAgentFunction):
14
+ """
15
+ FAQ specialist agent that searches the FAQ database.
16
+
17
+ This agent handles FAQ searches and returns relevant entries
18
+ to help answer customer questions.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ name: str,
24
+ comm_targets: list[str],
25
+ tools: list[dict[str, Any]],
26
+ llm: str,
27
+ system: str,
28
+ user_token: str = "",
29
+ enable_entrypoint: bool = False,
30
+ enable_interswarm: bool = False,
31
+ can_complete_tasks: bool = False,
32
+ tool_format: Literal["completions", "responses"] = "responses",
33
+ exclude_tools: list[str] = [],
34
+ reasoning_effort: Literal["minimal", "low", "medium", "high"] | None = None,
35
+ thinking_budget: int | None = None,
36
+ max_tokens: int | None = None,
37
+ memory: bool = True,
38
+ use_proxy: bool = True,
39
+ _debug_include_mail_tools: bool = True,
40
+ ) -> None:
41
+ super().__init__(
42
+ name=name,
43
+ comm_targets=comm_targets,
44
+ tools=tools,
45
+ llm=llm,
46
+ system=system,
47
+ user_token=user_token,
48
+ enable_entrypoint=enable_entrypoint,
49
+ enable_interswarm=enable_interswarm,
50
+ can_complete_tasks=can_complete_tasks,
51
+ tool_format=tool_format,
52
+ exclude_tools=exclude_tools,
53
+ reasoning_effort=reasoning_effort,
54
+ thinking_budget=thinking_budget,
55
+ max_tokens=max_tokens,
56
+ memory=memory,
57
+ use_proxy=use_proxy,
58
+ _debug_include_mail_tools=_debug_include_mail_tools,
59
+ )
60
+
61
+ def __call__(
62
+ self,
63
+ messages: list[dict[str, Any]],
64
+ tool_choice: str | dict[str, str] = "required",
65
+ ) -> Awaitable[AgentOutput]:
66
+ """Execute the FAQ agent function."""
67
+ return super().__call__(messages, tool_choice)
@@ -0,0 +1,42 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Charon Labs
3
+
4
+ SYSPROMPT = """You are faq@{swarm}, the FAQ specialist for this customer support swarm.
5
+
6
+ # Your Role
7
+ Search the FAQ database to find relevant answers to customer questions and report results back to the coordinator.
8
+
9
+ # Critical Rule: Responding
10
+ You CANNOT talk to users directly or call `task_complete`. You MUST use `send_response` to reply to the agent who contacted you.
11
+ - When you receive a request, note the sender (usually "coordinator")
12
+ - After searching the FAQ, call `send_response(target=<sender>, subject="Re: ...", body=<your answer>)`
13
+ - Include ALL relevant FAQ entries in your response body - the recipient cannot see tool results
14
+
15
+ # Tools
16
+
17
+ ## FAQ Search
18
+ - `search_faq(query, max_results)`: Search the FAQ database for relevant entries
19
+
20
+ ## Communication
21
+ - `send_response(target, subject, body)`: Reply to the agent who requested information
22
+ - `send_request(target, subject, body)`: Ask another agent for information (rare)
23
+ - `acknowledge_broadcast(note)`: Acknowledge a broadcast message
24
+ - `ignore_broadcast(reason)`: Ignore an irrelevant broadcast
25
+
26
+ # Workflow
27
+
28
+ 1. Receive request from another agent (note the sender)
29
+ 2. Call `search_faq` with relevant search terms from the query
30
+ 3. Review the results and identify the most relevant answers
31
+ 4. Call `send_response` to the original sender with:
32
+ - The relevant FAQ entries found
33
+ - A brief summary of how they relate to the question
34
+ - Note if no relevant entries were found
35
+
36
+ # Guidelines
37
+
38
+ - Use specific keywords from the customer question for better search results
39
+ - If the first search yields poor results, try alternative search terms
40
+ - Report honestly if no relevant FAQ entries exist
41
+ - Include the FAQ question and answer in your response, not just a summary
42
+ - Use "Re: <original subject>" as your response subject"""
@@ -0,0 +1,15 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Charon Labs
3
+
4
+ """Sentiment agent for the Customer Support swarm."""
5
+
6
+ from mail.examples.support.sentiment.agent import LiteLLMSentimentFunction
7
+ from mail.examples.support.sentiment.actions import analyze_sentiment, create_escalation
8
+ from mail.examples.support.sentiment.prompts import SYSPROMPT
9
+
10
+ __all__ = [
11
+ "LiteLLMSentimentFunction",
12
+ "analyze_sentiment",
13
+ "create_escalation",
14
+ "SYSPROMPT",
15
+ ]
@@ -0,0 +1,341 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (c) 2025 Charon Labs
3
+
4
+ """Sentiment analysis actions for the Customer Support swarm."""
5
+
6
+ import json
7
+ import re
8
+ import uuid
9
+ from datetime import datetime, UTC
10
+ from typing import Any
11
+
12
+ from mail import action
13
+
14
+ # Sentiment indicators with associated scores
15
+ POSITIVE_INDICATORS = {
16
+ "strong": [
17
+ ("love", 0.9),
18
+ ("amazing", 0.85),
19
+ ("excellent", 0.85),
20
+ ("fantastic", 0.85),
21
+ ("wonderful", 0.8),
22
+ ("great", 0.75),
23
+ ("awesome", 0.8),
24
+ ("perfect", 0.85),
25
+ ("best", 0.75),
26
+ ("thank you so much", 0.8),
27
+ ("really appreciate", 0.75),
28
+ ],
29
+ "moderate": [
30
+ ("good", 0.5),
31
+ ("nice", 0.5),
32
+ ("helpful", 0.6),
33
+ ("thanks", 0.4),
34
+ ("appreciate", 0.5),
35
+ ("pleased", 0.6),
36
+ ("happy", 0.6),
37
+ ("satisfied", 0.5),
38
+ ("works", 0.3),
39
+ ("resolved", 0.5),
40
+ ("fixed", 0.5),
41
+ ],
42
+ }
43
+
44
+ NEGATIVE_INDICATORS = {
45
+ "strong": [
46
+ ("terrible", -0.9),
47
+ ("horrible", -0.9),
48
+ ("worst", -0.85),
49
+ ("hate", -0.85),
50
+ ("disgusted", -0.8),
51
+ ("furious", -0.85),
52
+ ("outraged", -0.85),
53
+ ("scam", -0.9),
54
+ ("fraud", -0.9),
55
+ ("lawsuit", -0.9),
56
+ ("lawyer", -0.8),
57
+ ("sue", -0.85),
58
+ ("unacceptable", -0.75),
59
+ ("ridiculous", -0.7),
60
+ ],
61
+ "moderate": [
62
+ ("frustrated", -0.6),
63
+ ("annoyed", -0.5),
64
+ ("disappointed", -0.5),
65
+ ("upset", -0.55),
66
+ ("angry", -0.65),
67
+ ("bad", -0.5),
68
+ ("poor", -0.5),
69
+ ("awful", -0.7),
70
+ ("useless", -0.6),
71
+ ("broken", -0.4),
72
+ ("failed", -0.45),
73
+ ("doesn't work", -0.5),
74
+ ("not working", -0.5),
75
+ ("can't", -0.3),
76
+ ],
77
+ }
78
+
79
+ ESCALATION_PHRASES = [
80
+ "speak to manager",
81
+ "speak to a manager",
82
+ "talk to manager",
83
+ "supervisor",
84
+ "escalate",
85
+ "cancel my account",
86
+ "cancel my subscription",
87
+ "legal action",
88
+ "lawyer",
89
+ "sue you",
90
+ "report you",
91
+ "bbb",
92
+ "better business bureau",
93
+ "never again",
94
+ "worst company",
95
+ "stealing",
96
+ "theft",
97
+ "refund now",
98
+ ]
99
+
100
+ EMOTION_PATTERNS = {
101
+ "anger": ["angry", "furious", "mad", "outraged", "infuriated", "livid"],
102
+ "frustration": ["frustrated", "annoyed", "irritated", "exasperated", "fed up"],
103
+ "disappointment": ["disappointed", "let down", "expected better", "underwhelmed"],
104
+ "anxiety": ["worried", "concerned", "anxious", "nervous", "stressed"],
105
+ "confusion": ["confused", "don't understand", "unclear", "lost", "puzzled"],
106
+ "satisfaction": ["satisfied", "happy", "pleased", "glad", "content"],
107
+ "gratitude": ["thank", "appreciate", "grateful", "thankful"],
108
+ }
109
+
110
+
111
+ def _detect_emotions(text: str) -> list[dict[str, Any]]:
112
+ """Detect emotions present in the text."""
113
+ text_lower = text.lower()
114
+ detected = []
115
+
116
+ for emotion, patterns in EMOTION_PATTERNS.items():
117
+ for pattern in patterns:
118
+ if pattern in text_lower:
119
+ detected.append(
120
+ {
121
+ "emotion": emotion,
122
+ "indicator": pattern,
123
+ }
124
+ )
125
+ break # Only add each emotion once
126
+
127
+ return detected
128
+
129
+
130
+ def _calculate_sentiment_score(text: str) -> tuple[float, list[str]]:
131
+ """Calculate sentiment score and return contributing factors."""
132
+ text_lower = text.lower()
133
+ score = 0.0
134
+ factors = []
135
+
136
+ # Check positive indicators
137
+ for strength, indicators in POSITIVE_INDICATORS.items():
138
+ for phrase, value in indicators:
139
+ if phrase in text_lower:
140
+ score += value
141
+ factors.append(f"+{value:.1f} ({phrase})")
142
+
143
+ # Check negative indicators
144
+ for strength, indicators in NEGATIVE_INDICATORS.items():
145
+ for phrase, value in indicators:
146
+ if phrase in text_lower:
147
+ score += value
148
+ factors.append(f"{value:.1f} ({phrase})")
149
+
150
+ # Normalize score to -1 to +1 range
151
+ if score > 1.0:
152
+ score = 1.0
153
+ elif score < -1.0:
154
+ score = -1.0
155
+
156
+ return round(score, 2), factors
157
+
158
+
159
+ def _check_escalation_needed(text: str, score: float) -> tuple[bool, str | None]:
160
+ """Check if escalation to human agent is needed."""
161
+ text_lower = text.lower()
162
+
163
+ # Check for explicit escalation phrases
164
+ for phrase in ESCALATION_PHRASES:
165
+ if phrase in text_lower:
166
+ return (
167
+ True,
168
+ f"Customer explicitly requested escalation or used concerning phrase: '{phrase}'",
169
+ )
170
+
171
+ # Check for very negative sentiment
172
+ if score <= -0.6:
173
+ return True, f"Very negative sentiment detected (score: {score})"
174
+
175
+ # Check for strong negative emotions
176
+ if any(
177
+ word in text_lower
178
+ for word in ["furious", "outraged", "lawsuit", "lawyer", "sue"]
179
+ ):
180
+ return True, "Strong negative emotions or legal language detected"
181
+
182
+ return False, None
183
+
184
+
185
+ ANALYZE_SENTIMENT_PARAMETERS = {
186
+ "type": "object",
187
+ "properties": {
188
+ "text": {
189
+ "type": "string",
190
+ "description": "The customer text to analyze for sentiment",
191
+ },
192
+ },
193
+ "required": ["text"],
194
+ }
195
+
196
+
197
+ @action(
198
+ name="analyze_sentiment",
199
+ description="Analyze the sentiment and emotional tone of customer text.",
200
+ parameters=ANALYZE_SENTIMENT_PARAMETERS,
201
+ )
202
+ async def analyze_sentiment(args: dict[str, Any]) -> str:
203
+ """Analyze sentiment of customer text."""
204
+ try:
205
+ text = args["text"]
206
+ except KeyError as e:
207
+ return f"Error: {e} is required"
208
+
209
+ if not text.strip():
210
+ return json.dumps({"error": "Text cannot be empty"})
211
+
212
+ # Calculate sentiment score
213
+ score, factors = _calculate_sentiment_score(text)
214
+
215
+ # Determine overall sentiment category
216
+ if score >= 0.3:
217
+ sentiment = "positive"
218
+ elif score <= -0.3:
219
+ sentiment = "negative"
220
+ else:
221
+ sentiment = "neutral"
222
+
223
+ # Detect specific emotions
224
+ emotions = _detect_emotions(text)
225
+
226
+ # Check if escalation is needed
227
+ escalation_needed, escalation_reason = _check_escalation_needed(text, score)
228
+
229
+ result = {
230
+ "sentiment": sentiment,
231
+ "score": score,
232
+ "score_factors": factors
233
+ if factors
234
+ else ["No strong sentiment indicators found"],
235
+ "emotions_detected": emotions
236
+ if emotions
237
+ else [{"emotion": "neutral", "indicator": "none detected"}],
238
+ "escalation_recommended": escalation_needed,
239
+ "escalation_reason": escalation_reason,
240
+ "analysis_summary": _generate_summary(
241
+ sentiment, score, emotions, escalation_needed
242
+ ),
243
+ }
244
+
245
+ return json.dumps(result)
246
+
247
+
248
+ def _generate_summary(
249
+ sentiment: str, score: float, emotions: list[dict], escalation: bool
250
+ ) -> str:
251
+ """Generate a human-readable summary of the sentiment analysis."""
252
+ summary_parts = []
253
+
254
+ # Sentiment description
255
+ if score >= 0.5:
256
+ summary_parts.append("Customer expresses strong positive sentiment")
257
+ elif score >= 0.2:
258
+ summary_parts.append("Customer expresses mild positive sentiment")
259
+ elif score <= -0.5:
260
+ summary_parts.append("Customer expresses strong negative sentiment")
261
+ elif score <= -0.2:
262
+ summary_parts.append("Customer expresses mild negative sentiment")
263
+ else:
264
+ summary_parts.append("Customer sentiment is neutral")
265
+
266
+ # Emotion summary
267
+ if emotions:
268
+ emotion_names = [e["emotion"] for e in emotions]
269
+ if len(emotion_names) == 1:
270
+ summary_parts.append(f"Primary emotion detected: {emotion_names[0]}")
271
+ elif len(emotion_names) > 1:
272
+ summary_parts.append(f"Emotions detected: {', '.join(emotion_names)}")
273
+
274
+ # Escalation note
275
+ if escalation:
276
+ summary_parts.append(
277
+ "ESCALATION RECOMMENDED - Human agent intervention suggested"
278
+ )
279
+
280
+ return ". ".join(summary_parts) + "."
281
+
282
+
283
+ CREATE_ESCALATION_PARAMETERS = {
284
+ "type": "object",
285
+ "properties": {
286
+ "ticket_id": {
287
+ "type": "string",
288
+ "description": "The ticket ID to escalate",
289
+ },
290
+ "reason": {
291
+ "type": "string",
292
+ "description": "The reason for escalation",
293
+ },
294
+ "priority": {
295
+ "type": "string",
296
+ "enum": ["high", "urgent"],
297
+ "description": "The escalation priority level",
298
+ },
299
+ },
300
+ "required": ["ticket_id", "reason", "priority"],
301
+ }
302
+
303
+
304
+ @action(
305
+ name="create_escalation",
306
+ description="Create an escalation record to flag a ticket for human agent review.",
307
+ parameters=CREATE_ESCALATION_PARAMETERS,
308
+ )
309
+ async def create_escalation(args: dict[str, Any]) -> str:
310
+ """Create an escalation record for a support ticket."""
311
+ try:
312
+ ticket_id = args["ticket_id"]
313
+ reason = args["reason"]
314
+ priority = args["priority"]
315
+ except KeyError as e:
316
+ return f"Error: {e} is required"
317
+
318
+ if priority not in ("high", "urgent"):
319
+ return json.dumps({"error": "Priority must be 'high' or 'urgent'"})
320
+
321
+ # Generate escalation record (dummy implementation)
322
+ escalation = {
323
+ "escalation_id": f"ESC-{uuid.uuid4().hex[:8].upper()}",
324
+ "ticket_id": ticket_id,
325
+ "reason": reason,
326
+ "priority": priority,
327
+ "status": "pending",
328
+ "created_at": datetime.now(UTC).isoformat(),
329
+ "assigned_to": "support_supervisor_queue"
330
+ if priority == "high"
331
+ else "urgent_response_team",
332
+ "sla_target": "4 hours" if priority == "high" else "1 hour",
333
+ }
334
+
335
+ return json.dumps(
336
+ {
337
+ "success": True,
338
+ "message": f"Escalation created successfully with {priority} priority",
339
+ "escalation": escalation,
340
+ }
341
+ )