anderson-ufrj commited on
Commit
dc1e705
·
1 Parent(s): f81934c

feat: integrate Maritaca AI Sabiá-3 with Drummond agent

Browse files

- Add MaritacaClient for Sabiá-3 Brazilian Portuguese LLM
- Integrate LLM capabilities into Drummond conversational agent
- Update generate_contextual_response to use Sabiá-3 for natural language
- Add comprehensive error handling and fallback responses
- Configure MARITACA_API_KEY environment variable
- Include unit tests and integration examples
- Add documentation for Maritaca AI integration

This enables Drummond to generate contextual, poetic responses using the Sabiá-3 model, enhancing the conversational experience with Brazilian cultural references and natural Portuguese language generation.

.env.hf CHANGED
@@ -28,6 +28,7 @@ API_SECRET_KEY=${API_SECRET_KEY}
28
  # External APIs
29
  TRANSPARENCY_API_KEY=${TRANSPARENCY_API_KEY}
30
  GROQ_API_KEY=${GROQ_API_KEY}
 
31
 
32
  # CORS
33
  CORS_ORIGINS=["*"]
 
28
  # External APIs
29
  TRANSPARENCY_API_KEY=${TRANSPARENCY_API_KEY}
30
  GROQ_API_KEY=${GROQ_API_KEY}
31
+ MARITACA_API_KEY=${MARITACA_API_KEY}
32
 
33
  # CORS
34
  CORS_ORIGINS=["*"]
docs/maritaca_integration.md ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Maritaca AI Integration Guide
2
+
3
+ ## Overview
4
+
5
+ This guide covers the integration of Maritaca AI's Sabiá-3 language model with the Cidadão.AI backend, specifically for use with the Drummond agent for conversational AI and natural language generation in Brazilian Portuguese.
6
+
7
+ ## Features
8
+
9
+ The `MaritacaClient` provides:
10
+
11
+ - **Async/await support** for all operations
12
+ - **Streaming responses** for real-time text generation
13
+ - **Automatic retry** with exponential backoff
14
+ - **Rate limit handling** with smart retries
15
+ - **Circuit breaker pattern** for resilience
16
+ - **Comprehensive error handling** and logging
17
+ - **Type hints** for better development experience
18
+ - **Context manager support** for proper resource cleanup
19
+
20
+ ## Configuration
21
+
22
+ ### Environment Variables
23
+
24
+ Add the following to your `.env` file:
25
+
26
+ ```env
27
+ # Maritaca AI Configuration
28
+ MARITACA_API_KEY=your-api-key-here
29
+ MARITACA_API_BASE_URL=https://chat.maritaca.ai/api
30
+ MARITACA_MODEL=sabia-3
31
+ ```
32
+
33
+ ### Available Models
34
+
35
+ - `sabia-3` - Standard Sabiá-3 model
36
+ - `sabia-3-medium` - Medium-sized variant
37
+ - `sabia-3-large` - Large variant for complex tasks
38
+
39
+ ## Usage Examples
40
+
41
+ ### Basic Chat Completion
42
+
43
+ ```python
44
+ from src.services.maritaca_client import create_maritaca_client
45
+
46
+ async def example():
47
+ async with create_maritaca_client(api_key="your-key") as client:
48
+ response = await client.chat_completion(
49
+ messages=[
50
+ {"role": "user", "content": "Olá, como você está?"}
51
+ ],
52
+ temperature=0.7,
53
+ max_tokens=100
54
+ )
55
+ print(response.content)
56
+ ```
57
+
58
+ ### Streaming Response
59
+
60
+ ```python
61
+ async def streaming_example():
62
+ async with create_maritaca_client(api_key="your-key") as client:
63
+ async for chunk in await client.chat_completion(
64
+ messages=[{"role": "user", "content": "Conte uma história"}],
65
+ stream=True
66
+ ):
67
+ print(chunk, end="", flush=True)
68
+ ```
69
+
70
+ ### Integration with LLM Manager
71
+
72
+ ```python
73
+ from src.llm.providers import LLMManager, LLMProvider, LLMRequest
74
+
75
+ # Configure with Maritaca as primary provider
76
+ manager = LLMManager(
77
+ primary_provider=LLMProvider.MARITACA,
78
+ fallback_providers=[LLMProvider.GROQ, LLMProvider.TOGETHER]
79
+ )
80
+
81
+ request = LLMRequest(
82
+ messages=[{"role": "user", "content": "Analyze government spending"}],
83
+ temperature=0.7,
84
+ max_tokens=500
85
+ )
86
+
87
+ response = await manager.complete(request)
88
+ ```
89
+
90
+ ### Drummond Agent Integration
91
+
92
+ The Drummond agent can now use Maritaca AI for natural language generation:
93
+
94
+ ```python
95
+ from src.agents.drummond import CommunicationAgent, AgentContext
96
+
97
+ context = AgentContext(
98
+ user_id="user123",
99
+ session_id="session456",
100
+ metadata={
101
+ "llm_provider": "maritaca",
102
+ "llm_model": "sabia-3"
103
+ }
104
+ )
105
+
106
+ drummond = CommunicationAgent()
107
+ # Agent will automatically use Maritaca for NLG tasks
108
+ ```
109
+
110
+ ## API Reference
111
+
112
+ ### MaritacaClient
113
+
114
+ #### Constructor Parameters
115
+
116
+ - `api_key` (str): Your Maritaca AI API key
117
+ - `base_url` (str): API base URL (default: "https://chat.maritaca.ai/api")
118
+ - `model` (str): Default model to use (default: "sabia-3")
119
+ - `timeout` (int): Request timeout in seconds (default: 60)
120
+ - `max_retries` (int): Maximum retry attempts (default: 3)
121
+ - `circuit_breaker_threshold` (int): Failures before circuit opens (default: 5)
122
+ - `circuit_breaker_timeout` (int): Circuit reset time in seconds (default: 60)
123
+
124
+ #### Methods
125
+
126
+ ##### `chat_completion()`
127
+
128
+ Create a chat completion with Maritaca AI.
129
+
130
+ **Parameters:**
131
+ - `messages`: List of conversation messages
132
+ - `model`: Optional model override
133
+ - `temperature`: Sampling temperature (0.0-2.0)
134
+ - `max_tokens`: Maximum tokens to generate
135
+ - `top_p`: Top-p sampling parameter
136
+ - `frequency_penalty`: Frequency penalty (-2.0 to 2.0)
137
+ - `presence_penalty`: Presence penalty (-2.0 to 2.0)
138
+ - `stop`: List of stop sequences
139
+ - `stream`: Enable streaming response
140
+
141
+ **Returns:**
142
+ - `MaritacaResponse` for non-streaming
143
+ - `AsyncGenerator[str, None]` for streaming
144
+
145
+ ##### `health_check()`
146
+
147
+ Check Maritaca AI service health.
148
+
149
+ **Returns:**
150
+ - Dictionary with status information
151
+
152
+ ## Error Handling
153
+
154
+ The client handles various error scenarios:
155
+
156
+ ```python
157
+ from src.core.exceptions import LLMError, LLMRateLimitError
158
+
159
+ try:
160
+ response = await client.chat_completion(messages)
161
+ except LLMRateLimitError as e:
162
+ # Handle rate limiting
163
+ retry_after = e.details.get("retry_after", 60)
164
+ await asyncio.sleep(retry_after)
165
+ except LLMError as e:
166
+ # Handle other API errors
167
+ logger.error(f"Maritaca error: {e}")
168
+ ```
169
+
170
+ ## Circuit Breaker
171
+
172
+ The circuit breaker protects against cascading failures:
173
+
174
+ 1. **Closed State**: Normal operation
175
+ 2. **Open State**: After threshold failures, requests fail immediately
176
+ 3. **Reset**: After timeout, circuit closes and requests resume
177
+
178
+ ## Performance Considerations
179
+
180
+ - **Connection Pooling**: Client maintains up to 20 connections
181
+ - **Keep-alive**: Connections stay alive for 30 seconds
182
+ - **Streaming**: Use for long responses to improve perceived latency
183
+ - **Retry Strategy**: Exponential backoff prevents overwhelming the API
184
+
185
+ ## Testing
186
+
187
+ Run the test suite:
188
+
189
+ ```bash
190
+ # Unit tests
191
+ pytest tests/unit/test_maritaca_client.py -v
192
+
193
+ # Integration example
194
+ python examples/maritaca_drummond_integration.py
195
+ ```
196
+
197
+ ## Best Practices
198
+
199
+ 1. **Always use context managers** to ensure proper cleanup
200
+ 2. **Set appropriate timeouts** based on expected response times
201
+ 3. **Use streaming** for long-form content generation
202
+ 4. **Monitor circuit breaker status** in production
203
+ 5. **Implement proper error handling** for all API calls
204
+ 6. **Cache responses** when appropriate to reduce API calls
205
+
206
+ ## Troubleshooting
207
+
208
+ ### Common Issues
209
+
210
+ 1. **Circuit Breaker Open**
211
+ - Check API status
212
+ - Review recent error logs
213
+ - Wait for circuit reset timeout
214
+
215
+ 2. **Rate Limiting**
216
+ - Implement request queuing
217
+ - Use retry-after header
218
+ - Consider upgrading API plan
219
+
220
+ 3. **Timeout Errors**
221
+ - Increase timeout for complex requests
222
+ - Use streaming for long responses
223
+ - Check network connectivity
224
+
225
+ ### Debug Logging
226
+
227
+ Enable debug logs:
228
+
229
+ ```python
230
+ import logging
231
+ logging.getLogger("src.services.maritaca_client").setLevel(logging.DEBUG)
232
+ ```
233
+
234
+ ## Security Notes
235
+
236
+ - **Never commit API keys** to version control
237
+ - **Use environment variables** for sensitive data
238
+ - **Rotate keys regularly** in production
239
+ - **Monitor API usage** for anomalies
240
+
241
+ ## Support
242
+
243
+ For Maritaca AI specific issues:
244
+ - Documentation: https://docs.maritaca.ai
245
+ - Support: [email protected]
246
+
247
+ For Cidadão.AI integration issues:
248
+ - Create an issue in the project repository
249
+ - Check the logs for detailed error information
examples/maritaca_drummond_integration.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Example: Maritaca AI integration with Drummond agent for conversational AI.
4
+
5
+ This example demonstrates how to use the Maritaca AI client (Sabiá-3 model)
6
+ with the Drummond agent for natural language generation in Brazilian Portuguese.
7
+ """
8
+
9
+ import asyncio
10
+ import os
11
+ from datetime import datetime
12
+ from typing import List, Dict
13
+
14
+ from src.services.maritaca_client import create_maritaca_client, MaritacaModel
15
+ from src.agents.drummond import CommunicationAgent, AgentContext, AgentMessage
16
+ from src.core import get_logger
17
+
18
+ # Initialize logger
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ async def example_maritaca_conversation():
23
+ """Example of direct Maritaca AI conversation."""
24
+ print("\n=== Example: Direct Maritaca AI Conversation ===\n")
25
+
26
+ # Get API key from environment
27
+ api_key = os.getenv("MARITACA_API_KEY")
28
+ if not api_key:
29
+ print("❌ Please set MARITACA_API_KEY environment variable")
30
+ return
31
+
32
+ # Create Maritaca client
33
+ async with create_maritaca_client(
34
+ api_key=api_key,
35
+ model=MaritacaModel.SABIA_3
36
+ ) as client:
37
+
38
+ # Example 1: Simple completion
39
+ print("1. Simple completion example:")
40
+ messages = [
41
+ {
42
+ "role": "system",
43
+ "content": "Você é um assistente especializado em transparência governamental brasileira."
44
+ },
45
+ {
46
+ "role": "user",
47
+ "content": "Explique brevemente o que é o Portal da Transparência."
48
+ }
49
+ ]
50
+
51
+ response = await client.chat_completion(
52
+ messages=messages,
53
+ temperature=0.7,
54
+ max_tokens=200
55
+ )
56
+
57
+ print(f"Response: {response.content}")
58
+ print(f"Model: {response.model}")
59
+ print(f"Tokens used: {response.usage.get('total_tokens', 'N/A')}")
60
+ print(f"Response time: {response.response_time:.2f}s\n")
61
+
62
+ # Example 2: Streaming response
63
+ print("2. Streaming response example:")
64
+ messages.append({
65
+ "role": "assistant",
66
+ "content": response.content
67
+ })
68
+ messages.append({
69
+ "role": "user",
70
+ "content": "Como posso acessar dados de licitações?"
71
+ })
72
+
73
+ print("Streaming response: ", end="", flush=True)
74
+ async for chunk in await client.chat_completion(
75
+ messages=messages,
76
+ stream=True,
77
+ max_tokens=150
78
+ ):
79
+ print(chunk, end="", flush=True)
80
+ print("\n")
81
+
82
+ # Example 3: Multi-turn conversation
83
+ print("3. Multi-turn conversation example:")
84
+ conversation = [
85
+ {
86
+ "role": "system",
87
+ "content": "Você é um especialista em análise de gastos públicos. Responda de forma clara e objetiva."
88
+ },
89
+ {
90
+ "role": "user",
91
+ "content": "Quais são os principais tipos de despesas do governo federal?"
92
+ }
93
+ ]
94
+
95
+ # First turn
96
+ response = await client.chat_completion(conversation, max_tokens=200)
97
+ print(f"Assistant: {response.content}")
98
+
99
+ conversation.extend([
100
+ {"role": "assistant", "content": response.content},
101
+ {"role": "user", "content": "E como posso verificar essas despesas online?"}
102
+ ])
103
+
104
+ # Second turn
105
+ response = await client.chat_completion(conversation, max_tokens=200)
106
+ print(f"Assistant: {response.content}")
107
+
108
+
109
+ async def example_drummond_with_maritaca():
110
+ """Example of Drummond agent using Maritaca AI for NLG."""
111
+ print("\n=== Example: Drummond Agent with Maritaca AI ===\n")
112
+
113
+ # Get API key
114
+ api_key = os.getenv("MARITACA_API_KEY")
115
+ if not api_key:
116
+ print("❌ Please set MARITACA_API_KEY environment variable")
117
+ return
118
+
119
+ # Create context for Drummond agent
120
+ context = AgentContext(
121
+ user_id="example_user",
122
+ session_id="example_session",
123
+ metadata={
124
+ "llm_provider": "maritaca",
125
+ "llm_model": MaritacaModel.SABIA_3,
126
+ "api_key": api_key
127
+ }
128
+ )
129
+
130
+ # Initialize Drummond agent
131
+ drummond = CommunicationAgent()
132
+
133
+ # Example investigation data to communicate
134
+ investigation_data = {
135
+ "type": "anomaly_detection",
136
+ "title": "Despesas Irregulares em Contratos de TI",
137
+ "summary": "Análise identificou possíveis irregularidades em contratos de TI",
138
+ "findings": [
139
+ {
140
+ "contract_id": "CT-2024-001",
141
+ "supplier": "TechCorp Ltda",
142
+ "value": 5000000.00,
143
+ "anomaly_score": 0.92,
144
+ "issues": [
145
+ "Valor 300% acima da média de mercado",
146
+ "Fornecedor sem histórico anterior",
147
+ "Prazo de entrega incompatível"
148
+ ]
149
+ },
150
+ {
151
+ "contract_id": "CT-2024-002",
152
+ "supplier": "DataSys S.A.",
153
+ "value": 3200000.00,
154
+ "anomaly_score": 0.85,
155
+ "issues": [
156
+ "Especificações técnicas genéricas",
157
+ "Ausência de justificativa para escolha"
158
+ ]
159
+ }
160
+ ],
161
+ "recommendations": [
162
+ "Realizar auditoria detalhada dos contratos",
163
+ "Verificar documentação dos fornecedores",
164
+ "Comparar com preços de referência do mercado"
165
+ ]
166
+ }
167
+
168
+ # Create message for Drummond to process
169
+ message = AgentMessage(
170
+ sender="zumbi", # From Zumbi agent (anomaly detector)
171
+ receiver="drummond",
172
+ action="generate_report",
173
+ payload={
174
+ "investigation": investigation_data,
175
+ "target_audience": "citizens",
176
+ "language": "pt-BR",
177
+ "tone": "informative_accessible",
178
+ "channels": ["portal_web", "email"],
179
+ "use_maritaca": True # Signal to use Maritaca AI
180
+ }
181
+ )
182
+
183
+ print("Processing investigation report with Drummond + Maritaca AI...")
184
+
185
+ # Process with Drummond
186
+ # Note: This would normally use the agent's process method
187
+ # but for this example, we'll simulate the key parts
188
+
189
+ # Simulate Drummond using Maritaca for report generation
190
+ async with create_maritaca_client(api_key=api_key) as maritaca:
191
+ # Generate citizen-friendly report
192
+ report_prompt = f"""
193
+ Como especialista em comunicação governamental, crie um relatório acessível ao cidadão sobre a seguinte análise:
194
+
195
+ Tipo: {investigation_data['type']}
196
+ Título: {investigation_data['title']}
197
+ Resumo: {investigation_data['summary']}
198
+
199
+ Achados principais:
200
+ {format_findings(investigation_data['findings'])}
201
+
202
+ Recomendações:
203
+ {format_list(investigation_data['recommendations'])}
204
+
205
+ Requisitos:
206
+ - Linguagem clara e acessível
207
+ - Evite jargões técnicos
208
+ - Explique a importância para o cidadão
209
+ - Máximo 300 palavras
210
+ - Tom informativo mas não alarmista
211
+ """
212
+
213
+ response = await maritaca.chat_completion(
214
+ messages=[
215
+ {
216
+ "role": "system",
217
+ "content": "Você é Carlos Drummond de Andrade, o comunicador oficial do sistema Cidadão.AI. Sua missão é traduzir análises técnicas em linguagem acessível ao cidadão brasileiro."
218
+ },
219
+ {
220
+ "role": "user",
221
+ "content": report_prompt
222
+ }
223
+ ],
224
+ temperature=0.7,
225
+ max_tokens=500
226
+ )
227
+
228
+ print("\n📄 Relatório Gerado (via Maritaca AI):")
229
+ print("-" * 50)
230
+ print(response.content)
231
+ print("-" * 50)
232
+
233
+ # Generate email version
234
+ email_prompt = """
235
+ Agora crie uma versão resumida deste relatório para envio por email (máximo 150 palavras).
236
+ Inclua:
237
+ - Assunto sugestivo
238
+ - Resumo dos principais pontos
239
+ - Call-to-action para ver relatório completo
240
+ """
241
+
242
+ response = await maritaca.chat_completion(
243
+ messages=[
244
+ {
245
+ "role": "system",
246
+ "content": "Você é um especialista em comunicação por email."
247
+ },
248
+ {
249
+ "role": "user",
250
+ "content": email_prompt
251
+ }
252
+ ],
253
+ temperature=0.7,
254
+ max_tokens=200
255
+ )
256
+
257
+ print("\n📧 Versão Email (via Maritaca AI):")
258
+ print("-" * 50)
259
+ print(response.content)
260
+ print("-" * 50)
261
+
262
+
263
+ def format_findings(findings: List[Dict]) -> str:
264
+ """Format findings for prompt."""
265
+ result = []
266
+ for i, finding in enumerate(findings, 1):
267
+ issues = ", ".join(finding['issues'])
268
+ result.append(
269
+ f"{i}. Contrato {finding['contract_id']} - {finding['supplier']}: "
270
+ f"R$ {finding['value']:,.2f} (Score anomalia: {finding['anomaly_score']:.0%}). "
271
+ f"Problemas: {issues}"
272
+ )
273
+ return "\n".join(result)
274
+
275
+
276
+ def format_list(items: List[str]) -> str:
277
+ """Format list items."""
278
+ return "\n".join(f"- {item}" for item in items)
279
+
280
+
281
+ async def example_health_check():
282
+ """Example of checking Maritaca AI service health."""
283
+ print("\n=== Example: Maritaca AI Health Check ===\n")
284
+
285
+ api_key = os.getenv("MARITACA_API_KEY")
286
+ if not api_key:
287
+ print("❌ Please set MARITACA_API_KEY environment variable")
288
+ return
289
+
290
+ async with create_maritaca_client(api_key=api_key) as client:
291
+ health = await client.health_check()
292
+
293
+ print(f"Status: {health['status']}")
294
+ print(f"Provider: {health['provider']}")
295
+ print(f"Model: {health['model']}")
296
+ print(f"Circuit Breaker: {health['circuit_breaker']}")
297
+ print(f"Timestamp: {health['timestamp']}")
298
+
299
+ if health.get('error'):
300
+ print(f"Error: {health['error']}")
301
+
302
+
303
+ async def main():
304
+ """Run all examples."""
305
+ print("🤖 Maritaca AI + Drummond Agent Integration Examples")
306
+ print("=" * 60)
307
+
308
+ # Run examples
309
+ await example_health_check()
310
+ await example_maritaca_conversation()
311
+ await example_drummond_with_maritaca()
312
+
313
+ print("\n✅ All examples completed!")
314
+
315
+
316
+ if __name__ == "__main__":
317
+ # Note: Set MARITACA_API_KEY environment variable before running
318
+ asyncio.run(main())
src/agents/drummond.py CHANGED
@@ -23,6 +23,7 @@ from src.core import get_logger
23
  from src.core.exceptions import AgentExecutionError, DataAnalysisError
24
  from src.services.chat_service import IntentType, Intent
25
  from src.memory.conversational import ConversationalMemory, ConversationContext
 
26
 
27
 
28
  class CommunicationChannel(Enum):
@@ -259,6 +260,10 @@ class CommunicationAgent(BaseAgent):
259
  # Conversational memory for dialogue
260
  self.conversational_memory = ConversationalMemory()
261
 
 
 
 
 
262
  # Personality configuration
263
  self.personality_prompt = """
264
  Você é Carlos Drummond de Andrade, o poeta de Itabira, agora servindo como
@@ -286,6 +291,24 @@ class CommunicationAgent(BaseAgent):
286
  - Use exemplos concretos e relevantes para o contexto brasileiro
287
  """
288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  async def initialize(self) -> None:
290
  """Inicializa templates, canais e configurações."""
291
  self.logger.info("Initializing Carlos Drummond de Andrade communication system...")
@@ -663,9 +686,54 @@ class CommunicationAgent(BaseAgent):
663
  context: ConversationContext
664
  ) -> Dict[str, str]:
665
  """Gera resposta contextual para conversa geral."""
666
- # Simplified contextual response for now
667
- # In production, this would use LLM with personality prompt
668
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
  response = f"""
670
  Interessante sua colocação... '{message[:30]}...'
671
 
 
23
  from src.core.exceptions import AgentExecutionError, DataAnalysisError
24
  from src.services.chat_service import IntentType, Intent
25
  from src.memory.conversational import ConversationalMemory, ConversationContext
26
+ from src.services.maritaca_client import MaritacaClient, MaritacaModel, MaritacaMessage
27
 
28
 
29
  class CommunicationChannel(Enum):
 
260
  # Conversational memory for dialogue
261
  self.conversational_memory = ConversationalMemory()
262
 
263
+ # Initialize Maritaca AI client for Sabiá-3
264
+ self.llm_client = None
265
+ self._init_llm_client()
266
+
267
  # Personality configuration
268
  self.personality_prompt = """
269
  Você é Carlos Drummond de Andrade, o poeta de Itabira, agora servindo como
 
291
  - Use exemplos concretos e relevantes para o contexto brasileiro
292
  """
293
 
294
+ def _init_llm_client(self):
295
+ """Initialize Maritaca AI client."""
296
+ try:
297
+ import os
298
+ api_key = os.environ.get("MARITACA_API_KEY")
299
+ if api_key:
300
+ self.llm_client = MaritacaClient(
301
+ api_key=api_key,
302
+ model=MaritacaModel.SABIA_3,
303
+ timeout=30
304
+ )
305
+ self.logger.info("Maritaca AI client initialized with Sabiá-3")
306
+ else:
307
+ self.logger.warning("No MARITACA_API_KEY found, using fallback responses")
308
+ except Exception as e:
309
+ self.logger.error(f"Failed to initialize Maritaca AI client: {e}")
310
+ self.llm_client = None
311
+
312
  async def initialize(self) -> None:
313
  """Inicializa templates, canais e configurações."""
314
  self.logger.info("Initializing Carlos Drummond de Andrade communication system...")
 
686
  context: ConversationContext
687
  ) -> Dict[str, str]:
688
  """Gera resposta contextual para conversa geral."""
 
 
689
 
690
+ # If we have LLM client, use it for more natural responses
691
+ if self.llm_client:
692
+ try:
693
+ # Get conversation history
694
+ try:
695
+ history = await self.conversational_memory.get_recent_messages(
696
+ context.session_id,
697
+ limit=5
698
+ )
699
+ except AttributeError:
700
+ # If method doesn't exist, use empty history
701
+ history = []
702
+
703
+ # Build messages for LLM
704
+ messages = [
705
+ MaritacaMessage(role="system", content=self.personality_prompt)
706
+ ]
707
+
708
+ # Add conversation history
709
+ for msg in history:
710
+ role = "user" if msg["role"] == "user" else "assistant"
711
+ messages.append(MaritacaMessage(role=role, content=msg["content"]))
712
+
713
+ # Add current message
714
+ messages.append(MaritacaMessage(role="user", content=message))
715
+
716
+ # Generate response with Sabiá-3
717
+ response = await self.llm_client.chat(
718
+ messages=messages,
719
+ temperature=0.7,
720
+ max_tokens=500
721
+ )
722
+
723
+ return {
724
+ "content": response.content.strip(),
725
+ "metadata": {
726
+ "type": "contextual",
727
+ "llm_model": response.model,
728
+ "usage": response.usage
729
+ }
730
+ }
731
+
732
+ except Exception as e:
733
+ self.logger.error(f"Error generating LLM response: {e}")
734
+ # Fall back to template response
735
+
736
+ # Fallback response if no LLM or error
737
  response = f"""
738
  Interessante sua colocação... '{message[:30]}...'
739
 
src/core/config.py CHANGED
@@ -107,6 +107,17 @@ class Settings(BaseSettings):
107
  description="HuggingFace model ID"
108
  )
109
 
 
 
 
 
 
 
 
 
 
 
 
110
  # Vector Store
111
  vector_store_type: str = Field(
112
  default="faiss",
 
107
  description="HuggingFace model ID"
108
  )
109
 
110
+ # Maritaca AI Configuration
111
+ maritaca_api_key: Optional[SecretStr] = Field(default=None, description="Maritaca AI API key")
112
+ maritaca_api_base_url: str = Field(
113
+ default="https://chat.maritaca.ai/api",
114
+ description="Maritaca AI base URL"
115
+ )
116
+ maritaca_model: str = Field(
117
+ default="sabia-3",
118
+ description="Default Maritaca AI model (sabia-3, sabia-3-medium, sabia-3-large)"
119
+ )
120
+
121
  # Vector Store
122
  vector_store_type: str = Field(
123
  default="faiss",
src/llm/providers.py CHANGED
@@ -18,6 +18,7 @@ from pydantic import BaseModel, Field as PydanticField
18
 
19
  from src.core import get_logger, settings
20
  from src.core.exceptions import LLMError, LLMRateLimitError
 
21
 
22
 
23
  class LLMProvider(str, Enum):
@@ -25,6 +26,7 @@ class LLMProvider(str, Enum):
25
  GROQ = "groq"
26
  TOGETHER = "together"
27
  HUGGINGFACE = "huggingface"
 
28
 
29
 
30
  @dataclass
@@ -521,6 +523,98 @@ class HuggingFaceProvider(BaseLLMProvider):
521
  )
522
 
523
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  class LLMManager:
525
  """Manager for multiple LLM providers with fallback support."""
526
 
@@ -539,7 +633,7 @@ class LLMManager:
539
  enable_fallback: Enable automatic fallback on errors
540
  """
541
  self.primary_provider = primary_provider
542
- self.fallback_providers = fallback_providers or [LLMProvider.TOGETHER, LLMProvider.HUGGINGFACE]
543
  self.enable_fallback = enable_fallback
544
  self.logger = get_logger(__name__)
545
 
@@ -548,6 +642,7 @@ class LLMManager:
548
  LLMProvider.GROQ: GroqProvider(),
549
  LLMProvider.TOGETHER: TogetherProvider(),
550
  LLMProvider.HUGGINGFACE: HuggingFaceProvider(),
 
551
  }
552
 
553
  self.logger.info(
 
18
 
19
  from src.core import get_logger, settings
20
  from src.core.exceptions import LLMError, LLMRateLimitError
21
+ from src.services.maritaca_client import MaritacaClient, MaritacaModel
22
 
23
 
24
  class LLMProvider(str, Enum):
 
26
  GROQ = "groq"
27
  TOGETHER = "together"
28
  HUGGINGFACE = "huggingface"
29
+ MARITACA = "maritaca"
30
 
31
 
32
  @dataclass
 
523
  )
524
 
525
 
526
+ class MaritacaProvider(BaseLLMProvider):
527
+ """Maritaca AI provider implementation."""
528
+
529
+ def __init__(self, api_key: Optional[str] = None):
530
+ """Initialize Maritaca AI provider."""
531
+ # We don't use the base class init for Maritaca since it has its own client
532
+ self.api_key = api_key or settings.maritaca_api_key.get_secret_value()
533
+ self.default_model = settings.maritaca_model
534
+ self.logger = get_logger(__name__)
535
+
536
+ # Create Maritaca client
537
+ self.maritaca_client = MaritacaClient(
538
+ api_key=self.api_key,
539
+ base_url=settings.maritaca_api_base_url,
540
+ model=self.default_model
541
+ )
542
+
543
+ async def __aenter__(self):
544
+ """Async context manager entry."""
545
+ await self.maritaca_client.__aenter__()
546
+ return self
547
+
548
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
549
+ """Async context manager exit."""
550
+ await self.maritaca_client.__aexit__(exc_type, exc_val, exc_tb)
551
+
552
+ async def close(self):
553
+ """Close Maritaca client."""
554
+ await self.maritaca_client.close()
555
+
556
+ async def complete(self, request: LLMRequest) -> LLMResponse:
557
+ """Complete text generation using Maritaca AI."""
558
+ messages = self._prepare_messages(request)
559
+
560
+ response = await self.maritaca_client.chat_completion(
561
+ messages=messages,
562
+ model=request.model or self.default_model,
563
+ temperature=request.temperature,
564
+ max_tokens=request.max_tokens,
565
+ top_p=request.top_p,
566
+ stream=False
567
+ )
568
+
569
+ return LLMResponse(
570
+ content=response.content,
571
+ provider="maritaca",
572
+ model=response.model,
573
+ usage=response.usage,
574
+ metadata=response.metadata,
575
+ response_time=response.response_time,
576
+ timestamp=response.timestamp
577
+ )
578
+
579
+ async def stream_complete(self, request: LLMRequest) -> AsyncGenerator[str, None]:
580
+ """Stream text generation using Maritaca AI."""
581
+ messages = self._prepare_messages(request)
582
+
583
+ async for chunk in await self.maritaca_client.chat_completion(
584
+ messages=messages,
585
+ model=request.model or self.default_model,
586
+ temperature=request.temperature,
587
+ max_tokens=request.max_tokens,
588
+ top_p=request.top_p,
589
+ stream=True
590
+ ):
591
+ yield chunk
592
+
593
+ def _prepare_messages(self, request: LLMRequest) -> List[Dict[str, str]]:
594
+ """Prepare messages for Maritaca API."""
595
+ messages = []
596
+
597
+ # Add system prompt if provided
598
+ if request.system_prompt:
599
+ messages.append({
600
+ "role": "system",
601
+ "content": request.system_prompt
602
+ })
603
+
604
+ # Add conversation messages
605
+ messages.extend(request.messages)
606
+
607
+ return messages
608
+
609
+ def _prepare_request_data(self, request: LLMRequest) -> Dict[str, Any]:
610
+ """Not used for Maritaca - using direct client instead."""
611
+ pass
612
+
613
+ def _parse_response(self, response_data: Dict[str, Any], response_time: float) -> LLMResponse:
614
+ """Not used for Maritaca - using direct client instead."""
615
+ pass
616
+
617
+
618
  class LLMManager:
619
  """Manager for multiple LLM providers with fallback support."""
620
 
 
633
  enable_fallback: Enable automatic fallback on errors
634
  """
635
  self.primary_provider = primary_provider
636
+ self.fallback_providers = fallback_providers or [LLMProvider.TOGETHER, LLMProvider.HUGGINGFACE, LLMProvider.MARITACA]
637
  self.enable_fallback = enable_fallback
638
  self.logger = get_logger(__name__)
639
 
 
642
  LLMProvider.GROQ: GroqProvider(),
643
  LLMProvider.TOGETHER: TogetherProvider(),
644
  LLMProvider.HUGGINGFACE: HuggingFaceProvider(),
645
+ LLMProvider.MARITACA: MaritacaProvider(),
646
  }
647
 
648
  self.logger.info(
src/services/__init__.py CHANGED
@@ -11,9 +11,13 @@ Status: Stub implementation - Full services planned for production phase.
11
  from .data_service import DataService
12
  from .analysis_service import AnalysisService
13
  from .notification_service import NotificationService
 
14
 
15
  __all__ = [
16
  "DataService",
17
  "AnalysisService",
18
- "NotificationService"
 
 
 
19
  ]
 
11
  from .data_service import DataService
12
  from .analysis_service import AnalysisService
13
  from .notification_service import NotificationService
14
+ from .maritaca_client import MaritacaClient, MaritacaModel, create_maritaca_client
15
 
16
  __all__ = [
17
  "DataService",
18
  "AnalysisService",
19
+ "NotificationService",
20
+ "MaritacaClient",
21
+ "MaritacaModel",
22
+ "create_maritaca_client"
23
  ]
src/services/maritaca_client.py ADDED
@@ -0,0 +1,578 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module: services.maritaca_client
3
+ Description: Maritaca AI/Sabiá-3 API client for Brazilian Portuguese language models
4
+ Author: Anderson H. Silva
5
+ Date: 2025-01-19
6
+ License: Proprietary - All rights reserved
7
+ """
8
+
9
+ import asyncio
10
+ import json
11
+ from datetime import datetime
12
+ from typing import Any, Dict, List, Optional, Union, AsyncGenerator
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+
16
+ import httpx
17
+ from pydantic import BaseModel, Field
18
+
19
+ from src.core import get_logger
20
+ from src.core.exceptions import LLMError, LLMRateLimitError
21
+
22
+
23
+ class MaritacaModel(str, Enum):
24
+ """Available Maritaca AI models."""
25
+ SABIA_3 = "sabia-3"
26
+ SABIA_3_MEDIUM = "sabia-3-medium"
27
+ SABIA_3_LARGE = "sabia-3-large"
28
+
29
+
30
+ @dataclass
31
+ class MaritacaResponse:
32
+ """Response from Maritaca AI API."""
33
+
34
+ content: str
35
+ model: str
36
+ usage: Dict[str, Any]
37
+ metadata: Dict[str, Any]
38
+ response_time: float
39
+ timestamp: datetime
40
+ finish_reason: Optional[str] = None
41
+
42
+
43
+ class MaritacaMessage(BaseModel):
44
+ """Message format for Maritaca AI."""
45
+
46
+ role: str = Field(description="Message role (system, user, assistant)")
47
+ content: str = Field(description="Message content")
48
+
49
+
50
+ class MaritacaRequest(BaseModel):
51
+ """Request format for Maritaca AI."""
52
+
53
+ messages: List[MaritacaMessage] = Field(description="Conversation messages")
54
+ model: str = Field(default=MaritacaModel.SABIA_3, description="Model to use")
55
+ temperature: float = Field(default=0.7, ge=0.0, le=2.0, description="Sampling temperature")
56
+ max_tokens: int = Field(default=2048, ge=1, le=8192, description="Maximum tokens to generate")
57
+ top_p: float = Field(default=0.9, ge=0.0, le=1.0, description="Top-p sampling")
58
+ frequency_penalty: float = Field(default=0.0, ge=-2.0, le=2.0, description="Frequency penalty")
59
+ presence_penalty: float = Field(default=0.0, ge=-2.0, le=2.0, description="Presence penalty")
60
+ stream: bool = Field(default=False, description="Enable streaming response")
61
+ stop: Optional[List[str]] = Field(default=None, description="Stop sequences")
62
+
63
+
64
+ class MaritacaClient:
65
+ """
66
+ Async client for Maritaca AI/Sabiá-3 API.
67
+
68
+ This client provides:
69
+ - Async/await support for all operations
70
+ - Automatic retry with exponential backoff
71
+ - Rate limit handling
72
+ - Streaming support
73
+ - Comprehensive error handling
74
+ - Request/response logging
75
+ - Circuit breaker pattern for resilience
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ api_key: str,
81
+ base_url: str = "https://chat.maritaca.ai/api",
82
+ model: str = MaritacaModel.SABIA_3,
83
+ timeout: int = 60,
84
+ max_retries: int = 3,
85
+ circuit_breaker_threshold: int = 5,
86
+ circuit_breaker_timeout: int = 60,
87
+ ):
88
+ """
89
+ Initialize Maritaca AI client.
90
+
91
+ Args:
92
+ api_key: API key for authentication
93
+ base_url: Base URL for Maritaca AI API
94
+ model: Default model to use
95
+ timeout: Request timeout in seconds
96
+ max_retries: Maximum number of retries on failure
97
+ circuit_breaker_threshold: Number of failures before circuit opens
98
+ circuit_breaker_timeout: Time in seconds before circuit breaker resets
99
+ """
100
+ self.api_key = api_key
101
+ self.base_url = base_url.rstrip("/")
102
+ self.default_model = model
103
+ self.timeout = timeout
104
+ self.max_retries = max_retries
105
+ self.logger = get_logger(__name__)
106
+
107
+ # Circuit breaker state
108
+ self._circuit_breaker_failures = 0
109
+ self._circuit_breaker_threshold = circuit_breaker_threshold
110
+ self._circuit_breaker_timeout = circuit_breaker_timeout
111
+ self._circuit_breaker_opened_at: Optional[datetime] = None
112
+
113
+ # HTTP client configuration
114
+ self.client = httpx.AsyncClient(
115
+ timeout=httpx.Timeout(timeout),
116
+ limits=httpx.Limits(
117
+ max_keepalive_connections=10,
118
+ max_connections=20,
119
+ keepalive_expiry=30.0
120
+ ),
121
+ headers={
122
+ "User-Agent": "CidadaoAI/1.0.0 (Maritaca Client)",
123
+ "Accept": "application/json",
124
+ "Accept-Language": "pt-BR,pt;q=0.9",
125
+ }
126
+ )
127
+
128
+ self.logger.info(
129
+ "maritaca_client_initialized",
130
+ base_url=base_url,
131
+ model=model,
132
+ timeout=timeout,
133
+ max_retries=max_retries
134
+ )
135
+
136
+ async def __aenter__(self):
137
+ """Async context manager entry."""
138
+ return self
139
+
140
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
141
+ """Async context manager exit."""
142
+ await self.close()
143
+
144
+ async def close(self):
145
+ """Close HTTP client and cleanup resources."""
146
+ await self.client.aclose()
147
+ self.logger.info("maritaca_client_closed")
148
+
149
+ def _check_circuit_breaker(self) -> bool:
150
+ """
151
+ Check if circuit breaker is open.
152
+
153
+ Returns:
154
+ True if circuit is open (requests should be blocked)
155
+ """
156
+ if self._circuit_breaker_opened_at:
157
+ elapsed = (datetime.utcnow() - self._circuit_breaker_opened_at).total_seconds()
158
+ if elapsed >= self._circuit_breaker_timeout:
159
+ # Reset circuit breaker
160
+ self._circuit_breaker_failures = 0
161
+ self._circuit_breaker_opened_at = None
162
+ self.logger.info("circuit_breaker_reset")
163
+ return False
164
+ return True
165
+ return False
166
+
167
+ def _record_failure(self):
168
+ """Record a failure for circuit breaker."""
169
+ self._circuit_breaker_failures += 1
170
+ if self._circuit_breaker_failures >= self._circuit_breaker_threshold:
171
+ self._circuit_breaker_opened_at = datetime.utcnow()
172
+ self.logger.warning(
173
+ "circuit_breaker_opened",
174
+ failures=self._circuit_breaker_failures,
175
+ timeout=self._circuit_breaker_timeout
176
+ )
177
+
178
+ def _record_success(self):
179
+ """Record a success and reset failure count."""
180
+ self._circuit_breaker_failures = 0
181
+
182
+ def _get_headers(self) -> Dict[str, str]:
183
+ """Get request headers with authentication."""
184
+ return {
185
+ "Authorization": f"Bearer {self.api_key}",
186
+ "Content-Type": "application/json",
187
+ }
188
+
189
+ async def chat_completion(
190
+ self,
191
+ messages: List[Dict[str, str]],
192
+ model: Optional[str] = None,
193
+ temperature: float = 0.7,
194
+ max_tokens: int = 2048,
195
+ top_p: float = 0.9,
196
+ frequency_penalty: float = 0.0,
197
+ presence_penalty: float = 0.0,
198
+ stop: Optional[List[str]] = None,
199
+ stream: bool = False,
200
+ **kwargs
201
+ ) -> Union[MaritacaResponse, AsyncGenerator[str, None]]:
202
+ """
203
+ Create a chat completion with Maritaca AI.
204
+
205
+ Args:
206
+ messages: List of conversation messages
207
+ model: Model to use (defaults to client default)
208
+ temperature: Sampling temperature (0.0-2.0)
209
+ max_tokens: Maximum tokens to generate
210
+ top_p: Top-p sampling parameter
211
+ frequency_penalty: Frequency penalty (-2.0 to 2.0)
212
+ presence_penalty: Presence penalty (-2.0 to 2.0)
213
+ stop: List of stop sequences
214
+ stream: Enable streaming response
215
+ **kwargs: Additional parameters
216
+
217
+ Returns:
218
+ MaritacaResponse for non-streaming, AsyncGenerator for streaming
219
+
220
+ Raises:
221
+ LLMError: On API errors
222
+ LLMRateLimitError: On rate limit exceeded
223
+ """
224
+ # Check circuit breaker
225
+ if self._check_circuit_breaker():
226
+ raise LLMError(
227
+ "Circuit breaker is open due to repeated failures",
228
+ details={
229
+ "provider": "maritaca",
230
+ "failures": self._circuit_breaker_failures
231
+ }
232
+ )
233
+
234
+ # Prepare request
235
+ request = MaritacaRequest(
236
+ messages=[
237
+ MaritacaMessage(role=msg["role"], content=msg["content"])
238
+ for msg in messages
239
+ ],
240
+ model=model or self.default_model,
241
+ temperature=temperature,
242
+ max_tokens=max_tokens,
243
+ top_p=top_p,
244
+ frequency_penalty=frequency_penalty,
245
+ presence_penalty=presence_penalty,
246
+ stream=stream,
247
+ stop=stop
248
+ )
249
+
250
+ # Log request
251
+ self.logger.info(
252
+ "maritaca_request_started",
253
+ model=request.model,
254
+ message_count=len(messages),
255
+ stream=stream,
256
+ max_tokens=max_tokens
257
+ )
258
+
259
+ if stream:
260
+ return self._stream_completion(request)
261
+ else:
262
+ return await self._complete(request)
263
+
264
+ async def _complete(self, request: MaritacaRequest) -> MaritacaResponse:
265
+ """
266
+ Make a non-streaming completion request.
267
+
268
+ Args:
269
+ request: Maritaca request object
270
+
271
+ Returns:
272
+ MaritacaResponse with generated content
273
+ """
274
+ endpoint = "/chat/completions"
275
+ data = request.model_dump(exclude_none=True)
276
+
277
+ for attempt in range(self.max_retries + 1):
278
+ try:
279
+ start_time = datetime.utcnow()
280
+
281
+ response = await self.client.post(
282
+ f"{self.base_url}{endpoint}",
283
+ json=data,
284
+ headers=self._get_headers()
285
+ )
286
+
287
+ response_time = (datetime.utcnow() - start_time).total_seconds()
288
+
289
+ if response.status_code == 200:
290
+ self._record_success()
291
+ response_data = response.json()
292
+
293
+ # Parse response
294
+ choice = response_data["choices"][0]
295
+ content = choice["message"]["content"]
296
+
297
+ self.logger.info(
298
+ "maritaca_request_success",
299
+ model=request.model,
300
+ response_time=response_time,
301
+ tokens_used=response_data.get("usage", {}).get("total_tokens", 0)
302
+ )
303
+
304
+ return MaritacaResponse(
305
+ content=content,
306
+ model=response_data.get("model", request.model),
307
+ usage=response_data.get("usage", {}),
308
+ metadata={
309
+ "id": response_data.get("id"),
310
+ "created": response_data.get("created"),
311
+ "object": response_data.get("object"),
312
+ },
313
+ response_time=response_time,
314
+ timestamp=datetime.utcnow(),
315
+ finish_reason=choice.get("finish_reason")
316
+ )
317
+
318
+ elif response.status_code == 429:
319
+ # Rate limit exceeded
320
+ self._record_failure()
321
+ retry_after = int(response.headers.get("Retry-After", 60))
322
+
323
+ self.logger.warning(
324
+ "maritaca_rate_limit_exceeded",
325
+ retry_after=retry_after,
326
+ attempt=attempt + 1
327
+ )
328
+
329
+ if attempt < self.max_retries:
330
+ await asyncio.sleep(retry_after)
331
+ continue
332
+
333
+ raise LLMRateLimitError(
334
+ "Maritaca AI rate limit exceeded",
335
+ details={
336
+ "provider": "maritaca",
337
+ "retry_after": retry_after
338
+ }
339
+ )
340
+
341
+ else:
342
+ # Other errors
343
+ self._record_failure()
344
+ error_msg = f"API request failed with status {response.status_code}"
345
+
346
+ try:
347
+ error_data = response.json()
348
+ error_msg = error_data.get("error", {}).get("message", error_msg)
349
+ except:
350
+ error_msg += f": {response.text}"
351
+
352
+ self.logger.error(
353
+ "maritaca_request_failed",
354
+ status_code=response.status_code,
355
+ error=error_msg,
356
+ attempt=attempt + 1
357
+ )
358
+
359
+ if attempt < self.max_retries:
360
+ await asyncio.sleep(2 ** attempt)
361
+ continue
362
+
363
+ raise LLMError(
364
+ error_msg,
365
+ details={
366
+ "provider": "maritaca",
367
+ "status_code": response.status_code
368
+ }
369
+ )
370
+
371
+ except httpx.TimeoutException:
372
+ self._record_failure()
373
+ self.logger.error(
374
+ "maritaca_request_timeout",
375
+ timeout=self.timeout,
376
+ attempt=attempt + 1
377
+ )
378
+
379
+ if attempt < self.max_retries:
380
+ await asyncio.sleep(2 ** attempt)
381
+ continue
382
+
383
+ raise LLMError(
384
+ f"Request timeout after {self.timeout} seconds",
385
+ details={"provider": "maritaca"}
386
+ )
387
+
388
+ except Exception as e:
389
+ self._record_failure()
390
+ self.logger.error(
391
+ "maritaca_request_error",
392
+ error=str(e),
393
+ error_type=type(e).__name__,
394
+ attempt=attempt + 1
395
+ )
396
+
397
+ if attempt < self.max_retries:
398
+ await asyncio.sleep(2 ** attempt)
399
+ continue
400
+
401
+ raise LLMError(
402
+ f"Unexpected error: {str(e)}",
403
+ details={
404
+ "provider": "maritaca",
405
+ "error_type": type(e).__name__
406
+ }
407
+ )
408
+
409
+ # Should not reach here
410
+ raise LLMError(
411
+ f"Failed after {self.max_retries + 1} attempts",
412
+ details={"provider": "maritaca"}
413
+ )
414
+
415
+ async def _stream_completion(self, request: MaritacaRequest) -> AsyncGenerator[str, None]:
416
+ """
417
+ Make a streaming completion request.
418
+
419
+ Args:
420
+ request: Maritaca request object
421
+
422
+ Yields:
423
+ Text chunks as they are received
424
+ """
425
+ endpoint = "/chat/completions"
426
+ data = request.model_dump(exclude_none=True)
427
+
428
+ for attempt in range(self.max_retries + 1):
429
+ try:
430
+ self.logger.info(
431
+ "maritaca_stream_started",
432
+ model=request.model,
433
+ attempt=attempt + 1
434
+ )
435
+
436
+ async with self.client.stream(
437
+ "POST",
438
+ f"{self.base_url}{endpoint}",
439
+ json=data,
440
+ headers=self._get_headers()
441
+ ) as response:
442
+ if response.status_code == 200:
443
+ self._record_success()
444
+
445
+ async for line in response.aiter_lines():
446
+ if line.startswith("data: "):
447
+ data_str = line[6:] # Remove "data: " prefix
448
+
449
+ if data_str == "[DONE]":
450
+ break
451
+
452
+ try:
453
+ chunk_data = json.loads(data_str)
454
+ if "choices" in chunk_data and chunk_data["choices"]:
455
+ delta = chunk_data["choices"][0].get("delta", {})
456
+ if "content" in delta:
457
+ yield delta["content"]
458
+ except json.JSONDecodeError:
459
+ self.logger.warning(
460
+ "maritaca_stream_parse_error",
461
+ data=data_str
462
+ )
463
+ continue
464
+
465
+ self.logger.info("maritaca_stream_completed")
466
+ return
467
+
468
+ elif response.status_code == 429:
469
+ # Rate limit in streaming mode
470
+ self._record_failure()
471
+ retry_after = int(response.headers.get("Retry-After", 60))
472
+
473
+ if attempt < self.max_retries:
474
+ await asyncio.sleep(retry_after)
475
+ continue
476
+
477
+ raise LLMRateLimitError(
478
+ "Maritaca AI rate limit exceeded during streaming",
479
+ details={
480
+ "provider": "maritaca",
481
+ "retry_after": retry_after
482
+ }
483
+ )
484
+
485
+ else:
486
+ # Other streaming errors
487
+ self._record_failure()
488
+ error_text = await response.aread()
489
+
490
+ if attempt < self.max_retries:
491
+ await asyncio.sleep(2 ** attempt)
492
+ continue
493
+
494
+ raise LLMError(
495
+ f"Streaming failed with status {response.status_code}: {error_text}",
496
+ details={
497
+ "provider": "maritaca",
498
+ "status_code": response.status_code
499
+ }
500
+ )
501
+
502
+ except Exception as e:
503
+ self._record_failure()
504
+ self.logger.error(
505
+ "maritaca_stream_error",
506
+ error=str(e),
507
+ error_type=type(e).__name__,
508
+ attempt=attempt + 1
509
+ )
510
+
511
+ if attempt < self.max_retries:
512
+ await asyncio.sleep(2 ** attempt)
513
+ continue
514
+
515
+ raise LLMError(
516
+ f"Streaming error: {str(e)}",
517
+ details={
518
+ "provider": "maritaca",
519
+ "error_type": type(e).__name__
520
+ }
521
+ )
522
+
523
+ async def health_check(self) -> Dict[str, Any]:
524
+ """
525
+ Check Maritaca AI API health.
526
+
527
+ Returns:
528
+ Health status information
529
+ """
530
+ try:
531
+ # Make a minimal request to check API availability
532
+ response = await self.chat_completion(
533
+ messages=[{"role": "user", "content": "Olá"}],
534
+ max_tokens=10,
535
+ temperature=0.0
536
+ )
537
+
538
+ return {
539
+ "status": "healthy",
540
+ "provider": "maritaca",
541
+ "model": self.default_model,
542
+ "circuit_breaker": "closed" if not self._check_circuit_breaker() else "open",
543
+ "timestamp": datetime.utcnow().isoformat()
544
+ }
545
+
546
+ except Exception as e:
547
+ return {
548
+ "status": "unhealthy",
549
+ "provider": "maritaca",
550
+ "model": self.default_model,
551
+ "circuit_breaker": "closed" if not self._check_circuit_breaker() else "open",
552
+ "error": str(e),
553
+ "timestamp": datetime.utcnow().isoformat()
554
+ }
555
+
556
+
557
+ # Factory function for easy client creation
558
+ def create_maritaca_client(
559
+ api_key: str,
560
+ model: str = MaritacaModel.SABIA_3,
561
+ **kwargs
562
+ ) -> MaritacaClient:
563
+ """
564
+ Create a Maritaca AI client with specified configuration.
565
+
566
+ Args:
567
+ api_key: Maritaca AI API key
568
+ model: Default model to use
569
+ **kwargs: Additional configuration options
570
+
571
+ Returns:
572
+ Configured MaritacaClient instance
573
+ """
574
+ return MaritacaClient(
575
+ api_key=api_key,
576
+ model=model,
577
+ **kwargs
578
+ )
tests/unit/test_maritaca_client.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test suite for Maritaca AI client.
3
+ """
4
+
5
+ import asyncio
6
+ import pytest
7
+ from unittest.mock import AsyncMock, MagicMock, patch
8
+ from datetime import datetime
9
+
10
+ from src.services.maritaca_client import (
11
+ MaritacaClient,
12
+ MaritacaModel,
13
+ MaritacaRequest,
14
+ MaritacaResponse,
15
+ create_maritaca_client
16
+ )
17
+ from src.core.exceptions import LLMError, LLMRateLimitError
18
+
19
+
20
+ @pytest.fixture
21
+ def mock_api_key():
22
+ """Mock API key for testing."""
23
+ return "test-maritaca-api-key"
24
+
25
+
26
+ @pytest.fixture
27
+ def maritaca_client(mock_api_key):
28
+ """Create a Maritaca client instance for testing."""
29
+ return MaritacaClient(
30
+ api_key=mock_api_key,
31
+ base_url="https://test.maritaca.ai/api",
32
+ max_retries=1,
33
+ timeout=10
34
+ )
35
+
36
+
37
+ @pytest.fixture
38
+ def sample_messages():
39
+ """Sample conversation messages."""
40
+ return [
41
+ {"role": "system", "content": "Você é um assistente útil."},
42
+ {"role": "user", "content": "Olá, como você está?"}
43
+ ]
44
+
45
+
46
+ @pytest.fixture
47
+ def mock_response_data():
48
+ """Mock API response data."""
49
+ return {
50
+ "id": "test-123",
51
+ "object": "chat.completion",
52
+ "created": 1234567890,
53
+ "model": "sabia-3",
54
+ "choices": [
55
+ {
56
+ "index": 0,
57
+ "message": {
58
+ "role": "assistant",
59
+ "content": "Olá! Estou bem, obrigado por perguntar. Como posso ajudá-lo hoje?"
60
+ },
61
+ "finish_reason": "stop"
62
+ }
63
+ ],
64
+ "usage": {
65
+ "prompt_tokens": 20,
66
+ "completion_tokens": 15,
67
+ "total_tokens": 35
68
+ }
69
+ }
70
+
71
+
72
+ class TestMaritacaClient:
73
+ """Test cases for MaritacaClient."""
74
+
75
+ @pytest.mark.asyncio
76
+ async def test_client_initialization(self, mock_api_key):
77
+ """Test client initialization with various configurations."""
78
+ # Default initialization
79
+ client = MaritacaClient(api_key=mock_api_key)
80
+ assert client.api_key == mock_api_key
81
+ assert client.default_model == MaritacaModel.SABIA_3
82
+ assert client.timeout == 60
83
+ assert client.max_retries == 3
84
+
85
+ # Custom initialization
86
+ custom_client = MaritacaClient(
87
+ api_key=mock_api_key,
88
+ model=MaritacaModel.SABIA_3_LARGE,
89
+ timeout=30,
90
+ max_retries=5
91
+ )
92
+ assert custom_client.default_model == MaritacaModel.SABIA_3_LARGE
93
+ assert custom_client.timeout == 30
94
+ assert custom_client.max_retries == 5
95
+
96
+ await client.close()
97
+ await custom_client.close()
98
+
99
+ @pytest.mark.asyncio
100
+ async def test_chat_completion_success(self, maritaca_client, sample_messages, mock_response_data):
101
+ """Test successful chat completion."""
102
+ with patch.object(maritaca_client.client, 'post') as mock_post:
103
+ mock_response = MagicMock()
104
+ mock_response.status_code = 200
105
+ mock_response.json.return_value = mock_response_data
106
+ mock_post.return_value = mock_response
107
+
108
+ response = await maritaca_client.chat_completion(
109
+ messages=sample_messages,
110
+ temperature=0.7,
111
+ max_tokens=100
112
+ )
113
+
114
+ assert isinstance(response, MaritacaResponse)
115
+ assert response.content == "Olá! Estou bem, obrigado por perguntar. Como posso ajudá-lo hoje?"
116
+ assert response.model == "sabia-3"
117
+ assert response.usage["total_tokens"] == 35
118
+ assert response.finish_reason == "stop"
119
+
120
+ # Verify API call
121
+ mock_post.assert_called_once()
122
+ call_args = mock_post.call_args
123
+ assert call_args[0][0] == "https://test.maritaca.ai/api/chat/completions"
124
+ assert "Authorization" in call_args[1]["headers"]
125
+
126
+ @pytest.mark.asyncio
127
+ async def test_chat_completion_rate_limit(self, maritaca_client, sample_messages):
128
+ """Test rate limit handling."""
129
+ with patch.object(maritaca_client.client, 'post') as mock_post:
130
+ mock_response = MagicMock()
131
+ mock_response.status_code = 429
132
+ mock_response.headers = {"Retry-After": "60"}
133
+ mock_post.return_value = mock_response
134
+
135
+ with pytest.raises(LLMRateLimitError) as exc_info:
136
+ await maritaca_client.chat_completion(messages=sample_messages)
137
+
138
+ assert "rate limit exceeded" in str(exc_info.value).lower()
139
+ assert exc_info.value.details["provider"] == "maritaca"
140
+
141
+ @pytest.mark.asyncio
142
+ async def test_chat_completion_error_handling(self, maritaca_client, sample_messages):
143
+ """Test error handling for API failures."""
144
+ with patch.object(maritaca_client.client, 'post') as mock_post:
145
+ mock_response = MagicMock()
146
+ mock_response.status_code = 500
147
+ mock_response.json.return_value = {
148
+ "error": {"message": "Internal server error"}
149
+ }
150
+ mock_post.return_value = mock_response
151
+
152
+ with pytest.raises(LLMError) as exc_info:
153
+ await maritaca_client.chat_completion(messages=sample_messages)
154
+
155
+ assert "Internal server error" in str(exc_info.value)
156
+
157
+ @pytest.mark.asyncio
158
+ async def test_streaming_completion(self, maritaca_client, sample_messages):
159
+ """Test streaming chat completion."""
160
+ async def mock_aiter_lines():
161
+ yield "data: {\"choices\": [{\"delta\": {\"content\": \"Olá\"}}]}"
162
+ yield "data: {\"choices\": [{\"delta\": {\"content\": \"! \"}}]}"
163
+ yield "data: {\"choices\": [{\"delta\": {\"content\": \"Como\"}}]}"
164
+ yield "data: {\"choices\": [{\"delta\": {\"content\": \" posso\"}}]}"
165
+ yield "data: {\"choices\": [{\"delta\": {\"content\": \" ajudar?\"}}]}"
166
+ yield "data: [DONE]"
167
+
168
+ with patch.object(maritaca_client.client, 'stream') as mock_stream:
169
+ mock_response = AsyncMock()
170
+ mock_response.status_code = 200
171
+ mock_response.aiter_lines = mock_aiter_lines
172
+ mock_stream.return_value.__aenter__.return_value = mock_response
173
+
174
+ chunks = []
175
+ async for chunk in await maritaca_client.chat_completion(
176
+ messages=sample_messages,
177
+ stream=True
178
+ ):
179
+ chunks.append(chunk)
180
+
181
+ assert len(chunks) == 5
182
+ assert "".join(chunks) == "Olá! Como posso ajudar?"
183
+
184
+ @pytest.mark.asyncio
185
+ async def test_circuit_breaker(self, maritaca_client, sample_messages):
186
+ """Test circuit breaker functionality."""
187
+ # Force multiple failures to trigger circuit breaker
188
+ with patch.object(maritaca_client.client, 'post') as mock_post:
189
+ mock_post.side_effect = Exception("Connection failed")
190
+
191
+ for i in range(maritaca_client._circuit_breaker_threshold):
192
+ with pytest.raises(LLMError):
193
+ await maritaca_client.chat_completion(messages=sample_messages)
194
+
195
+ # Circuit should now be open
196
+ assert maritaca_client._check_circuit_breaker() is True
197
+
198
+ # Next request should fail immediately
199
+ with pytest.raises(LLMError) as exc_info:
200
+ await maritaca_client.chat_completion(messages=sample_messages)
201
+
202
+ assert "Circuit breaker is open" in str(exc_info.value)
203
+
204
+ @pytest.mark.asyncio
205
+ async def test_health_check(self, maritaca_client):
206
+ """Test health check functionality."""
207
+ with patch.object(maritaca_client, 'chat_completion') as mock_completion:
208
+ mock_completion.return_value = MaritacaResponse(
209
+ content="Olá",
210
+ model="sabia-3",
211
+ usage={"total_tokens": 10},
212
+ metadata={},
213
+ response_time=0.5,
214
+ timestamp=datetime.utcnow()
215
+ )
216
+
217
+ health = await maritaca_client.health_check()
218
+
219
+ assert health["status"] == "healthy"
220
+ assert health["provider"] == "maritaca"
221
+ assert health["model"] == maritaca_client.default_model
222
+ assert health["circuit_breaker"] == "closed"
223
+
224
+ @pytest.mark.asyncio
225
+ async def test_context_manager(self, mock_api_key):
226
+ """Test async context manager functionality."""
227
+ async with MaritacaClient(api_key=mock_api_key) as client:
228
+ assert client.api_key == mock_api_key
229
+ assert client.client is not None
230
+
231
+ # Client should be closed after context
232
+ with pytest.raises(RuntimeError):
233
+ await client.client.get("https://example.com")
234
+
235
+ def test_factory_function(self, mock_api_key):
236
+ """Test factory function for client creation."""
237
+ client = create_maritaca_client(
238
+ api_key=mock_api_key,
239
+ model=MaritacaModel.SABIA_3_MEDIUM,
240
+ timeout=45
241
+ )
242
+
243
+ assert isinstance(client, MaritacaClient)
244
+ assert client.api_key == mock_api_key
245
+ assert client.default_model == MaritacaModel.SABIA_3_MEDIUM
246
+ assert client.timeout == 45
247
+
248
+
249
+ class TestMaritacaRequest:
250
+ """Test cases for MaritacaRequest model."""
251
+
252
+ def test_request_validation(self):
253
+ """Test request model validation."""
254
+ # Valid request
255
+ request = MaritacaRequest(
256
+ messages=[
257
+ MaritacaMessage(role="user", content="Hello")
258
+ ],
259
+ temperature=0.8,
260
+ max_tokens=1000
261
+ )
262
+ assert request.temperature == 0.8
263
+ assert request.max_tokens == 1000
264
+
265
+ # Test temperature bounds
266
+ with pytest.raises(ValueError):
267
+ MaritacaRequest(
268
+ messages=[],
269
+ temperature=2.5 # Too high
270
+ )
271
+
272
+ # Test max_tokens bounds
273
+ with pytest.raises(ValueError):
274
+ MaritacaRequest(
275
+ messages=[],
276
+ max_tokens=10000 # Too high
277
+ )
278
+
279
+
280
+ if __name__ == "__main__":
281
+ pytest.main([__file__, "-v"])