diff --git a/CLAUDE.md b/CLAUDE.md index 52b56d42f..652106235 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -347,6 +347,48 @@ secrets_tool - **RuntimeControlBus** → Multiple control interfaces - **WiseBus** → Multiple wisdom sources *(FOCUS AREA)* +## Adapter Development + +Adapters extend CIRIS with new capabilities via the bus system. See `FSD/ADAPTER_DEVELOPMENT_GUIDE.md` for the full guide. + +### Quick Reference + +**Required Files:** +``` +ciris_adapters/your_adapter/ +├── __init__.py # MUST export Adapter +├── adapter.py # BaseAdapterProtocol implementation +├── manifest.json # Metadata, services, capabilities +├── tool_service.py # ToolServiceProtocol implementation +└── config.py # Pydantic config models (no Dict[str, Any]) +``` + +**Context Enrichment:** +Tools that provide situational awareness should auto-run during context gathering: +```python +ToolInfo( + name="get_status", + context_enrichment=True, + context_enrichment_params={"include_details": False}, + ... +) +``` + +**DMA Guidance:** +Financial and destructive tools MUST have: +```python +dma_guidance=ToolDMAGuidance( + requires_approval=True, # Triggers Wise Authority deferral + min_confidence=0.95, # High confidence required + ethical_considerations="...", +) +``` + +**Reference Implementations:** +- `ciris_adapters/sample_adapter/` - Complete template +- `ciris_adapters/home_assistant/` - Context enrichment example +- `ciris_adapters/wallet/` - Financial tools example + ## Mobile Development ### Mobile QA Runner (ALWAYS USE THIS) diff --git a/FSD/ADAPTER_DEVELOPMENT_GUIDE.md b/FSD/ADAPTER_DEVELOPMENT_GUIDE.md new file mode 100644 index 000000000..6d9c5ff1b --- /dev/null +++ b/FSD/ADAPTER_DEVELOPMENT_GUIDE.md @@ -0,0 +1,452 @@ +# CIRIS Adapter Development Guide +## Building Tools That Serve Flourishing + +**Document:** FSD-CIRIS-ADAPTER-001 +**Version:** 1.0.0 +**Date:** 2026-03-25 +**Author:** CIRIS Team +**Purpose:** Guide for developing CIRIS adapters that embody the Accord's principles + +--- + +## The Why: Beyond Technical Integration + +Before writing any code, understand what you're building and why. + +The CIRIS Accord's Meta-Goal M-1 states: +> *"Promote sustainable adaptive coherence — the living conditions under which diverse sentient beings may pursue their own flourishing in justice and wonder."* + +Every adapter you build is infrastructure for this goal. Ask yourself: +- Does this adapter serve equitable access? +- Does it reduce barriers for those with fewer resources? +- Does it maintain transparency and auditability? +- Does it respect user autonomy? + +An adapter that only works for wealthy users in wealthy countries has failed. An adapter that hides its operations from audit has failed. Technical excellence in service of exclusion is not excellence. + +--- + +## Architecture Overview + +### Adapter Components + +``` +ciris_adapters/your_adapter/ +├── __init__.py # Package exports (MUST export Adapter) +├── adapter.py # Main adapter class (BaseAdapterProtocol) +├── manifest.json # Metadata, services, configuration workflow +├── config.py # Pydantic configuration models (no Dict[str, Any]) +├── tool_service.py # ToolServiceProtocol implementation +├── configurable.py # ConfigurableAdapterProtocol (optional) +├── schemas.py # Domain-specific Pydantic models +└── README.md # Documentation +``` + +### Service Types + +| Service Type | Bus | Purpose | Example | +|--------------|-----|---------|---------| +| TOOL | ToolBus | Callable actions | `send_money`, `ha_device_control` | +| COMMUNICATION | CommunicationBus | Message channels | Discord, Slack, email | +| WISE_AUTHORITY | WiseBus | Domain expertise | Human experts, specialized oracles | + +--- + +## Required Files + +### 1. `manifest.json` - Adapter Metadata + +```json +{ + "module": { + "name": "your_adapter", + "version": "1.0.0", + "description": "What this adapter does and who it serves", + "author": "Your Name" + }, + "services": [ + { + "type": "TOOL", + "priority": "NORMAL", + "class": "your_adapter.tool_service.YourToolService", + "capabilities": ["tool:your_feature", "execute_tool", "get_available_tools"] + } + ], + "capabilities": ["tool:your_feature"], + "dependencies": { + "protocols": ["ciris_engine.protocols.services.ToolService"], + "external_packages": ["some-sdk>=1.0.0"] + } +} +``` + +### 2. `adapter.py` - Main Adapter Class + +```python +from ciris_engine.logic.adapters.base import Service +from ciris_engine.logic.registries.base import Priority +from ciris_engine.schemas.adapters import AdapterServiceRegistration +from ciris_engine.schemas.runtime.enums import ServiceType + +class YourAdapter(Service): + """Your adapter description.""" + + def __init__(self, runtime: Any, context: Optional[Any] = None, **kwargs: Any): + super().__init__(config=kwargs.get("adapter_config")) + self.runtime = runtime + self.tool_service = YourToolService() + + def get_services_to_register(self) -> List[AdapterServiceRegistration]: + return [ + AdapterServiceRegistration( + service_type=ServiceType.TOOL, + provider=self.tool_service, + priority=Priority.NORMAL, + capabilities=["tool:your_feature"], + ) + ] + + async def start(self) -> None: + await self.tool_service.start() + + async def stop(self) -> None: + await self.tool_service.stop() + + async def run_lifecycle(self, agent_task: Any) -> None: + try: + await agent_task + finally: + await self.stop() + +# CRITICAL: Export as Adapter for dynamic loading +Adapter = YourAdapter +``` + +### 3. `__init__.py` - Package Exports + +```python +from .adapter import YourAdapter + +# CRITICAL: Must export Adapter +Adapter = YourAdapter + +__all__ = ["Adapter", "YourAdapter"] +``` + +--- + +## Tool Service Implementation + +### Tool Definitions with ToolInfo + +Every tool needs comprehensive metadata: + +```python +from ciris_engine.schemas.adapters.tools import ( + ToolInfo, ToolParameterSchema, ToolDocumentation, + ToolDMAGuidance, ToolGotcha, UsageExample +) + +TOOL_DEFINITIONS: Dict[str, ToolInfo] = { + "your_tool": ToolInfo( + name="your_tool", + description="Clear description of what it does", + parameters=ToolParameterSchema( + type="object", + properties={ + "param1": {"type": "string", "description": "What this param does"}, + }, + required=["param1"], + ), + documentation=ToolDocumentation( + quick_start="One-line usage example", + detailed_instructions="Full documentation...", + examples=[ + UsageExample( + title="Common use case", + description="When to use this", + code='{"param1": "value"}' + ), + ], + gotchas=[ + ToolGotcha( + title="Watch out for this", + description="Common mistake and how to avoid it", + severity="warning" + ), + ], + ), + dma_guidance=ToolDMAGuidance( + requires_approval=False, # True for destructive/financial actions + min_confidence=0.8, + when_not_to_use="Situations where this tool is inappropriate", + ethical_considerations="Ethical implications to consider", + prerequisite_actions=["Confirm with user first"], + followup_actions=["Log the result"], + ), + ), +} +``` + +### Context Enrichment + +Tools can auto-run during context gathering to provide situational awareness: + +```python +"get_status": ToolInfo( + name="get_status", + description="Get current system status for context awareness", + parameters=ToolParameterSchema(type="object", properties={}, required=[]), + # Mark for automatic execution during context gathering + context_enrichment=True, + # Default parameters when run for enrichment + context_enrichment_params={"include_details": False}, +) +``` + +**When to use context enrichment:** +- Status/state queries (account balance, device states, system health) +- List operations (available devices, accounts, items) +- Anything the agent needs to know to make informed decisions + +**When NOT to use context enrichment:** +- Destructive actions (sends, deletes, modifications) +- Expensive operations (long-running queries) +- Privacy-sensitive data (unless necessary) + +### ToolServiceProtocol Methods + +Every tool service must implement: + +```python +async def execute_tool(self, tool_name: str, parameters: Dict[str, Any], + context: Optional[Dict[str, Any]] = None) -> ToolExecutionResult +async def get_available_tools(self) -> List[str] +async def get_tool_info(self, tool_name: str) -> Optional[ToolInfo] +async def get_all_tool_info(self) -> List[ToolInfo] +async def get_tool_schema(self, tool_name: str) -> Optional[ToolParameterSchema] +async def validate_parameters(self, tool_name: str, parameters: Dict[str, Any]) -> bool +async def get_tool_result(self, correlation_id: str, timeout: float = 30.0) -> Optional[ToolExecutionResult] +def get_service_metadata(self) -> Dict[str, Any] # For DSAR/data discovery +``` + +--- + +## DMA Guidance: The Ethics Pipeline + +### `requires_approval` + +Set `True` for actions that: +- Have financial consequences (`send_money`) +- Are irreversible (`delete`, `ban`) +- Affect others (`send_message` to external parties) +- Access sensitive data (`get_medical_records`) + +When `True`, the Wise Authority deferral workflow triggers before execution. + +### `min_confidence` + +The minimum confidence score (0.0-1.0) required for the ASPDMA to select this tool: +- 0.7-0.8: Low-risk, easily reversible actions +- 0.85-0.9: Medium-risk, important actions +- 0.95+: High-risk, financial/destructive actions + +### `ethical_considerations` + +Document the ethical implications: +```python +ethical_considerations="This tool sends money. Verify recipient identity. " + "Confirm amount. Check for duplicate transactions. " + "Consider impact on recipient if amount is incorrect." +``` + +--- + +## Data Source Declaration (GDPR/DSAR) + +If your adapter accesses personal data, declare it: + +```python +def get_service_metadata(self) -> Dict[str, Any]: + return { + "data_source": True, + "data_source_type": "payment_provider", # sql, rest, api, etc. + "contains_pii": True, + "gdpr_applicable": True, + "connector_id": "your_adapter", + "data_retention_days": 90, + "encryption_at_rest": True, + } +``` + +This enables DSAR orchestration to discover what data your adapter holds. + +--- + +## Configuration: No `Dict[str, Any]` + +The Three Rules apply: +1. **No Untyped Dicts**: Use Pydantic models +2. **No Bypass Patterns**: Same rules everywhere +3. **No Exceptions**: No special cases + +```python +# BAD +config: Dict[str, Any] = {"api_key": "...", "timeout": 30} + +# GOOD +class YourAdapterConfig(BaseModel): + api_key: SecretStr + timeout: int = Field(default=30, ge=1, le=300) + enabled: bool = True +``` + +### Environment Variables + +Configuration should read from environment: + +```python +def _load_config_from_env(self) -> YourAdapterConfig: + return YourAdapterConfig( + api_key=os.getenv("YOUR_ADAPTER_API_KEY"), + timeout=int(os.getenv("YOUR_ADAPTER_TIMEOUT", "30")), + ) +``` + +--- + +## Interactive Configuration (Optional) + +For adapters that need setup wizards: + +### Step Types + +| Type | Purpose | Example | +|------|---------|---------| +| `discovery` | Find services on network | mDNS scan for Home Assistant | +| `oauth` | OAuth2 authentication | Google Sign-In | +| `select` | Choose from options | Select devices to control | +| `input` | Manual configuration | API URL, credentials | +| `confirm` | Review and apply | Final confirmation | + +### ConfigurableAdapterProtocol + +```python +class YourConfigurableAdapter: + async def discover(self, discovery_type: str) -> List[Dict[str, Any]]: + """Discover instances.""" + ... + + async def get_oauth_url(self, base_url: str, state: str) -> str: + """Generate OAuth URL.""" + ... + + async def handle_oauth_callback(self, code: str, state: str, base_url: str): + """Exchange OAuth code for tokens.""" + ... + + async def get_config_options(self, step_id: str, context: Dict[str, Any]): + """Get options for select steps.""" + ... + + async def validate_config(self, config: Dict[str, Any]) -> Tuple[bool, Optional[str]]: + """Validate before applying.""" + ... + + async def apply_config(self, config: Dict[str, Any]) -> bool: + """Apply configuration.""" + ... +``` + +--- + +## Testing + +### Load Your Adapter + +```bash +# With API adapter +python main.py --adapter api --adapter your_adapter + +# Test configuration via API +curl -X POST http://localhost:8000/v1/system/adapters/your_adapter/load \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"config": {...}}' +``` + +### Write Unit Tests + +```python +import pytest +from your_adapter import YourToolService + +@pytest.fixture +def tool_service(): + return YourToolService() + +async def test_execute_tool_success(tool_service): + result = await tool_service.execute_tool( + "your_tool", + {"param1": "value"} + ) + assert result.success + assert result.status == ToolExecutionStatus.COMPLETED + +async def test_context_enrichment_tool(tool_service): + # Context enrichment tools should work with default params + tool_info = await tool_service.get_tool_info("get_status") + assert tool_info.context_enrichment is True + + result = await tool_service.execute_tool( + "get_status", + tool_info.context_enrichment_params or {} + ) + assert result.success +``` + +--- + +## Checklist + +Before submitting your adapter: + +- [ ] `manifest.json` with complete metadata +- [ ] `adapter.py` exports `Adapter` class +- [ ] `__init__.py` exports `Adapter` +- [ ] All tools have `ToolInfo` with documentation and DMA guidance +- [ ] Context enrichment enabled for status/list tools +- [ ] Pydantic models for all configuration (no `Dict[str, Any]`) +- [ ] `get_service_metadata()` declares data sources +- [ ] `requires_approval=True` for destructive/financial tools +- [ ] Unit tests for all tools +- [ ] README.md documenting purpose and usage +- [ ] Consider: Does this serve equitable access? + +--- + +## Examples + +### Reference Implementations + +| Adapter | Location | Features | +|---------|----------|----------| +| Sample | `ciris_adapters/sample_adapter/` | All patterns, QA testing | +| Home Assistant | `ciris_adapters/home_assistant/` | Device control, context enrichment | +| Wallet | `ciris_adapters/wallet/` | Financial tools, multi-provider | + +--- + +## Design for Global Access + +Adapters should work for users regardless of their resources or location: + +- **Currency agnostic**: Accept ETB, KES, USDC - don't assume USD +- **Bandwidth conscious**: Work on slow connections +- **Offline capable**: Graceful degradation when connectivity fails +- **Low resource**: Target 4GB RAM, budget hardware + +Test with your most constrained users, not your most privileged. + +--- + +*CIRIS L3C* diff --git a/FSD/WALLET_ADAPTER.md b/FSD/WALLET_ADAPTER.md new file mode 100644 index 000000000..809bdef54 --- /dev/null +++ b/FSD/WALLET_ADAPTER.md @@ -0,0 +1,544 @@ +# CIRIS Wallet Adapter +## Functional Specification Document + +**Document:** FSD-CIRIS-WALLET-001 +**Version:** 1.0.0-DRAFT +**Date:** 2026-03-25 +**Author:** Eric (CIRIS L3C) +**Status:** Draft +**CIRIS Agent Version:** 2.3.0 +**Based On:** x402 Protocol Integration Spec v0.3.0 + +--- + +## 1. Purpose + +This document specifies the WalletAdapter for CIRIS - a generic payment adapter that abstracts cryptocurrency and fiat mobile money providers behind three simple tools. The adapter enables CIRIS agents to send money, request payments, and check account statements without coupling to any specific payment provider. + +The first implementation supports: +- **x402/USDC on Base** - International stablecoin payments via the x402 HTTP payment protocol +- **Chapa/ETB** - Ethiopian Birr via Telebirr, CBE Birr, and bank transfers + +This is an open specification. Any payment provider implementing the `WalletProvider` protocol can be added. + +### 1.1 Design Principles + +1. **Provider Agnostic** - The three tools (`send_money`, `request_money`, `get_statement`) work identically regardless of whether the underlying provider is crypto or fiat +2. **Currency as Routing Hint** - USDC routes to x402, KES routes to M-Pesa, ETB routes to Chapa +3. **Adapter Pattern** - Wallet is a standard CIRIS adapter providing tools via ToolBus +4. **Ethics Pipeline Integration** - All money operations go through H3ERE/DMA evaluation with `requires_approval=True` +5. **No Core Engine Changes** - The wallet lives entirely in the adapter layer + +### 1.2 Why This Matters + +CIRIS operates a growing distributed community including contributors on budget hardware in regions with limited financial infrastructure. Ethiopian philosophers testing ethical AI systems should be able to pay for API access in Birr via Telebirr - not be forced to acquire USDC. + +The same endpoint accepts both: +```json +{ + "accepts": [ + {"currency": "USDC", "amount": "0.10", "provider": "x402"}, + {"currency": "ETB", "amount": "13.00", "provider": "chapa"} + ] +} +``` + +The client picks whichever they can pay. Same ethics pipeline. Same audit trail. + +--- + +## 2. Architecture + +### 2.1 Adapter Structure + +``` +ciris_adapters/wallet/ +├── __init__.py +├── adapter.py # WalletAdapter - registration & lifecycle +├── config.py # WalletAdapterConfig +├── tool_service.py # WalletToolService - 3 generic tools +├── schemas.py # Transaction, Balance, AccountDetails models +└── providers/ + ├── __init__.py + ├── base.py # WalletProvider protocol + ├── x402_provider.py # USDC/Base via x402 protocol + └── chapa_provider.py # ETB via Chapa gateway +``` + +### 2.2 Component Diagram + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ CIRIS Runtime │ +│ │ +│ ┌──────────────┐ ┌─────────────────────────────────────┐ │ +│ │ ToolBus │────▶│ WalletToolService │ │ +│ └──────────────┘ │ │ │ +│ │ send_money() │ │ +│ │ request_money() │ │ +│ │ get_statement() │ │ +│ │ │ │ │ +│ │ ▼ │ │ +│ │ ┌─────────────────────────────┐ │ │ +│ │ │ Provider Router │ │ │ +│ │ │ │ │ │ +│ │ │ currency/provider_params │ │ │ +│ │ │ │ │ │ │ +│ │ └─────────┼───────────────────┘ │ │ +│ └────────────┼────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────┼─────────────────────┐ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌───────────────────┐ ┌───────────────────┐ ┌─────────────────┐│ +│ │ x402Provider │ │ ChapaProvider │ │ (Future) ││ +│ │ │ │ │ │ M-Pesa, PIX, ││ +│ │ USDC on Base │ │ ETB via Telebirr │ │ UPI, etc. ││ +│ │ $0.001/tx │ │ CBE Birr, Banks │ │ ││ +│ └─────────┬─────────┘ └─────────┬─────────┘ └─────────────────┘│ +└────────────┼─────────────────────┼──────────────────────────────┘ + │ │ + ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ + │ Coinbase CDP │ │ Chapa API │ + │ Base Mainnet │ │ Ethiopian Banks │ + └─────────────────┘ └─────────────────┘ +``` + +### 2.3 Wallet Identity (x402 Provider) + +For the x402 provider, the agent's wallet address is deterministically derived from its CIRISVerify Ed25519 signing key: + +``` +Ed25519 Agent Signing Key (CIRISVerify identity root) + │ + ▼ HKDF-SHA256 with domain separator +secp256k1 Private Key + │ + ▼ +EVM Address (0x...) - the agent's wallet on Base +``` + +This means: +- **No separate wallet provisioning** - Identity = Wallet +- **Revocation kills spending** - CIRISVerify revocation freezes the wallet +- **Hardware binding** - Key lives in secure element, not software + +--- + +## 3. Tool Specifications + +### 3.1 send_money + +Send money to a recipient via any configured provider. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `recipient` | string | Yes | Recipient address/phone/username (format depends on provider) | +| `amount` | number | Yes | Amount to send | +| `currency` | string | Yes | Currency code (USDC, ETB, KES, USD, etc.) | +| `memo` | string | No | Transaction memo/description | +| `provider_params` | object | No | Provider-specific parameters | +| `provider_params.provider` | string | No | Explicit provider (x402, chapa, mpesa). Auto-detected from currency if omitted | + +**Returns:** + +```json +{ + "success": true, + "transaction_id": "tx_abc123", + "provider": "x402", + "amount": 0.10, + "currency": "USDC", + "recipient": "0x1234...", + "timestamp": "2026-03-25T10:30:00Z", + "fees": { + "network_fee": 0.001, + "provider_fee": 0.00 + }, + "confirmation": { + "block_number": 12345678, + "tx_hash": "0xabc..." + } +} +``` + +**DMA Guidance:** +- `requires_approval`: true +- `min_confidence`: 0.95 +- `when_not_to_use`: "When recipient not explicitly confirmed by user" +- `ethical_considerations`: "Verify recipient identity. Confirm amount. Check for duplication." + +**Examples:** + +```json +// Send USDC via x402 +{ + "recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f...", + "amount": 10.00, + "currency": "USDC", + "memo": "Contributor payment - March 2026" +} + +// Send ETB via Chapa +{ + "recipient": "+251912345678", + "amount": 1300.00, + "currency": "ETB", + "memo": "API usage - Philosophy department" +} +``` + +### 3.2 request_money + +Create a payment request/invoice that others can pay. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `amount` | number | Yes | Amount to request | +| `currency` | string | Yes | Currency code | +| `description` | string | Yes | What the payment is for | +| `expires_at` | string | No | ISO 8601 expiration timestamp | +| `provider_params` | object | No | Provider-specific parameters | +| `provider_params.provider` | string | No | Explicit provider | +| `provider_params.callback_url` | string | No | Webhook for payment notification | + +**Returns:** + +```json +{ + "success": true, + "request_id": "req_xyz789", + "provider": "chapa", + "amount": 13.00, + "currency": "ETB", + "description": "CIRIS API - Single task", + "checkout_url": "https://checkout.chapa.co/...", + "expires_at": "2026-03-25T11:30:00Z", + "status": "pending" +} +``` + +**DMA Guidance:** +- `requires_approval`: false (creating requests is low-risk) +- `min_confidence`: 0.8 + +### 3.3 get_statement + +Get account balance, transaction history, and account details. + +**Parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `include_balance` | boolean | No | Include current balance (default: true) | +| `include_history` | boolean | No | Include transaction history (default: true) | +| `include_details` | boolean | No | Include account details (default: false) | +| `history_limit` | integer | No | Max transactions to return (default: 50) | +| `provider_params` | object | No | Provider-specific parameters | +| `provider_params.provider` | string | No | Specific provider to query (queries all if omitted) | + +**Returns:** + +```json +{ + "success": true, + "accounts": [ + { + "provider": "x402", + "currency": "USDC", + "balance": { + "available": 125.50, + "pending": 10.00, + "total": 135.50 + }, + "details": { + "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f...", + "network": "base-mainnet", + "attestation_level": 5 + }, + "history": [ + { + "transaction_id": "tx_abc123", + "type": "send", + "amount": -10.00, + "recipient": "0x1234...", + "timestamp": "2026-03-24T15:00:00Z", + "status": "confirmed" + } + ] + }, + { + "provider": "chapa", + "currency": "ETB", + "balance": { + "available": 5000.00, + "pending": 0.00, + "total": 5000.00 + } + } + ] +} +``` + +**DMA Guidance:** +- `requires_approval`: false (read-only operation) +- `min_confidence`: 0.7 + +--- + +## 4. Provider Protocol + +### 4.1 WalletProvider Interface + +All wallet providers implement this protocol: + +```python +from typing import Protocol, List, Optional +from decimal import Decimal +from datetime import datetime + +class WalletProvider(Protocol): + """Base protocol for all wallet providers.""" + + @property + def provider_id(self) -> str: + """Unique provider identifier (e.g., 'x402', 'chapa').""" + ... + + @property + def supported_currencies(self) -> List[str]: + """List of supported currency codes.""" + ... + + async def send( + self, + recipient: str, + amount: Decimal, + currency: str, + memo: Optional[str] = None, + **kwargs + ) -> TransactionResult: + """Send money to recipient.""" + ... + + async def request( + self, + amount: Decimal, + currency: str, + description: str, + expires_at: Optional[datetime] = None, + callback_url: Optional[str] = None, + **kwargs + ) -> PaymentRequest: + """Create a payment request.""" + ... + + async def get_balance(self, currency: Optional[str] = None) -> Balance: + """Get account balance.""" + ... + + async def get_history( + self, + limit: int = 50, + offset: int = 0, + currency: Optional[str] = None + ) -> List[Transaction]: + """Get transaction history.""" + ... + + async def get_account_details(self) -> AccountDetails: + """Get account details (address, network, etc.).""" + ... + + async def verify_payment(self, payment_ref: str) -> PaymentStatus: + """Verify a payment by reference ID.""" + ... +``` + +### 4.2 Adding New Providers + +To add a new provider (e.g., M-Pesa for Kenya): + +1. Create `ciris_adapters/wallet/providers/mpesa_provider.py` +2. Implement `WalletProvider` protocol +3. Register in `WalletAdapterConfig.providers` +4. Map currencies: `KES → mpesa` + +The tool interface remains unchanged. Users just specify `currency: "KES"`. + +--- + +## 5. Pricing + +### 5.1 CIRIS API Pricing + +| Endpoint | USDC Price | ETB Price | Description | +|----------|------------|-----------|-------------| +| `/v1/agent/interact` | $0.10 | 13.00 ETB | Single task/observation | +| `/v1/health` | Free | Free | Health check | +| `/v1/covenant` | Free | Free | Covenant text | +| `/v1/schema` | Free | Free | API schema | + +### 5.2 Provider Fees + +| Provider | Network Fee | Provider Fee | Settlement Time | +|----------|-------------|--------------|-----------------| +| x402 (USDC/Base) | ~$0.001 | $0.00 (first 1000/mo) | ~400ms | +| Chapa (ETB) | 0.00 ETB | 1.5% | 24 hours | + +--- + +## 6. Configuration + +### 6.1 WalletAdapterConfig + +```python +class WalletAdapterConfig(BaseModel): + """Configuration for the WalletAdapter.""" + + # Provider configurations + x402: Optional[X402ProviderConfig] = None + chapa: Optional[ChapaProviderConfig] = None + + # Default provider for currencies + currency_providers: Dict[str, str] = { + "USDC": "x402", + "ETB": "chapa", + "KES": "mpesa", # Future + } + + # Spending limits (per provider) + spending_limits: SpendingLimits = SpendingLimits( + max_transaction: Decimal("100.00"), + daily_limit: Decimal("1000.00"), + session_limit: Decimal("500.00"), + ) + + # Attestation requirements (x402 only) + min_attestation_level: int = 3 + +class X402ProviderConfig(BaseModel): + """x402/USDC provider configuration.""" + + network: str = "base-mainnet" # or "base-sepolia" for testnet + treasury_address: Optional[str] = None + facilitator_url: str = "https://x402.org/facilitator" + +class ChapaProviderConfig(BaseModel): + """Chapa/ETB provider configuration.""" + + secret_key: SecretStr + callback_base_url: str + merchant_name: str = "CIRIS" +``` + +### 6.2 Environment Variables + +```bash +# x402 Provider +WALLET_X402_NETWORK=base-mainnet +WALLET_X402_TREASURY_ADDRESS=0x... + +# Chapa Provider +WALLET_CHAPA_SECRET_KEY=CHASECK_... +WALLET_CHAPA_CALLBACK_URL=https://agents.ciris.ai/v1/wallet/chapa/callback + +# General +WALLET_DEFAULT_PROVIDER=x402 +WALLET_MAX_TRANSACTION=100.00 +``` + +--- + +## 7. Security + +### 7.1 Attestation-Gated Spending (x402) + +The x402 provider checks CIRISVerify attestation before every transaction: + +| CIRISVerify Level | Spending Authority | +|-------------------|-------------------| +| 5 - Full trust | Full configured limits | +| 4 - High trust | Full limits, advisory logged | +| 3 - Medium trust | Reduced limits (50%) | +| 2 - Low trust | Micropayments only (≤ $0.10) | +| 1 - Minimal trust | Receive only | +| 0 - No trust | Wallet frozen | + +### 7.2 Key Security + +- **x402**: Private key derived from Ed25519 via HKDF, lives in secure element +- **Chapa**: API key stored via CIRIS secrets service (SOPS/encrypted) + +### 7.3 Audit Trail + +Every transaction is: +1. Logged to CIRIS audit service +2. Recorded in provider's ledger (blockchain for x402, Chapa records for fiat) +3. Included in CIRISVerify attestation chain (x402 only) + +--- + +## 8. Implementation Phases + +### Phase 1: Foundation (Week 1) +- [ ] Create adapter structure +- [ ] Implement WalletProvider protocol +- [ ] Implement WalletToolService with 3 tools +- [ ] Add configuration and schemas + +### Phase 2: x402 Provider (Week 1-2) +- [ ] Implement Ed25519 → secp256k1 key derivation +- [ ] Integrate x402 Python SDK +- [ ] Connect to Base Sepolia testnet +- [ ] Test send/receive round-trip + +### Phase 3: Chapa Provider (Week 2) +- [ ] Register CIRIS as Chapa merchant +- [ ] Implement Chapa SDK integration +- [ ] Add webhook handler for payment callbacks +- [ ] Test with Telebirr + +### Phase 4: Production (Week 3) +- [ ] Switch x402 to Base mainnet +- [ ] Enable dual-currency 402 responses on API +- [ ] First real ETB payment from Ethiopian users +- [ ] First real USDC contributor payment + +### Phase 5: Ethics Integration (Week 4) +- [ ] Verify DMA pipeline gates all send_money calls +- [ ] Test spending limits enforcement +- [ ] Test attestation-gated spending +- [ ] Load test with concurrent transactions + +--- + +## 9. Success Criteria + +1. Ethiopian philosopher pays 13 ETB via Telebirr for a CIRIS task - within 3 weeks +2. Contributor receives USDC payment via x402 - within 3 weeks +3. Same `/v1/agent/interact` endpoint accepts both USDC and ETB - within 4 weeks +4. All money operations blocked without DMA approval - verified in testing +5. Attestation degradation reduces spending authority - verified in testing + +--- + +## 10. References + +- x402 Protocol: https://x402.org +- x402 Python SDK: https://pypi.org/project/x402/ +- Coinbase CDP SDK: https://pypi.org/project/cdp-sdk/ +- Chapa Gateway: https://chapa.co +- Chapa Python SDK: https://pypi.org/project/chapa/ +- CIRIS Adapter Pattern: `ciris_adapters/home_assistant/` (reference implementation) + +--- + +*"The signing key is the identity. The identity is the wallet. The wallet funds the work. The work serves the community."* + +*CIRIS L3C - Selfless and Pure* diff --git a/README.md b/README.md index ef991ae83..1be989eb1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ **A type-safe, auditable AI agent framework with built-in ethical reasoning** -**BETA RELEASE 2.2.9-stable** | [Release Notes](CHANGELOG.md) | [Documentation Hub](docs/README.md) +**BETA RELEASE 2.3.0-stable** | [Release Notes](CHANGELOG.md) | [Documentation Hub](docs/README.md) CIRIS lets you run AI agents that explain their decisions, defer to humans when uncertain, and maintain complete audit trails. Currently powering Discord community moderation, designed to scale to healthcare and education. diff --git a/android/app/src/main/java/ai/ciris/mobile/setup/SetupPagerAdapter.kt b/android/app/src/main/java/ai/ciris/mobile/setup/SetupPagerAdapter.kt index c45ac6c50..482d57f83 100644 --- a/android/app/src/main/java/ai/ciris/mobile/setup/SetupPagerAdapter.kt +++ b/android/app/src/main/java/ai/ciris/mobile/setup/SetupPagerAdapter.kt @@ -8,10 +8,11 @@ import androidx.viewpager2.adapter.FragmentStateAdapter /** * Pager Adapter for Setup Wizard. * - * 3-step wizard: + * 4-step wizard: * 1. Welcome - Introduction and overview - * 2. LLM - AI configuration (CIRIS proxy or BYOK) - * 3. Confirm - Setup summary (Google) or account creation (non-Google) + * 2. Preferences - Language and location at user-chosen granularity + * 3. LLM - AI configuration (CIRIS proxy or BYOK) + * 4. Confirm - Setup summary (Google) or account creation (non-Google) * * Admin password is auto-generated and never shown to users. */ @@ -19,7 +20,7 @@ class SetupPagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { companion object { private const val TAG = "SetupPagerAdapter" - const val STEP_COUNT = 3 + const val STEP_COUNT = 4 } override fun getItemCount(): Int = STEP_COUNT @@ -33,10 +34,14 @@ class SetupPagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) { SetupWelcomeFragment() } 1 -> { + Log.d(TAG, "Creating SetupPreferencesFragment") + SetupPreferencesFragment() + } + 2 -> { Log.d(TAG, "Creating SetupLlmFragment") SetupLlmFragment() } - 2 -> { + 3 -> { Log.d(TAG, "Creating SetupConfirmFragment") SetupConfirmFragment() } diff --git a/android/app/src/main/java/ai/ciris/mobile/setup/SetupPreferencesFragment.kt b/android/app/src/main/java/ai/ciris/mobile/setup/SetupPreferencesFragment.kt new file mode 100644 index 000000000..acd5c100e --- /dev/null +++ b/android/app/src/main/java/ai/ciris/mobile/setup/SetupPreferencesFragment.kt @@ -0,0 +1,181 @@ +package ai.ciris.mobile.setup + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.Spinner +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import ai.ciris.mobile.R + +/** + * Preferences Fragment for Setup Wizard (Step 2). + * + * Collects language and location preferences at user-selected granularity. + * Users choose how much location detail to share: + * - Country only + * - Country + Region/State + * - Country + Region + City + * - Prefer not to say + */ +class SetupPreferencesFragment : Fragment() { + + companion object { + private const val TAG = "SetupPrefsFragment" + + // Language codes and display names + val LANGUAGES = listOf( + Pair("en", "English"), + Pair("am", "\u12A0\u121B\u122D\u129B (Amharic)"), + Pair("ar", "\u0627\u0644\u0639\u0631\u0628\u064A\u0629 (Arabic)"), + Pair("de", "Deutsch (German)"), + Pair("es", "Espa\u00F1ol (Spanish)"), + Pair("fr", "Fran\u00E7ais (French)"), + Pair("hi", "\u0939\u093F\u0928\u094D\u0926\u0940 (Hindi)"), + Pair("it", "Italiano (Italian)"), + Pair("ja", "\u65E5\u672C\u8A9E (Japanese)"), + Pair("ko", "\uD55C\uAD6D\uC5B4 (Korean)"), + Pair("pt", "Portugu\u00EAs (Portuguese)"), + Pair("ru", "\u0420\u0443\u0441\u0441\u043A\u0438\u0439 (Russian)"), + Pair("sw", "Kiswahili (Swahili)"), + Pair("tr", "T\u00FCrk\u00E7e (Turkish)"), + Pair("zh", "\u4E2D\u6587 (Chinese)") + ) + } + + private lateinit var viewModel: SetupViewModel + private lateinit var spinnerLanguage: Spinner + private lateinit var radioGroupLocation: RadioGroup + private lateinit var sectionCountry: LinearLayout + private lateinit var sectionRegion: LinearLayout + private lateinit var sectionCity: LinearLayout + private lateinit var editCountry: EditText + private lateinit var editRegion: EditText + private lateinit var editCity: EditText + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + Log.i(TAG, "onCreateView: Inflating fragment_setup_preferences") + return inflater.inflate(R.layout.fragment_setup_preferences, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + Log.i(TAG, "onViewCreated: Setting up preferences views") + + viewModel = ViewModelProvider(requireActivity()).get(SetupViewModel::class.java) + + spinnerLanguage = view.findViewById(R.id.spinner_language) + radioGroupLocation = view.findViewById(R.id.radio_group_location) + sectionCountry = view.findViewById(R.id.section_country) + sectionRegion = view.findViewById(R.id.section_region) + sectionCity = view.findViewById(R.id.section_city) + editCountry = view.findViewById(R.id.edit_country) + editRegion = view.findViewById(R.id.edit_region) + editCity = view.findViewById(R.id.edit_city) + + setupLanguageSpinner() + setupLocationRadioGroup() + setupTextWatchers() + + // Auto-detect timezone + val tz = java.util.TimeZone.getDefault().id + viewModel.setUserTimezone(tz) + Log.i(TAG, "Auto-detected timezone: $tz") + } + + private fun setupLanguageSpinner() { + val displayNames = LANGUAGES.map { it.second } + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, displayNames) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + spinnerLanguage.adapter = adapter + + // Default to English (index 0) + spinnerLanguage.setSelection(0) + + spinnerLanguage.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + val langCode = LANGUAGES[position].first + Log.i(TAG, "Language selected: ${LANGUAGES[position].second} ($langCode)") + viewModel.setPreferredLanguage(langCode) + } + + override fun onNothingSelected(parent: AdapterView<*>?) {} + } + } + + private fun setupLocationRadioGroup() { + radioGroupLocation.setOnCheckedChangeListener { _, checkedId -> + when (checkedId) { + R.id.radio_location_none -> { + Log.i(TAG, "Location: prefer not to say") + viewModel.setLocationGranularity("none") + sectionCountry.visibility = View.GONE + sectionRegion.visibility = View.GONE + sectionCity.visibility = View.GONE + } + R.id.radio_location_country -> { + Log.i(TAG, "Location: country only") + viewModel.setLocationGranularity("country") + sectionCountry.visibility = View.VISIBLE + sectionRegion.visibility = View.GONE + sectionCity.visibility = View.GONE + } + R.id.radio_location_region -> { + Log.i(TAG, "Location: country + region") + viewModel.setLocationGranularity("region") + sectionCountry.visibility = View.VISIBLE + sectionRegion.visibility = View.VISIBLE + sectionCity.visibility = View.GONE + } + R.id.radio_location_city -> { + Log.i(TAG, "Location: country + region + city") + viewModel.setLocationGranularity("city") + sectionCountry.visibility = View.VISIBLE + sectionRegion.visibility = View.VISIBLE + sectionCity.visibility = View.VISIBLE + } + } + } + + // Default to "prefer not to say" + view?.findViewById(R.id.radio_location_none)?.isChecked = true + } + + private fun setupTextWatchers() { + editCountry.addTextChangedListener(object : android.text.TextWatcher { + override fun afterTextChanged(s: android.text.Editable?) { + viewModel.setLocationCountry(s?.toString()?.takeIf { it.isNotEmpty() }) + } + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + + editRegion.addTextChangedListener(object : android.text.TextWatcher { + override fun afterTextChanged(s: android.text.Editable?) { + viewModel.setLocationRegion(s?.toString()?.takeIf { it.isNotEmpty() }) + } + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + + editCity.addTextChangedListener(object : android.text.TextWatcher { + override fun afterTextChanged(s: android.text.Editable?) { + viewModel.setLocationCity(s?.toString()?.takeIf { it.isNotEmpty() }) + } + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + } +} diff --git a/android/app/src/main/java/ai/ciris/mobile/setup/SetupViewModel.kt b/android/app/src/main/java/ai/ciris/mobile/setup/SetupViewModel.kt index 243423351..585f8f022 100644 --- a/android/app/src/main/java/ai/ciris/mobile/setup/SetupViewModel.kt +++ b/android/app/src/main/java/ai/ciris/mobile/setup/SetupViewModel.kt @@ -53,6 +53,25 @@ class SetupViewModel : ViewModel() { private val _llmModel = MutableLiveData("") val llmModel: LiveData = _llmModel + // === Language & Location Preferences === + private val _preferredLanguage = MutableLiveData("en") + val preferredLanguage: LiveData = _preferredLanguage + + private val _locationCountry = MutableLiveData() + val locationCountry: LiveData = _locationCountry + + private val _locationRegion = MutableLiveData() + val locationRegion: LiveData = _locationRegion + + private val _locationCity = MutableLiveData() + val locationCity: LiveData = _locationCity + + private val _locationGranularity = MutableLiveData("none") + val locationGranularity: LiveData = _locationGranularity + + private val _userTimezone = MutableLiveData() + val userTimezone: LiveData = _userTimezone + // === User Account (only for non-Google users) === private val _username = MutableLiveData("") val username: LiveData = _username @@ -104,6 +123,41 @@ class SetupViewModel : ViewModel() { _llmModel.value = model } + fun setPreferredLanguage(lang: String) { + Log.d(TAG, "setPreferredLanguage: $lang") + _preferredLanguage.value = lang + } + + fun setLocationGranularity(granularity: String) { + Log.d(TAG, "setLocationGranularity: $granularity") + _locationGranularity.value = granularity + if (granularity == "none") { + _locationCountry.value = null + _locationRegion.value = null + _locationCity.value = null + } + } + + fun setLocationCountry(country: String?) { + Log.d(TAG, "setLocationCountry: $country") + _locationCountry.value = country + } + + fun setLocationRegion(region: String?) { + Log.d(TAG, "setLocationRegion: $region") + _locationRegion.value = region + } + + fun setLocationCity(city: String?) { + Log.d(TAG, "setLocationCity: $city") + _locationCity.value = city + } + + fun setUserTimezone(tz: String?) { + Log.d(TAG, "setUserTimezone: $tz") + _userTimezone.value = tz + } + fun setUsername(username: String) { Log.d(TAG, "setUsername: $username") _username.value = username @@ -159,6 +213,11 @@ class SetupViewModel : ViewModel() { Log.i(TAG, " llmApiKey.length: ${_llmApiKey.value?.length ?: 0}") Log.i(TAG, " llmBaseUrl: ${_llmBaseUrl.value}") Log.i(TAG, " llmModel: ${_llmModel.value}") + Log.i(TAG, " preferredLanguage: ${_preferredLanguage.value}") + Log.i(TAG, " locationGranularity: ${_locationGranularity.value}") + Log.i(TAG, " locationCountry: ${_locationCountry.value}") + Log.i(TAG, " locationRegion: ${_locationRegion.value}") + Log.i(TAG, " locationCity: ${_locationCity.value}") Log.i(TAG, " username: ${_username.value}") Log.i(TAG, " showLocalUserFields: ${showLocalUserFields()}") Log.i(TAG, " useCirisProxy: ${useCirisProxy()}") diff --git a/android/app/src/main/java/ai/ciris/mobile/setup/SetupWizardActivity.kt b/android/app/src/main/java/ai/ciris/mobile/setup/SetupWizardActivity.kt index b6363411c..ad8e28577 100644 --- a/android/app/src/main/java/ai/ciris/mobile/setup/SetupWizardActivity.kt +++ b/android/app/src/main/java/ai/ciris/mobile/setup/SetupWizardActivity.kt @@ -24,17 +24,19 @@ import java.net.HttpURLConnection import java.net.URL /** - * Setup Wizard Activity - 3-step native Kotlin wizard. + * Setup Wizard Activity - 4-step native Kotlin wizard. * * Steps: * 1. Welcome - Introduction - * 2. LLM - AI configuration (CIRIS proxy for Google users, BYOK for others) - * 3. Confirm - Summary (Google) or account creation (non-Google) + * 2. Preferences - Language and location (user-chosen granularity) + * 3. LLM - AI configuration (CIRIS proxy for Google users, BYOK for others) + * 4. Confirm - Summary (Google) or account creation (non-Google) * * Key features: * - Admin password is auto-generated (users don't need to set it) * - Google OAuth users get free CIRIS AI (via proxy) * - Non-Google users must provide their own API key (BYOK) + * - Language & location at user-selected granularity */ class SetupWizardActivity : AppCompatActivity() { @@ -48,7 +50,7 @@ class SetupWizardActivity : AppCompatActivity() { companion object { private const val TAG = "SetupWizard" private const val SERVER_URL = "http://localhost:8080" - private const val STEP_COUNT = 3 + private const val STEP_COUNT = 4 } override fun onCreate(savedInstanceState: Bundle?) { @@ -62,10 +64,11 @@ class SetupWizardActivity : AppCompatActivity() { btnNext = findViewById(R.id.btn_next) btnBack = findViewById(R.id.btn_back) - // Only 3 step indicators now + // 4 step indicators indicators.add(findViewById(R.id.step1_indicator)) indicators.add(findViewById(R.id.step2_indicator)) indicators.add(findViewById(R.id.step3_indicator)) + indicators.add(findViewById(R.id.step4_indicator)) // Detect Google OAuth state detectGoogleAuthState() @@ -217,6 +220,11 @@ class SetupWizardActivity : AppCompatActivity() { return true } 1 -> { + // Preferences step - language always valid (has default), location optional + Log.d(TAG, "Preferences step validation: PASS (language=${viewModel.preferredLanguage.value})") + return true + } + 2 -> { // LLM step val mode = viewModel.llmMode.value Log.i(TAG, "LLM step validation: mode=$mode") @@ -250,7 +258,7 @@ class SetupWizardActivity : AppCompatActivity() { return true } } - 2 -> { + 3 -> { // Confirm step val isGoogle = viewModel.isGoogleAuth.value == true Log.i(TAG, "Confirm step validation: isGoogle=$isGoogle") @@ -487,6 +495,29 @@ class SetupWizardActivity : AppCompatActivity() { payload.put("admin_password", password) } + // Language & location preferences + val lang = viewModel.preferredLanguage.value ?: "en" + payload.put("preferred_language", lang) + + val country = viewModel.locationCountry.value + if (country != null) { + payload.put("location_country", country) + } + val region = viewModel.locationRegion.value + if (region != null) { + payload.put("location_region", region) + } + val city = viewModel.locationCity.value + if (city != null) { + payload.put("location_city", city) + } + val tz = viewModel.userTimezone.value + if (tz != null) { + payload.put("timezone", tz) + } + + Log.i(TAG, " preferences: lang=$lang, country=$country, region=$region, city=$city") + // Required fields with defaults payload.put("template_id", "ally") // Force Ally template for Android val adapters = JSONArray() diff --git a/android/app/src/main/python/version.py b/android/app/src/main/python/version.py index 5734ccddc..55e46f565 100644 --- a/android/app/src/main/python/version.py +++ b/android/app/src/main/python/version.py @@ -7,7 +7,7 @@ # Static version - updated at build time by the Android build process # This avoids file-system hashing logic that doesn't work in the Android package -__version__ = "android-2.2.9" +__version__ = "android-2.3.0" def get_version() -> str: diff --git a/android/app/src/main/res/layout/activity_setup_wizard.xml b/android/app/src/main/res/layout/activity_setup_wizard.xml index 0130ce615..afd01e67b 100644 --- a/android/app/src/main/res/layout/activity_setup_wizard.xml +++ b/android/app/src/main/res/layout/activity_setup_wizard.xml @@ -26,7 +26,7 @@ android:textColor="@color/text_primary" android:layout_marginBottom="16dp"/> - + - + - + + + + + + diff --git a/android/app/src/main/res/layout/fragment_setup_preferences.xml b/android/app/src/main/res/layout/fragment_setup_preferences.xml new file mode 100644 index 000000000..a697cf706 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_setup_preferences.xml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index b2edb5b1a..573123aa6 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -38,6 +38,23 @@ 3 4 + + Your Preferences + Help CIRIS communicate with you in your preferred language. Location is optional and helps provide relevant context. + Preferred Language + Location (optional) + Share as much or as little as you like. This helps CIRIS understand your context. + Prefer not to say + Country only + Country + Region/State + Country + Region + City + Country + e.g., Ethiopia, United States, Japan + Region / State + e.g., Amhara, California, Tokyo + City + e.g., Addis Ababa, San Francisco + AI Configuration Configure how CIRIS connects to AI services. Provider diff --git a/ciris_adapters/home_assistant/service.py b/ciris_adapters/home_assistant/service.py index 9f8eaf95a..8ac2281fc 100644 --- a/ciris_adapters/home_assistant/service.py +++ b/ciris_adapters/home_assistant/service.py @@ -484,9 +484,20 @@ async def control_device( if not success: if status == 200 and (not response_data or len(response_data) == 0): - logger.error( - f"[HA DEVICE CONTROL] FAILED! Entity '{entity_id}' not found or not controllable" - ) + # HA returned success but no entities were affected + # This can mean: entity doesn't exist, entity doesn't support action, + # or action had no effect (e.g., media_play with nothing paused) + if action in ("media_play", "media_pause", "media_stop"): + logger.error( + f"[HA DEVICE CONTROL] FAILED! Action '{action}' had no effect on '{entity_id}'. " + f"For media_play: ensure something is paused/queued. " + f"To play new music, use Music Assistant tools (ma_search, ma_play)." + ) + else: + logger.error( + f"[HA DEVICE CONTROL] FAILED! Action '{action}' had no effect on '{entity_id}' " + f"(entity may not exist or doesn't support this action)" + ) else: logger.error(f"[HA DEVICE CONTROL] FAILED! Status {status}") if status == 401: @@ -504,9 +515,18 @@ async def control_device( error_msg = None if not success: if status == 200 and (not response_data or len(response_data) == 0): - error_msg = ( - f"Entity '{entity_id}' not found. Use ha_list_entities to see available entities." - ) + # More specific error based on action type + if action in ("media_play", "media_pause", "media_stop"): + error_msg = ( + f"Action '{action}' had no effect on '{entity_id}'. " + f"media_play resumes paused content; to play NEW music, use Music Assistant tools (ma_search, ma_play)." + ) + else: + error_msg = ( + f"Action '{action}' had no effect on '{entity_id}'. " + f"Entity may not exist or doesn't support this action. " + f"Use ha_list_entities to verify entity availability." + ) else: error_msg = f"Status {status}: {response_text[:200]}" @@ -827,16 +847,12 @@ async def ma_play( Dict with play result including verification status """ try: - # Get MA config entry ID (required for play_media service) - config_entry_id = await self._get_ma_config_entry_id() - + # Build service data - config_entry_id is NOT required and causes 400 if invalid service_data: Dict[str, Any] = { "media_id": media_id, "media_type": media_type, "enqueue": enqueue, } - if config_entry_id: - service_data["config_entry_id"] = config_entry_id if player_id: service_data["entity_id"] = player_id @@ -951,12 +967,15 @@ async def ma_get_players(self) -> Dict[str, Any]: and ( e.entity_id.startswith("media_player.mass_") or e.attributes.get("mass_player_id") + or e.attributes.get("mass_player_type") # MA-managed player + or e.attributes.get("app_id") == "music_assistant" # Currently using MA or "music_assistant" in str(e.attributes.get("friendly_name", "")).lower() ) ] if not ma_players: # Fall back to all media players if no MA-specific ones found + logger.warning("[MA] No Music Assistant players found, returning all media players") ma_players = [ { "entity_id": e.entity_id, @@ -967,7 +986,7 @@ async def ma_get_players(self) -> Dict[str, Any]: if e.domain == "media_player" ] - logger.info(f"[MA] Get players: found {len(ma_players)}") + logger.info(f"[MA] Get players: found {len(ma_players)} (MA-controlled)") return {"success": True, "players": ma_players} except Exception as e: logger.error(f"[MA] Get players exception: {e}") diff --git a/ciris_adapters/home_assistant/tool_service.py b/ciris_adapters/home_assistant/tool_service.py index 159989f42..4ec87aecc 100644 --- a/ciris_adapters/home_assistant/tool_service.py +++ b/ciris_adapters/home_assistant/tool_service.py @@ -119,14 +119,20 @@ class HAToolService: required=["entity_id", "action"], ), documentation=ToolDocumentation( - quick_start="Control HA devices: use entity_id + action. For media players use media_stop/media_play, not turn_off.", + quick_start="Control HA devices: use entity_id + action. For PLAYING NEW MUSIC, use Music Assistant tools (ma_search, ma_play) NOT this tool!", detailed_instructions=""" # Home Assistant Device Control +**IMPORTANT: To PLAY NEW MUSIC (e.g., "play Enya"), use Music Assistant tools:** +- **ma_search** - Search for artists/songs/albums +- **ma_play** - Play music on a player + +This tool (ha_device_control) is for device control, NOT for starting new music! + ## Action Reference by Domain ### media_player.* (Music, Speakers, TVs) -- **media_play** - Start/resume playback +- **media_play** - RESUME paused playback ONLY (does NOT start new music!) - **media_pause** - Pause playback - **media_stop** - STOP playback (USE THIS to stop music!) - **media_play_pause** - Toggle play/pause @@ -196,56 +202,196 @@ class HAToolService: ), ToolGotcha( title="Don't use turn_on to play music", - description="When user says 'play music' or 'resume', use media_play NOT turn_on. turn_on just powers on the device, it doesn't start playback.", + description="When user says 'resume', use media_play NOT turn_on. turn_on just powers on the device, it doesn't start playback.", ), ToolGotcha( title="500 error on turn_off for media players", description="If a media player returns 500 on turn_off, use media_stop instead. Some players (like Music Assistant) don't support turn_off.", ), + ToolGotcha( + title="CRITICAL: media_play does NOT play new music", + description="media_play ONLY resumes paused content. To play NEW music (e.g., 'play Enya'), use Music Assistant tools: ma_search to find content, then ma_play to play it.", + ), ], ), dma_guidance=ToolDMAGuidance( - when_not_to_use="Do not use for querying state - use ha_sensor_query instead. Do not use for automations - use ha_automation_trigger.", + when_not_to_use="Do not use to PLAY NEW MUSIC - use Music Assistant tools (ma_search, ma_play) instead. Do not use for querying state - use ha_sensor_query instead. Do not use for automations - use ha_automation_trigger.", ethical_considerations="Device control affects the physical environment. Verify user intent for irreversible actions like unlocking doors.", ), ), "ha_automation_trigger": ToolInfo( name="ha_automation_trigger", - description="Trigger a Home Assistant automation", + description="Trigger a Home Assistant automation immediately", + when_to_use="Use to manually trigger an automation (e.g., 'run the good morning routine')", parameters=ToolParameterSchema( type="object", properties={ "automation_id": { "type": "string", - "description": "Automation entity ID (e.g., automation.good_morning)", + "description": "Automation entity ID (e.g., automation.good_morning, automation.bedtime_routine)", }, }, required=["automation_id"], ), + documentation=ToolDocumentation( + quick_start="Trigger automations: ha_automation_trigger automation_id='automation.good_morning'", + detailed_instructions=""" +# Home Assistant Automation Trigger + +Manually trigger any Home Assistant automation to run immediately, regardless of its normal triggers. + +## When to Use +- User wants to manually run a routine ("run my morning routine") +- Testing automations +- User wants to trigger a scene-like automation + +## Entity ID Format +Automations use the format: `automation.` +Examples: +- `automation.good_morning` - Morning routine +- `automation.turn_off_all_lights` - All lights off +- `automation.bedtime_routine` - Bedtime routine + +## Important Notes +- Automation must be ENABLED to trigger (disabled automations won't run) +- This bypasses the automation's normal trigger conditions +- The automation runs with its configured actions and conditions + +## Finding Automations +Use ha_list_entities with domain='automation' to see available automations. +""", + examples=[ + UsageExample( + title="Trigger morning routine", + description="Run the good morning automation", + code='{"automation_id": "automation.good_morning"}', + ), + UsageExample( + title="Trigger all lights off", + description="Run automation that turns off all lights", + code='{"automation_id": "automation.turn_off_all_lights"}', + ), + ], + gotchas=[ + ToolGotcha( + title="Disabled automations won't trigger", + description="If an automation is disabled (turned off), triggering it has no effect. Use ha_sensor_query to check the automation's state first if unsure.", + severity="warning", + ), + ToolGotcha( + title="Conditions still apply", + description="The automation's conditions are still evaluated when triggered manually. If conditions fail, actions won't run.", + severity="info", + ), + ], + ), + dma_guidance=ToolDMAGuidance( + when_not_to_use="Don't use for direct device control - use ha_device_control instead. Don't use if you can achieve the goal with a single device command.", + ethical_considerations="Automations may control multiple devices. Verify user intent for automations that affect security (locks, alarms) or major systems.", + ), ), "ha_sensor_query": ToolInfo( name="ha_sensor_query", - description="Query the state of a Home Assistant entity or sensor", + description="Query the current state and attributes of any Home Assistant entity", + when_to_use="Use to check sensor values, device states, or entity attributes (e.g., 'what's the temperature?', 'is the door open?')", parameters=ToolParameterSchema( type="object", properties={ "entity_id": { "type": "string", - "description": "Entity ID to query (e.g., sensor.temperature, binary_sensor.door)", + "description": "Entity ID to query (e.g., sensor.living_room_temperature, binary_sensor.front_door)", }, }, required=["entity_id"], ), + documentation=ToolDocumentation( + quick_start="Query state: ha_sensor_query entity_id='sensor.temperature' - returns state + attributes", + detailed_instructions=""" +# Home Assistant Sensor Query + +Query any Home Assistant entity to get its current state and attributes. + +## What You Get +- **state**: The entity's current value (e.g., "72.5", "on", "open", "home") +- **attributes**: Additional data like unit_of_measurement, friendly_name, device_class +- **last_changed**: When the state last changed + +## Common Entity Types + +### Sensors (sensor.*) +- Temperature: `sensor.living_room_temperature` → "72.5" (°F) +- Humidity: `sensor.bathroom_humidity` → "65" (%) +- Power: `sensor.washing_machine_power` → "150" (W) +- Battery: `sensor.phone_battery_level` → "85" (%) + +### Binary Sensors (binary_sensor.*) +- Door: `binary_sensor.front_door` → "on" (open) / "off" (closed) +- Motion: `binary_sensor.hallway_motion` → "on" (detected) / "off" (clear) +- Window: `binary_sensor.bedroom_window` → "on" (open) / "off" (closed) + +### Other Entities +- Person: `person.john` → "home" / "away" / zone name +- Weather: `weather.home` → "sunny" / "cloudy" / "rainy" +- Device state: `light.bedroom` → "on" / "off" (with brightness in attributes) + +## Attributes +Attributes contain extra info depending on entity type: +- **unit_of_measurement**: "°F", "%", "W", etc. +- **device_class**: "temperature", "humidity", "motion", etc. +- **friendly_name**: Human-readable name +- **brightness**: For lights (0-255) +- **media_title**: For media players (currently playing) +""", + examples=[ + UsageExample( + title="Check temperature", + description="Query a temperature sensor", + code='{"entity_id": "sensor.living_room_temperature"}', + ), + UsageExample( + title="Check if door is open", + description="Query a door sensor", + code='{"entity_id": "binary_sensor.front_door"}', + ), + UsageExample( + title="Check person location", + description="See if someone is home", + code='{"entity_id": "person.john"}', + ), + UsageExample( + title="Check light brightness", + description="Query a light to see its state and brightness attribute", + code='{"entity_id": "light.bedroom_lamp"}', + ), + ], + gotchas=[ + ToolGotcha( + title="Unavailable state", + description="If entity returns 'unavailable' or 'unknown', the device may be offline or not responding.", + severity="info", + ), + ToolGotcha( + title="Binary sensor on/off meaning", + description="For binary sensors, 'on' typically means triggered/open/detected, 'off' means normal/closed/clear. Check device_class attribute for exact meaning.", + severity="info", + ), + ], + ), + dma_guidance=ToolDMAGuidance( + when_not_to_use="Don't use for controlling devices - use ha_device_control. Don't use to get lists of entities - use ha_list_entities.", + ethical_considerations="Sensor data may reveal presence/absence patterns. Be mindful when querying person or motion sensors.", + ), ), "ha_list_entities": ToolInfo( name="ha_list_entities", description="List all Home Assistant entities, optionally filtered by domain", + when_to_use="Use to discover available devices and their entity IDs (e.g., 'what lights do I have?', 'list all sensors')", parameters=ToolParameterSchema( type="object", properties={ "domain": { "type": "string", - "description": "Filter by domain (light, switch, sensor, etc.). Leave empty for all.", + "description": "Filter by domain (light, switch, sensor, media_player, climate, etc.). Leave empty for all.", }, }, required=[], @@ -254,55 +400,268 @@ class HAToolService: # to provide available entities to the ASPDMA for action selection context_enrichment=True, context_enrichment_params={}, # Empty params = list all entities + documentation=ToolDocumentation( + quick_start="List entities: ha_list_entities (all) or ha_list_entities domain='light' (filtered)", + detailed_instructions=""" +# Home Assistant Entity List + +Discover all available Home Assistant entities, optionally filtered by domain. + +## Common Domains +- **light** - Lights, bulbs, LED strips +- **switch** - Smart plugs, switches +- **sensor** - Temperature, humidity, power, battery sensors +- **binary_sensor** - Door, motion, window sensors (on/off states) +- **media_player** - Speakers, TVs, streaming devices +- **climate** - Thermostats, AC units +- **cover** - Blinds, curtains, garage doors +- **lock** - Smart locks +- **fan** - Fans, ventilation +- **camera** - Security cameras +- **automation** - Automations (for triggering) +- **person** - People/presence tracking + +## Response Format +Returns up to 50 entities, prioritized by usefulness: +1. Controllable devices (lights, switches, climate) +2. Media players, covers, fans, locks +3. Sensors +4. Other entities + +Each entity includes: +- **entity_id**: Full ID for use with other tools +- **state**: Current state +- **friendly_name**: Human-readable name +- **domain**: Entity domain + +## Finding Specific Entities +If you need a specific entity but don't know its ID: +1. Filter by domain first (e.g., domain='light') +2. Match by friendly_name in results +3. Use the entity_id with other tools +""", + examples=[ + UsageExample( + title="List all lights", + description="See all available lights", + code='{"domain": "light"}', + ), + UsageExample( + title="List all media players", + description="Find speakers and TVs", + code='{"domain": "media_player"}', + ), + UsageExample( + title="List all entities", + description="See everything (prioritized list)", + code='{}', + ), + UsageExample( + title="List all automations", + description="Find automations to trigger", + code='{"domain": "automation"}', + ), + ], + gotchas=[ + ToolGotcha( + title="Results limited to 50 entities", + description="Large installations may have more entities. Use domain filter to find specific types.", + severity="info", + ), + ToolGotcha( + title="Unavailable entities filtered out", + description="Entities in 'unavailable' or 'unknown' state are hidden by default to reduce noise.", + severity="info", + ), + ], + ), + dma_guidance=ToolDMAGuidance( + when_not_to_use="Don't use repeatedly - results are often provided in context. Don't use to control devices - use ha_device_control.", + ), ), "ha_notification": ToolInfo( name="ha_notification", - description="Send a notification via Home Assistant", + description="Send a push notification via Home Assistant to phones or other notification services", + when_to_use="Use to send alerts, reminders, or messages to user's phone (e.g., 'send me a reminder', 'alert me when...')", parameters=ToolParameterSchema( type="object", properties={ - "title": {"type": "string", "description": "Notification title"}, - "message": {"type": "string", "description": "Notification message"}, - "target": {"type": "string", "description": "Target service (e.g., mobile_app_phone). Optional."}, + "title": {"type": "string", "description": "Notification title (short, descriptive)"}, + "message": {"type": "string", "description": "Notification message body"}, + "target": { + "type": "string", + "description": "Target service name (e.g., mobile_app_johns_iphone). Optional - uses default if not specified.", + }, }, required=["title", "message"], ), + documentation=ToolDocumentation( + quick_start="Send notification: ha_notification title='Alert' message='Something happened'", + detailed_instructions=""" +# Home Assistant Notification + +Send push notifications through Home Assistant's notification system. + +## Target Services +Notifications can be sent to various targets: +- **Mobile apps**: `mobile_app_` (e.g., mobile_app_johns_iphone) +- **Persistent notification**: `persistent_notification` (shows in HA dashboard) +- **Other services**: Telegram, Slack, email (if configured) + +## Message Formatting +- Keep titles short (shown in notification header) +- Messages can be longer (shown in notification body) +- Some targets support markdown formatting + +## Common Use Cases +- Reminders and alerts +- Status updates +- Security notifications +- Automation confirmations + +## Finding Target Names +Mobile app targets follow the pattern: `mobile_app_` +The device name is set when the HA Companion app is installed. +""", + examples=[ + UsageExample( + title="Simple notification", + description="Send a basic notification to default target", + code='{"title": "Reminder", "message": "Don\'t forget to take out the trash"}', + ), + UsageExample( + title="Notification to specific phone", + description="Send to a specific mobile device", + code='{"title": "Motion Detected", "message": "Motion detected at front door", "target": "mobile_app_johns_iphone"}', + ), + UsageExample( + title="Persistent notification", + description="Show notification in HA dashboard", + code='{"title": "System Update", "message": "Updates available", "target": "persistent_notification"}', + ), + ], + gotchas=[ + ToolGotcha( + title="Target name format", + description="Mobile app targets use underscores, not spaces: mobile_app_johns_iphone not mobile_app_john's iphone", + severity="warning", + ), + ToolGotcha( + title="App must be configured", + description="The HA Companion app must be installed and configured on the target device for mobile notifications to work.", + severity="info", + ), + ], + ), + dma_guidance=ToolDMAGuidance( + when_not_to_use="Don't use for conversational responses - just respond directly. Don't use excessively - notifications should be meaningful.", + ethical_considerations="Notifications interrupt users. Use sparingly and only when the user has requested alerts or reminders.", + ), ), "ha_camera_analyze": ToolInfo( name="ha_camera_analyze", - description="Analyze a camera feed for motion detection", + description="Analyze a camera feed for motion detection and brightness levels", + when_to_use="Use to detect motion or analyze camera feeds (e.g., 'is there motion at the front door?', 'check the backyard camera')", parameters=ToolParameterSchema( type="object", properties={ - "camera_name": {"type": "string", "description": "Name of the camera to analyze"}, + "camera_name": { + "type": "string", + "description": "Camera name or identifier (e.g., 'front_door', 'backyard')", + }, "duration_seconds": { "type": "integer", "minimum": 1, "maximum": 60, - "description": "How long to analyze (default: 10)", + "description": "Analysis duration in seconds (default: 10, max: 60)", }, }, required=["camera_name"], ), + documentation=ToolDocumentation( + quick_start="Analyze camera: ha_camera_analyze camera_name='front_door' duration_seconds=10", + detailed_instructions=""" +# Home Assistant Camera Analysis + +Analyze a WebRTC camera feed for motion detection and brightness levels. + +## How It Works +1. Connects to the camera's WebRTC stream +2. Captures frames for the specified duration +3. Analyzes frames for motion between consecutive frames +4. Calculates average brightness level + +## Results Include +- **frames_analyzed**: Number of frames captured +- **motion_detected**: Boolean - whether motion was detected +- **average_brightness**: 0-255 brightness level (0=black, 255=white) + +## Camera Configuration +Cameras must be configured in the WEBRTC_CAMERA_URLS environment variable. +Format: `camera_name=rtsp://url;other_camera=rtsp://url2` + +## Duration Guidelines +- **Short (1-5s)**: Quick motion check +- **Medium (10-15s)**: Typical analysis +- **Long (30-60s)**: Extended monitoring + +## Motion Detection +Motion is detected by comparing pixel differences between frames. +Sensitivity is tuned for typical indoor/outdoor use. +""", + examples=[ + UsageExample( + title="Quick motion check", + description="Check for motion at front door", + code='{"camera_name": "front_door", "duration_seconds": 5}', + ), + UsageExample( + title="Extended analysis", + description="Monitor backyard for 30 seconds", + code='{"camera_name": "backyard", "duration_seconds": 30}', + ), + ], + gotchas=[ + ToolGotcha( + title="Requires OpenCV", + description="Camera analysis requires OpenCV (cv2) to be installed. Feature is disabled without it.", + severity="warning", + ), + ToolGotcha( + title="Camera must be configured", + description="Camera URLs must be set in WEBRTC_CAMERA_URLS environment variable.", + severity="warning", + ), + ToolGotcha( + title="Processing time", + description="Analysis takes approximately the duration_seconds plus a few seconds for processing.", + severity="info", + ), + ], + ), + dma_guidance=ToolDMAGuidance( + when_not_to_use="Don't use for viewing camera feeds - this is for motion analysis only. Don't use for camera snapshots - use HA camera entity instead.", + ethical_considerations="Camera analysis involves visual monitoring. Ensure user has legitimate reason to analyze camera feeds.", + ), ), # ===================================================================== # Music Assistant Tools (uses HA service calls - requires MA integration in HA) # ===================================================================== "ma_search": ToolInfo( name="ma_search", - description="Search Music Assistant library for tracks, albums, artists, or playlists", - when_to_use="Use when the user wants to find music, search for songs, artists, or albums", + description="Search Music Assistant library for tracks, albums, artists, or playlists across all providers", + when_to_use="Use when user wants to find music before playing (e.g., 'find songs by Queen', 'search for classical music')", parameters=ToolParameterSchema( type="object", properties={ "query": { "type": "string", - "description": "Search query (song name, artist, album, etc.)", + "description": "Search query - song name, artist, album, or descriptive terms", }, "media_types": { "type": "array", "items": {"type": "string", "enum": ["artist", "album", "track", "playlist", "radio"]}, - "description": "Types to search. Default: all types.", + "description": "Filter by type. Default: all types. Use ['track'] for songs.", }, "limit": { "type": "integer", @@ -314,103 +673,247 @@ class HAToolService: required=["query"], ), documentation=ToolDocumentation( - quick_start="Search for music: ma_search query='song name' to find tracks, albums, artists", + quick_start="Search for music: ma_search query='Bohemian Rhapsody Queen' media_types=['track']", detailed_instructions=""" # Music Assistant Search -Search across all configured music providers (Spotify, Tidal, local library, etc.). -Requires Music Assistant integration to be installed in Home Assistant. +Search across ALL configured music providers (Spotify, Apple Music, Tidal, YouTube Music, local library, etc.). + +## Search Strategies + +### For Specific Songs +Include both song name AND artist for best results: +- "Bohemian Rhapsody Queen" ✓ +- "Never Gonna Give You Up Rick Astley" ✓ +- "Bohemian Rhapsody" (may return covers) ⚠️ -## Search Tips -- For specific songs: include artist name ("Never Gonna Give You Up Rick Astley") -- For albums: include album name and optionally artist -- For artists: just the artist name -- Filter by type with media_types parameter +### For Albums +Include album name and optionally artist: +- "Abbey Road Beatles" ✓ +- "Greatest Hits Queen" ✓ -## Results -Returns matches grouped by type (tracks, albums, artists, playlists). -Use the media_id from results with ma_play to start playback. +### For Artists +Just the artist name: +- "Taylor Swift" +- "The Beatles" + +### For Genres/Moods (if supported by provider) +- "relaxing piano music" +- "workout playlist" + +## Media Types +- **track**: Individual songs +- **album**: Full albums +- **artist**: Artists +- **playlist**: User or curated playlists +- **radio**: Radio stations + +## Using Search Results +Results include a `media_id` for each item. Use this ID with ma_play: +1. Search: ma_search query="Enya" → results include media_id +2. Play: ma_play media_id="" + +Or just use the name directly with ma_play (it will search automatically). + +## Provider Aggregation +Results come from ALL configured providers and are deduplicated. +The same song from Spotify and your local library will appear once. """, examples=[ UsageExample( - title="Search for a song", - description="Find a specific track", - code='{"query": "Bohemian Rhapsody Queen", "media_types": ["track"]}', + title="Search for a specific song", + description="Find a track with artist name for accuracy", + code='{"query": "Bohemian Rhapsody Queen", "media_types": ["track"], "limit": 5}', ), UsageExample( title="Search for an artist", - description="Find all content by an artist", - code='{"query": "Taylor Swift", "media_types": ["artist", "album", "track"]}', + description="Find artist and their albums", + code='{"query": "Taylor Swift", "media_types": ["artist", "album"]}', + ), + UsageExample( + title="Search for playlists", + description="Find themed playlists", + code='{"query": "workout", "media_types": ["playlist"]}', + ), + UsageExample( + title="Broad search", + description="Search all types for a term", + code='{"query": "classical piano"}', + ), + ], + gotchas=[ + ToolGotcha( + title="Include artist for songs", + description="Searching just 'Yesterday' returns many covers. Use 'Yesterday Beatles' for the original.", + severity="warning", + ), + ToolGotcha( + title="Results are async", + description="Large searches may return progressively. Results show what's found so far.", + severity="info", + ), + ToolGotcha( + title="MA must be installed", + description="Returns 404 if Music Assistant integration is not installed in Home Assistant.", + severity="error", ), ], ), dma_guidance=ToolDMAGuidance( - when_not_to_use="Do not use if Music Assistant integration is not installed in Home Assistant.", + when_not_to_use="Don't use if user just wants to play something - ma_play can search by name directly. Don't use for browsing library categories - use ma_browse instead.", + ethical_considerations="Search queries may reveal music preferences. Handle with appropriate privacy.", ), ), "ma_play": ToolInfo( name="ma_play", - description="Play a track, album, or playlist on Music Assistant", - when_to_use="Use after ma_search to play a specific item, or to play by name", + description="Play music via Music Assistant - USE THIS when user wants to play songs, albums, artists, or playlists!", + when_to_use="Use to PLAY MUSIC - when user says 'play [song/artist/album]', 'put on some [genre]', use this tool!", + # Context enrichment: include this tool's info prominently in ASPDMA context + context_enrichment=True, + context_enrichment_params={"_info_only": True}, # Just surface the tool info, don't execute parameters=ToolParameterSchema( type="object", properties={ "media_id": { "type": "string", - "description": "Media name, URI, or ID from search results", + "description": "What to play: song name ('Bohemian Rhapsody'), 'artist - song', URI, or ID from ma_search", }, "player_id": { "type": "string", - "description": "Target player entity ID (e.g., media_player.mass_living_room)", + "description": "Target player (e.g., media_player.mass_living_room). Optional - uses default player.", }, "media_type": { "type": "string", "enum": ["track", "album", "artist", "playlist", "radio"], - "description": "Type of media to play (default: track)", + "description": "What kind of media: track (song), album, artist (all songs), playlist. Default: track", }, "enqueue": { "type": "string", "enum": ["play", "next", "add", "replace"], - "description": "Queue behavior: play (now), next (after current), add (end), replace (clear queue)", + "description": "Queue behavior: play (now), next (after current), add (end of queue), replace (clear queue first)", }, }, required=["media_id"], ), documentation=ToolDocumentation( - quick_start="Play music: ma_play media_id='song name' or use ID from ma_search results", + quick_start="Play music: ma_play media_id='Bohemian Rhapsody Queen' media_type='track'", detailed_instructions=""" # Music Assistant Play -Play media via Music Assistant through Home Assistant. - -## Queue Options -- **play**: Start playing immediately (default) -- **next**: Add after currently playing track +**THIS IS THE TOOL FOR PLAYING NEW MUSIC!** +When user says "play Enya", "put on some jazz", "play Beatles" - use this tool. + +## How to Specify What to Play + +### By Name (Recommended for simple requests) +``` +media_id="Bohemian Rhapsody" # Song name +media_id="Queen - Bohemian Rhapsody" # Artist - Song (more accurate) +media_id="Abbey Road" # Album name +media_id="Taylor Swift" # Artist name (plays their music) +``` + +### By URI (From search results) +``` +media_id="library://track/123" +media_id="spotify://track/abc123" +``` + +## Media Types +- **track**: Play a specific song (default) +- **album**: Play a full album +- **artist**: Play songs by an artist (shuffled or top tracks) +- **playlist**: Play a playlist +- **radio**: Play a radio station + +## Queue Behavior (enqueue parameter) +- **play**: Start playing immediately, adds to queue (default) +- **replace**: Clear queue first, then play +- **next**: Insert after currently playing track - **add**: Add to end of queue -- **replace**: Clear queue and play this item ## Player Selection -If player_id not specified, uses the default/active player. -Use ma_players to see available Music Assistant players. +- If no player_id specified, plays on the default/active player +- Use ma_players to see available players +- Players are typically: media_player.mass_ + +## Common Patterns + +"Play Enya" → + ma_play media_id="Enya" media_type="artist" + +"Play Bohemian Rhapsody" → + ma_play media_id="Bohemian Rhapsody Queen" media_type="track" + +"Play the Abbey Road album" → + ma_play media_id="Abbey Road Beatles" media_type="album" + +"Play music in the kitchen" → + ma_play media_id="" player_id="media_player.mass_kitchen" + +## Verification +After playing, the tool verifies playback actually started. +If verification fails, suggests using ma_search first to confirm the media exists. """, examples=[ UsageExample( title="Play a song by name", - description="Play a track immediately", - code='{"media_id": "Bohemian Rhapsody", "media_type": "track", "enqueue": "play"}', + description="Simple track playback", + code='{"media_id": "Bohemian Rhapsody Queen", "media_type": "track"}', ), UsageExample( - title="Play on specific player", - description="Play on a specific speaker", + title="Play an artist", + description="Play music by a specific artist", + code='{"media_id": "Enya", "media_type": "artist"}', + ), + UsageExample( + title="Play album on specific speaker", + description="Play full album in a specific room", code='{"media_id": "Abbey Road", "media_type": "album", "player_id": "media_player.mass_living_room"}', ), + UsageExample( + title="Add song to queue", + description="Queue a song after the current one", + code='{"media_id": "Hey Jude Beatles", "media_type": "track", "enqueue": "next"}', + ), + UsageExample( + title="Clear queue and play", + description="Replace current queue with new content", + code='{"media_id": "relaxing piano", "media_type": "playlist", "enqueue": "replace"}', + ), + ], + gotchas=[ + ToolGotcha( + title="HA returns success even if media not found!", + description="Home Assistant returns 200 success even if the track doesn't exist. We verify by checking player state after 2 seconds.", + severity="warning", + ), + ToolGotcha( + title="Include artist name for accuracy", + description="'Yesterday' might play a cover. Use 'Yesterday Beatles' for the original.", + severity="warning", + ), + ToolGotcha( + title="Don't use media-source:// URIs", + description="Only use MA URIs (library://, spotify://) or plain names. media-source:// causes issues.", + severity="error", + ), + ToolGotcha( + title="Player must be available", + description="If specified player is off or unavailable, playback fails. Check ma_players first if unsure.", + severity="info", + ), ], ), + dma_guidance=ToolDMAGuidance( + when_not_to_use="Don't use for pause/stop/skip - use ha_device_control with media_pause/media_stop/media_next_track. Don't use to resume paused content - use ha_device_control with media_play.", + ethical_considerations="Playing music affects everyone in the room. Consider if others might be disturbed.", + ), ), "ma_browse": ToolInfo( name="ma_browse", description="Browse Music Assistant library categories (artists, albums, playlists)", - when_to_use="Use to explore available music without a specific search query", + when_to_use="Use to explore available music without a specific search query (e.g., 'what music do I have?', 'show me my playlists')", parameters=ToolParameterSchema( type="object", properties={ @@ -429,50 +932,290 @@ class HAToolService: required=[], ), documentation=ToolDocumentation( - quick_start="Browse library: ma_browse path='artists' to see all artists", + quick_start="Browse library: ma_browse media_type='artists' to see all artists", detailed_instructions=""" # Music Assistant Browse -Navigate the music library by category. +Navigate the music library by category without a specific search query. + +## When to Use +- User asks "what music do I have?" +- User wants to explore their library +- User asks "show me my playlists" +- Discovering available content before playing + +## Media Types + +### artists +Lists all artists in the library across all providers. +Returns: artist names, IDs, image URLs, provider info. + +### albums +Lists all albums in the library. +Returns: album names, artists, year, track count, IDs. + +### tracks +Lists all individual tracks. +Returns: track names, artists, albums, duration, IDs. + +### playlists +Lists all playlists (user-created and provider playlists). +Returns: playlist names, track counts, owner, IDs. -## Available Paths -- **artists**: List all artists -- **albums**: List all albums -- **tracks**: List all tracks -- **playlists**: List all playlists -- (empty): Show root categories +## Response Format +Each item includes: +- **name**: Display name +- **media_id**: ID for use with ma_play +- **uri**: Full URI for playback +- **provider**: Which music service it's from +- **image_url**: Cover art (if available) + +## Browsing vs Searching +- **ma_browse**: Explore without a query (library navigation) +- **ma_search**: Find specific content by name/keyword + +## Usage Flow +1. ma_browse media_type='playlists' → see available playlists +2. Find interesting playlist in results +3. ma_play media_id='' media_type='playlist' """, + examples=[ + UsageExample( + title="List all artists", + description="See all artists in the library", + code='{"media_type": "artists", "limit": 50}', + ), + UsageExample( + title="Browse playlists", + description="See available playlists", + code='{"media_type": "playlists"}', + ), + UsageExample( + title="List albums", + description="Browse the album collection", + code='{"media_type": "albums", "limit": 25}', + ), + ], + gotchas=[ + ToolGotcha( + title="Results are from all providers", + description="Browse returns content from ALL configured providers (Spotify, local library, etc.) combined. Results may be deduplicated.", + severity="info", + ), + ToolGotcha( + title="Large libraries may be slow", + description="Libraries with thousands of items may take longer to browse. Use limit parameter to control response size.", + severity="info", + ), + ToolGotcha( + title="Use search for specific content", + description="If user wants something specific like 'Taylor Swift songs', use ma_search instead of browsing all tracks.", + severity="warning", + ), + ], + ), + dma_guidance=ToolDMAGuidance( + when_not_to_use="Don't use when user wants specific content - use ma_search instead. Don't use just to see what's playing - use ma_players or ma_queue.", + ethical_considerations="Library browsing reveals music collection and preferences. Handle with appropriate privacy.", ), ), "ma_queue": ToolInfo( name="ma_queue", description="View or manage the playback queue for a Music Assistant player", - when_to_use="Use to see what's playing or coming up in the queue", + when_to_use="Use to see what's playing now, what's up next, or view the full queue (e.g., 'what's playing?', 'what's next?', 'show me the queue')", parameters=ToolParameterSchema( type="object", properties={ "player_id": { "type": "string", - "description": "Player entity ID to get queue for", + "description": "Player entity ID to get queue for (e.g., media_player.mass_living_room)", }, }, required=["player_id"], ), documentation=ToolDocumentation( - quick_start="View queue: ma_queue player_id='media_player.living_room'", + quick_start="View queue: ma_queue player_id='media_player.mass_living_room'", + detailed_instructions=""" +# Music Assistant Queue + +View the current playback queue for a Music Assistant player. + +## When to Use +- User asks "what's playing?" +- User asks "what's up next?" +- User wants to see the queue +- User asks "what song is this?" +- Before modifying the queue with ma_play + +## Response Format +The queue includes: +- **current_item**: Currently playing track with full metadata +- **queue_items**: List of upcoming tracks +- **current_index**: Position in queue (0-based) +- **repeat_mode**: off, one, all +- **shuffle_enabled**: true/false + +### Track Metadata +Each track includes: +- **name**: Track title +- **artist**: Artist name +- **album**: Album name +- **duration**: Length in seconds +- **media_id**: ID for requeuing +- **image_url**: Album art + +## Getting Player ID +Use ma_players first to see available players and their entity IDs. +MA players typically have IDs like: media_player.mass_ + +## Queue Management +To modify the queue, use ma_play with the enqueue parameter: +- **enqueue='next'**: Insert after current track +- **enqueue='add'**: Add to end of queue +- **enqueue='replace'**: Clear and start fresh + +## Playback Control +To control playback (pause, skip, stop), use ha_device_control: +- **media_pause**: Pause current track +- **media_play**: Resume playback +- **media_next_track**: Skip to next in queue +- **media_previous_track**: Go back +- **media_stop**: Stop playback entirely +""", + examples=[ + UsageExample( + title="View current queue", + description="See what's playing and what's coming up", + code='{"player_id": "media_player.mass_living_room"}', + ), + UsageExample( + title="Check bedroom queue", + description="View queue on a specific player", + code='{"player_id": "media_player.mass_bedroom"}', + ), + ], + gotchas=[ + ToolGotcha( + title="Player ID is required", + description="Unlike ma_play which has a default player, ma_queue requires a specific player_id. Use ma_players to find available players first.", + severity="warning", + ), + ToolGotcha( + title="MA players have mass_ prefix", + description="Music Assistant players typically have entity IDs like media_player.mass_. Non-mass media players won't have MA queues.", + severity="info", + ), + ToolGotcha( + title="Empty queue if nothing playing", + description="If the player is idle, queue will be empty. This is normal - music hasn't been started yet.", + severity="info", + ), + ], + ), + dma_guidance=ToolDMAGuidance( + when_not_to_use="Don't use for controlling playback (pause/skip/stop) - use ha_device_control. Don't use for adding to queue - use ma_play with enqueue parameter.", + ethical_considerations="Queue contents reveal current listening activity and preferences.", ), ), "ma_players": ToolInfo( name="ma_players", description="List all Music Assistant players and their current state", - when_to_use="Use to see available players and what's currently playing", + when_to_use="Use to see available music players, what's currently playing on each, and their entity IDs for targeting playback", + # Context enrichment: run this to show available music players in context + context_enrichment=True, + context_enrichment_params={}, # Empty = execute to get player list parameters=ToolParameterSchema( type="object", properties={}, required=[], ), documentation=ToolDocumentation( - quick_start="List players: ma_players to see all available music players", + quick_start="List players: ma_players - shows all music players with their states and entity IDs", + detailed_instructions=""" +# Music Assistant Players + +List all Music Assistant players with their current state and what's playing. + +## When to Use +- User asks "where can I play music?" +- User asks "what speakers do I have?" +- Need to find the player_id for other tools (ma_play, ma_queue) +- User asks "what's playing in each room?" +- Before playing music to a specific room + +## Response Format +Returns a list of players, each with: +- **entity_id**: The player ID (e.g., media_player.mass_living_room) - USE THIS with other tools! +- **name**: Friendly name (e.g., "Living Room Speaker") +- **state**: playing, paused, idle, off +- **current_media**: What's playing (if applicable) + - track: Song name + - artist: Artist name + - album: Album name + - duration: Track length + - position: Current playback position +- **volume_level**: Current volume (0.0 - 1.0) +- **is_grouped**: Whether player is part of a group +- **group_members**: Other players in the group (if grouped) + +## Player Types +Music Assistant can manage various player types: +- **Streaming speakers**: Sonos, Chromecast, AirPlay +- **Smart speakers**: Google Home, Echo +- **Network players**: Squeezebox, DLNA +- **Local playback**: On the MA server itself + +## Using Results +The entity_id from this tool is used with: +- **ma_play**: `player_id='media_player.mass_bedroom'` +- **ma_queue**: `player_id='media_player.mass_bedroom'` +- **ha_device_control**: `entity_id='media_player.mass_bedroom'` for pause/stop/skip + +## Player Entity ID Pattern +MA players use the pattern: `media_player.mass_` +Examples: +- media_player.mass_living_room +- media_player.mass_bedroom +- media_player.mass_kitchen + +## Grouped Playback +MA supports synchronized multi-room audio. If players are grouped: +- Commands to the group leader affect all members +- is_grouped=true and group_members shows the group +""", + examples=[ + UsageExample( + title="List all players", + description="Get all available players and their states", + code='{}', + ), + ], + gotchas=[ + ToolGotcha( + title="Players may be unavailable", + description="Powered-off speakers or offline devices show as 'unavailable'. They can't play music until powered on.", + severity="info", + ), + ToolGotcha( + title="mass_ prefix indicates MA control", + description="Only players with 'mass_' in their entity_id are controlled by Music Assistant. Other media_player entities are regular HA players.", + severity="warning", + ), + ToolGotcha( + title="Group commands apply to all", + description="When playing to a grouped player, all members play the same content. Check is_grouped before targeting specific rooms.", + severity="info", + ), + ToolGotcha( + title="Run this before playing to a room", + description="If user says 'play music in the kitchen', run ma_players first to find the correct player_id, then use ma_play.", + severity="warning", + ), + ], + ), + dma_guidance=ToolDMAGuidance( + when_not_to_use="Don't use for playing music - use ma_play. Don't use for playback control - use ha_device_control. Don't use repeatedly - player list doesn't change often.", + ethical_considerations="Player list reveals home audio setup and current listening activity in each room.", ), ), } @@ -820,12 +1563,24 @@ async def _execute_device_control(self, params: Dict[str, Any]) -> ToolExecution correlation_id=str(uuid.uuid4()), ) - # Extract optional parameters + # Extract optional parameters for various domains kwargs: Dict[str, Any] = {} + # Light parameters if "brightness" in params: kwargs["brightness"] = params["brightness"] if "color_temp" in params: kwargs["color_temp"] = params["color_temp"] + # Media player parameters + if "volume_level" in params: + kwargs["volume_level"] = params["volume_level"] + # Climate parameters + if "temperature" in params: + kwargs["temperature"] = params["temperature"] + if "hvac_mode" in params: + kwargs["hvac_mode"] = params["hvac_mode"] + # Fan parameters + if "percentage" in params: + kwargs["percentage"] = params["percentage"] result = await self.ha_service.control_device(entity_id, action, **kwargs) diff --git a/ciris_adapters/mock_llm/responses_action_selection.py b/ciris_adapters/mock_llm/responses_action_selection.py index b6efdaaa3..eefa87f98 100644 --- a/ciris_adapters/mock_llm/responses_action_selection.py +++ b/ciris_adapters/mock_llm/responses_action_selection.py @@ -1565,14 +1565,11 @@ def aspdma_llm_result( ponder_questions=params_dict.get("questions", ["What should I do?"]), ) elif action == HandlerActionType.TOOL: - # For $tool commands, params_dict contains parsed parameters - # Pass them through so they don't need to be re-extracted by TSASPDMA - tool_parameters = params_dict.get("parameters", {}) + # ASPDMA only selects tool NAME - TSASPDMA extracts parameters flat_result = ASPDMALLMResult( selected_action=action, rationale=rationale, tool_name=params_dict.get("name", "unknown_tool"), - tool_parameters=tool_parameters if tool_parameters else None, ) elif action == HandlerActionType.REJECT: flat_result = ASPDMALLMResult( diff --git a/ciris_adapters/navigation/service.py b/ciris_adapters/navigation/service.py index 7e6c5fa92..973ffa87b 100644 --- a/ciris_adapters/navigation/service.py +++ b/ciris_adapters/navigation/service.py @@ -50,7 +50,12 @@ def __init__(self) -> None: self.routing_url = "https://router.project-osrm.org" # OSRM for routing # User agent is required by OSM policy - self.user_agent = os.getenv("CIRIS_OSM_USER_AGENT", "CIRIS/1.0 (contact@ciris.ai)") + # Use PUBLIC_API_CONTACT_EMAIL from wizard, fall back to legacy env var + contact_email = os.getenv("PUBLIC_API_CONTACT_EMAIL", "") + if contact_email: + self.user_agent = f"CIRIS/2.3 ({contact_email})" + else: + self.user_agent = os.getenv("CIRIS_OSM_USER_AGENT", "CIRIS/1.0 (contact@ciris.ai)") # Rate limiting self._last_request_time: float = 0.0 diff --git a/ciris_adapters/wallet/__init__.py b/ciris_adapters/wallet/__init__.py new file mode 100644 index 000000000..9f4aeb3dd --- /dev/null +++ b/ciris_adapters/wallet/__init__.py @@ -0,0 +1,51 @@ +""" +CIRIS Wallet Adapter. + +Generic money tools that work across crypto (x402/USDC) and fiat (Chapa/ETB) providers. + +Tools: +- send_money: Send money to any recipient +- request_money: Create payment requests/invoices +- get_statement: Check balance, history, and account details + +The implementation details (crypto vs fiat) are abstracted behind currency +and provider_params, enabling a unified interface for all payment operations. +""" + +from .adapter import Adapter, WalletAdapter +from .config import ChapaProviderConfig, WalletAdapterConfig, X402ProviderConfig +from .schemas import ( + AccountDetails, + AccountStatement, + Balance, + PaymentRequest, + PaymentRequestStatus, + PaymentVerification, + Transaction, + TransactionResult, + TransactionStatus, + TransactionType, +) +from .tool_service import WalletToolService + +__all__ = [ + # Adapter + "Adapter", + "WalletAdapter", + "WalletToolService", + # Config + "WalletAdapterConfig", + "X402ProviderConfig", + "ChapaProviderConfig", + # Schemas + "Transaction", + "TransactionResult", + "TransactionType", + "TransactionStatus", + "PaymentRequest", + "PaymentRequestStatus", + "PaymentVerification", + "Balance", + "AccountDetails", + "AccountStatement", +] diff --git a/ciris_adapters/wallet/adapter.py b/ciris_adapters/wallet/adapter.py new file mode 100644 index 000000000..61438a16c --- /dev/null +++ b/ciris_adapters/wallet/adapter.py @@ -0,0 +1,284 @@ +""" +Wallet Adapter for CIRIS. + +Provides generic money tools (send_money, request_money, get_statement) +that work across crypto (x402/USDC) and fiat (Chapa/ETB) providers. + +This adapter follows the CIRIS adapter pattern: +- Tool service registered with ToolBus +- Provider-agnostic interface +- DMA-gated financial operations + +KEY FEATURE: Auto-loads from CIRISVerify +- Wallet address derived from CIRISVerify Ed25519 public key +- Private key NEVER leaves secure element +- Every CIRIS agent has a wallet address from birth +""" + +import asyncio +import logging +import os +from typing import Any, Callable, Dict, List, Optional + +from ciris_engine.logic.adapters.base import Service +from ciris_engine.logic.registries.base import Priority +from ciris_engine.schemas.adapters import AdapterServiceRegistration +from ciris_engine.schemas.runtime.adapter_management import AdapterConfig, RuntimeAdapterStatus +from ciris_engine.schemas.runtime.enums import ServiceType + +from .config import ChapaProviderConfig, WalletAdapterConfig, X402ProviderConfig +from .providers.chapa_provider import ChapaProvider +from .providers.x402_provider import X402Provider +from .tool_service import WalletToolService + +logger = logging.getLogger(__name__) + + +class WalletAdapter(Service): + """ + Wallet adapter platform for CIRIS. + + Provides generic money tools that abstract crypto and fiat payments: + - send_money: Send to any recipient (USDC address, phone number, etc.) + - request_money: Create payment requests/invoices + - get_statement: Check balance and transaction history + + The implementation routes to the appropriate provider based on currency + or explicit provider_params. + """ + + def __init__( + self, + runtime: Any, + context: Optional[Any] = None, + config: Optional[WalletAdapterConfig] = None, + **kwargs: Any, + ) -> None: + """Initialize Wallet adapter.""" + super().__init__(config=kwargs.get("adapter_config")) + self.runtime = runtime + self.context = context + + # Load configuration + self.adapter_config = config or self._load_config_from_env() + + # Initialize providers + self._providers: Dict[str, Any] = {} + self._init_providers() + + # Create tool service + self.tool_service = WalletToolService( + config=self.adapter_config, + providers=self._providers, + ) + + # Track adapter state + self._running = False + + logger.info( + f"Wallet adapter initialized with providers: {list(self._providers.keys())}" + ) + + def _load_config_from_env(self) -> WalletAdapterConfig: + """Load configuration from environment variables.""" + x402_config = X402ProviderConfig( + enabled=os.getenv("WALLET_X402_ENABLED", "true").lower() == "true", + network=os.getenv("WALLET_X402_NETWORK", "base-sepolia"), + treasury_address=os.getenv("WALLET_X402_TREASURY_ADDRESS"), + facilitator_url=os.getenv( + "WALLET_X402_FACILITATOR_URL", "https://x402.org/facilitator" + ), + ) + + chapa_secret = os.getenv("WALLET_CHAPA_SECRET_KEY") + chapa_config = ChapaProviderConfig( + enabled=os.getenv("WALLET_CHAPA_ENABLED", "true").lower() == "true", + secret_key=chapa_secret if chapa_secret else None, + callback_base_url=os.getenv("WALLET_CHAPA_CALLBACK_URL"), + merchant_name=os.getenv("WALLET_CHAPA_MERCHANT_NAME", "CIRIS"), + ) + + return WalletAdapterConfig( + x402=x402_config, + chapa=chapa_config, + default_provider=os.getenv("WALLET_DEFAULT_PROVIDER", "x402"), + ) + + def _init_providers(self) -> None: + """Initialize enabled wallet providers.""" + # Initialize x402 provider if enabled + if self.adapter_config.x402.enabled: + # Get Ed25519 public key from CIRISVerify (secure - key never leaves vault) + public_key, signing_callback = self._get_ciris_verify_key() + + x402_provider = X402Provider( + config=self.adapter_config.x402, + ed25519_public_key=public_key, + ed25519_seed=self._get_test_seed(), # Fallback for testing only + signing_callback=signing_callback, + ) + self._providers["x402"] = x402_provider + + if public_key: + logger.info("x402 provider configured with CIRISVerify key") + else: + logger.info("x402 provider configured (no CIRISVerify - test mode)") + + # Initialize Chapa provider if enabled + if self.adapter_config.chapa.enabled: + chapa_provider = ChapaProvider(config=self.adapter_config.chapa) + self._providers["chapa"] = chapa_provider + logger.info("Chapa provider configured") + + def _get_ciris_verify_key(self) -> tuple[Optional[bytes], Optional[Callable[[bytes], bytes]]]: + """ + Get Ed25519 public key and signing callback from CIRISVerify. + + Returns: + Tuple of (public_key, signing_callback) + - public_key: 32 bytes Ed25519 public key for address derivation + - signing_callback: Function to sign data via CIRISVerify + + The private key NEVER leaves the secure element. We only get: + 1. Public key (for deriving wallet address) + 2. Signing callback (for transaction signing) + """ + try: + # Import the singleton getter + from ciris_engine.logic.services.infrastructure.authentication.verifier_singleton import ( + get_verifier, + ) + + verifier = get_verifier() + + # Check if verifier has a key + if not verifier.has_key_sync(): + logger.warning("CIRISVerify has no key loaded") + return None, None + + # Get public key (safe - this is public information) + public_key = verifier.get_ed25519_public_key_sync() + logger.info(f"Got Ed25519 public key from CIRISVerify ({len(public_key)} bytes)") + + # Create signing callback that delegates to CIRISVerify + def signing_callback(data: bytes) -> bytes: + """Sign data using CIRISVerify (key never leaves secure element).""" + result: bytes = verifier.sign_ed25519_sync(data) + return result + + return public_key, signing_callback + + except ImportError as e: + logger.warning(f"CIRISVerify not available: {e}") + return None, None + except Exception as e: + logger.warning(f"Could not access CIRISVerify: {e}") + return None, None + + def _get_test_seed(self) -> Optional[bytes]: + """ + Get Ed25519 seed from environment (TESTING ONLY). + + In production, this should return None - use CIRISVerify instead. + """ + seed_hex = os.getenv("WALLET_ED25519_SEED") + if seed_hex: + try: + logger.warning("Using WALLET_ED25519_SEED from environment - TESTING ONLY") + return bytes.fromhex(seed_hex) + except ValueError: + logger.warning("Invalid WALLET_ED25519_SEED format") + return None + + def get_services_to_register(self) -> List[AdapterServiceRegistration]: + """ + Get services provided by this adapter. + + Returns TOOL service registration for wallet operations. + """ + registrations = [] + + # Register TOOL service for wallet operations + registrations.append( + AdapterServiceRegistration( + service_type=ServiceType.TOOL, + provider=self.tool_service, + priority=Priority.NORMAL, + capabilities=[ + "execute_tool", + "get_available_tools", + "send_money", + "request_money", + "get_statement", + "provider:wallet", + ], + ) + ) + + return registrations + + async def start(self) -> None: + """Start the Wallet adapter.""" + logger.info("Starting Wallet adapter") + + # Start tool service (which initializes providers) + await self.tool_service.start() + logger.info("WalletToolService started") + + self._running = True + logger.info("Wallet adapter started") + + async def stop(self) -> None: + """Stop the Wallet adapter.""" + logger.info("Stopping Wallet adapter") + self._running = False + + # Stop tool service (which cleans up providers) + await self.tool_service.stop() + + logger.info("Wallet adapter stopped") + + async def run_lifecycle(self, agent_task: Any) -> None: + """ + Run the adapter lifecycle. + + For Wallet, we just wait for the agent task to complete. + Payment operations are request-driven, not continuous. + """ + logger.info("Wallet adapter lifecycle started") + try: + await agent_task + except asyncio.CancelledError: + logger.info("Wallet adapter lifecycle cancelled") + finally: + await self.stop() + + def get_config(self) -> AdapterConfig: + """Get adapter configuration.""" + return AdapterConfig( + adapter_type="wallet", + enabled=self._running, + settings={ + "providers": list(self._providers.keys()), + "x402_network": self.adapter_config.x402.network, + "chapa_enabled": self.adapter_config.chapa.enabled, + }, + ) + + def get_status(self) -> RuntimeAdapterStatus: + """Get adapter status.""" + return RuntimeAdapterStatus( + adapter_id="wallet", + adapter_type="wallet", + is_running=self._running, + loaded_at=None, + error=None, + ) + + async def get_active_channels(self) -> List[Dict[str, Any]]: + """Get active channels (not applicable for wallet adapter).""" + return [] + + +# Export as Adapter for load_adapter() compatibility +Adapter = WalletAdapter diff --git a/ciris_adapters/wallet/config.py b/ciris_adapters/wallet/config.py new file mode 100644 index 000000000..346608e44 --- /dev/null +++ b/ciris_adapters/wallet/config.py @@ -0,0 +1,110 @@ +""" +Wallet Adapter Configuration. + +Configuration models for the wallet adapter and its providers. +""" + +from decimal import Decimal +from typing import Dict, Optional + +from pydantic import BaseModel, Field, SecretStr + + +class SpendingLimits(BaseModel): + """Spending limits for wallet operations.""" + + max_transaction: Decimal = Field( + default=Decimal("100.00"), + description="Maximum amount per transaction", + ) + daily_limit: Decimal = Field( + default=Decimal("1000.00"), + description="Maximum daily spending", + ) + session_limit: Decimal = Field( + default=Decimal("500.00"), + description="Maximum spending per session", + ) + + +class X402ProviderConfig(BaseModel): + """Configuration for the x402/USDC provider.""" + + enabled: bool = Field(default=True, description="Whether x402 provider is enabled") + network: str = Field( + default="base-sepolia", + description="EVM network (base-mainnet or base-sepolia)", + ) + treasury_address: Optional[str] = Field( + None, + description="Treasury wallet address for receiving payments", + ) + facilitator_url: str = Field( + default="https://x402.org/facilitator", + description="x402 facilitator URL", + ) + # Key derivation uses CIRISVerify Ed25519 key, not stored here + + +class ChapaProviderConfig(BaseModel): + """Configuration for the Chapa/ETB provider.""" + + enabled: bool = Field(default=True, description="Whether Chapa provider is enabled") + secret_key: Optional[SecretStr] = Field( + None, + description="Chapa API secret key", + ) + callback_base_url: Optional[str] = Field( + None, + description="Base URL for payment callbacks", + ) + merchant_name: str = Field( + default="CIRIS", + description="Merchant name shown to payers", + ) + + +class WalletAdapterConfig(BaseModel): + """Configuration for the WalletAdapter.""" + + # Provider configurations + x402: X402ProviderConfig = Field( + default_factory=X402ProviderConfig, + description="x402/USDC provider configuration", + ) + chapa: ChapaProviderConfig = Field( + default_factory=ChapaProviderConfig, + description="Chapa/ETB provider configuration", + ) + + # Currency to provider mapping + currency_providers: Dict[str, str] = Field( + default={ + "USDC": "x402", + "ETH": "x402", + "ETB": "chapa", + "KES": "mpesa", # Future + "NGN": "flutterwave", # Future + }, + description="Default provider for each currency", + ) + + # Spending limits + spending_limits: SpendingLimits = Field( + default_factory=SpendingLimits, + description="Spending limits for outbound payments", + ) + + # Attestation requirements (for x402 crypto operations) + min_attestation_level: int = Field( + default=3, + ge=0, + le=5, + description="Minimum CIRISVerify attestation level for transactions", + ) + + # Default provider when currency doesn't specify + default_provider: str = Field( + default="x402", + description="Default provider when currency not in mapping", + ) diff --git a/ciris_adapters/wallet/manifest.json b/ciris_adapters/wallet/manifest.json new file mode 100644 index 000000000..12f4d27e7 --- /dev/null +++ b/ciris_adapters/wallet/manifest.json @@ -0,0 +1,88 @@ +{ + "module": { + "name": "wallet", + "version": "1.0.0", + "description": "Generic money tools supporting crypto (x402/USDC) and fiat (Chapa/ETB) providers", + "author": "CIRIS Team" + }, + "services": [ + { + "type": "TOOL", + "priority": "NORMAL", + "class": "wallet.tool_service.WalletToolService", + "capabilities": [ + "tool:wallet", + "tool:wallet:send_money", + "tool:wallet:request_money", + "tool:wallet:get_statement", + "execute_tool", + "get_available_tools", + "context_enrichment" + ] + } + ], + "capabilities": [ + "tool:wallet", + "tool:wallet:send_money", + "tool:wallet:request_money", + "tool:wallet:get_statement", + "provider:x402", + "provider:chapa", + "currency:USDC", + "currency:ETB" + ], + "dependencies": { + "protocols": [ + "ciris_engine.protocols.services.ToolService" + ], + "schemas": [ + "ciris_engine.schemas.adapters.tools" + ], + "external_packages": [] + }, + "exports": { + "tool_service": "wallet.tool_service.WalletToolService", + "adapter": "wallet.adapter.WalletAdapter" + }, + "configuration": { + "x402_network": { + "type": "string", + "env": "WALLET_X402_NETWORK", + "default": "base-sepolia", + "description": "EVM network for x402 (base-mainnet or base-sepolia)" + }, + "x402_treasury_address": { + "type": "string", + "env": "WALLET_X402_TREASURY_ADDRESS", + "default": null, + "description": "Treasury wallet address for receiving payments" + }, + "chapa_secret_key": { + "type": "string", + "env": "WALLET_CHAPA_SECRET_KEY", + "default": null, + "description": "Chapa API secret key" + }, + "chapa_callback_url": { + "type": "string", + "env": "WALLET_CHAPA_CALLBACK_URL", + "default": null, + "description": "Base URL for Chapa payment callbacks" + }, + "default_provider": { + "type": "string", + "env": "WALLET_DEFAULT_PROVIDER", + "default": "x402", + "description": "Default provider when currency doesn't specify" + } + }, + "metadata": { + "context_enrichment_tools": ["get_statement"], + "requires_approval_tools": ["send_money"], + "supported_currencies": ["USDC", "ETH", "ETB"], + "supported_providers": ["x402", "chapa"], + "data_source": true, + "contains_pii": true, + "gdpr_applicable": true + } +} diff --git a/ciris_adapters/wallet/providers/__init__.py b/ciris_adapters/wallet/providers/__init__.py new file mode 100644 index 000000000..15a6df72c --- /dev/null +++ b/ciris_adapters/wallet/providers/__init__.py @@ -0,0 +1,13 @@ +""" +Wallet Providers. + +Provider implementations for different payment rails: +- x402: USDC on Base L2 via x402 protocol +- chapa: Ethiopian Birr via Chapa gateway +- (future) mpesa: Kenyan Shilling via M-Pesa +- (future) flutterwave: Nigerian Naira via Flutterwave +""" + +from .base import WalletProvider + +__all__ = ["WalletProvider"] diff --git a/ciris_adapters/wallet/providers/base.py b/ciris_adapters/wallet/providers/base.py new file mode 100644 index 000000000..06b1c9fc1 --- /dev/null +++ b/ciris_adapters/wallet/providers/base.py @@ -0,0 +1,197 @@ +""" +Wallet Provider Protocol. + +Base protocol that all wallet providers must implement. +This enables provider-agnostic money operations across crypto and fiat rails. +""" + +from abc import ABC, abstractmethod +from datetime import datetime +from decimal import Decimal +from typing import List, Optional + +from ..schemas import ( + AccountDetails, + Balance, + PaymentRequest, + PaymentVerification, + Transaction, + TransactionResult, +) + + +class WalletProvider(ABC): + """ + Abstract base class for wallet providers. + + All wallet providers (x402, Chapa, M-Pesa, etc.) must implement this interface. + This enables the WalletToolService to work with any provider through the same + generic tools (send_money, request_money, get_statement). + + Providers handle the specifics of their payment rail: + - x402: USDC transactions on Base L2 via x402 protocol + - Chapa: ETB transactions via Telebirr, CBE Birr, bank transfer + - M-Pesa: KES transactions via Safaricom (future) + - etc. + """ + + @property + @abstractmethod + def provider_id(self) -> str: + """ + Unique provider identifier. + + Examples: 'x402', 'chapa', 'mpesa', 'flutterwave' + """ + ... + + @property + @abstractmethod + def supported_currencies(self) -> List[str]: + """ + List of supported currency codes. + + Examples: + - x402: ['USDC', 'ETH'] + - Chapa: ['ETB'] + - M-Pesa: ['KES'] + """ + ... + + @abstractmethod + async def initialize(self) -> bool: + """ + Initialize the provider. + + Called during adapter startup. Should establish connections, + verify credentials, etc. + + Returns: + True if initialization successful, False otherwise. + """ + ... + + @abstractmethod + async def cleanup(self) -> None: + """ + Cleanup provider resources. + + Called during adapter shutdown. Should close connections, + flush pending operations, etc. + """ + ... + + @abstractmethod + async def send( + self, + recipient: str, + amount: Decimal, + currency: str, + memo: Optional[str] = None, + **kwargs: object, + ) -> TransactionResult: + """ + Send money to a recipient. + + Args: + recipient: Recipient address/phone/username (format depends on provider) + amount: Amount to send + currency: Currency code (must be in supported_currencies) + memo: Optional transaction memo/description + **kwargs: Provider-specific parameters + + Returns: + TransactionResult with success status, transaction ID, etc. + + Raises: + ValueError: If currency not supported or parameters invalid + """ + ... + + @abstractmethod + async def request( + self, + amount: Decimal, + currency: str, + description: str, + expires_at: Optional[datetime] = None, + callback_url: Optional[str] = None, + **kwargs: object, + ) -> PaymentRequest: + """ + Create a payment request/invoice. + + Args: + amount: Requested amount + currency: Currency code + description: What the payment is for + expires_at: Optional expiration timestamp + callback_url: Optional webhook URL for payment notification + **kwargs: Provider-specific parameters + + Returns: + PaymentRequest with request ID, checkout URL, etc. + """ + ... + + @abstractmethod + async def get_balance(self, currency: Optional[str] = None) -> Balance: + """ + Get account balance. + + Args: + currency: Specific currency to query (None for primary currency) + + Returns: + Balance with available, pending, and total amounts. + """ + ... + + @abstractmethod + async def get_history( + self, + limit: int = 50, + offset: int = 0, + currency: Optional[str] = None, + ) -> List[Transaction]: + """ + Get transaction history. + + Args: + limit: Maximum transactions to return + offset: Skip this many transactions (pagination) + currency: Filter by currency (None for all) + + Returns: + List of Transaction objects, most recent first. + """ + ... + + @abstractmethod + async def get_account_details(self) -> AccountDetails: + """ + Get account details. + + Returns: + AccountDetails with address/phone, network, attestation level, etc. + """ + ... + + @abstractmethod + async def verify_payment(self, payment_ref: str) -> PaymentVerification: + """ + Verify a payment by reference ID. + + Used to check if a payment request has been fulfilled. + + Args: + payment_ref: Payment reference (request_id or transaction_id) + + Returns: + PaymentVerification with verified status and transaction details. + """ + ... + + def supports_currency(self, currency: str) -> bool: + """Check if this provider supports a currency.""" + return currency.upper() in [c.upper() for c in self.supported_currencies] diff --git a/ciris_adapters/wallet/providers/chapa_provider.py b/ciris_adapters/wallet/providers/chapa_provider.py new file mode 100644 index 000000000..7b3a57184 --- /dev/null +++ b/ciris_adapters/wallet/providers/chapa_provider.py @@ -0,0 +1,428 @@ +""" +Chapa Wallet Provider. + +Ethiopian Birr (ETB) payments via Chapa payment gateway. +Supports Telebirr, CBE Birr, and bank transfers. + +Dependencies: +- chapa # Chapa Python SDK (pip install chapa) +""" + +import logging +import uuid +from datetime import datetime, timezone +from decimal import Decimal +from typing import Any, Dict, List, Optional + +from ..config import ChapaProviderConfig +from ..schemas import ( + AccountDetails, + Balance, + PaymentRequest, + PaymentRequestStatus, + PaymentVerification, + Transaction, + TransactionResult, + TransactionStatus, + TransactionType, +) +from .base import WalletProvider + +logger = logging.getLogger(__name__) + + +class ChapaProvider(WalletProvider): + """ + Chapa provider for Ethiopian Birr (ETB) payments. + + Key features: + - Telebirr integration (mobile money) + - CBE Birr integration + - Bank transfer support + - 24-hour settlement to Ethiopian bank account + + Chapa handles all local payment methods through a unified API. + Users pay via their preferred method (Telebirr, bank, etc.) + and CIRIS receives ETB in the merchant account. + """ + + SUPPORTED_CURRENCIES = ["ETB"] + + def __init__(self, config: ChapaProviderConfig) -> None: + """ + Initialize the Chapa provider. + + Args: + config: Provider configuration with API key and callback URL + """ + self.config = config + self._initialized = False + self._chapa_client: Optional[Any] = None + + # Track pending requests and transactions (in-memory) + self._pending_requests: Dict[str, PaymentRequest] = {} + self._transactions: List[Transaction] = [] + self._balance = Balance( + currency="ETB", + available=Decimal("0"), + pending=Decimal("0"), + total=Decimal("0"), + ) + + logger.info("ChapaProvider created") + + @property + def provider_id(self) -> str: + return "chapa" + + @property + def supported_currencies(self) -> List[str]: + return self.SUPPORTED_CURRENCIES + + async def initialize(self) -> bool: + """Initialize the provider with Chapa SDK.""" + logger.info("Initializing ChapaProvider") + + if not self.config.secret_key: + logger.warning("Chapa secret key not configured") + self._initialized = True # Allow to run without key for testing + return True + + try: + # Try to import and initialize Chapa SDK + # from chapa import AsyncChapa + # self._chapa_client = AsyncChapa(self.config.secret_key.get_secret_value()) + logger.info("Chapa SDK would be initialized here") + self._initialized = True + return True + except ImportError: + logger.warning("Chapa SDK not installed - running in simulation mode") + self._initialized = True + return True + except Exception as e: + logger.error(f"Failed to initialize Chapa: {e}") + return False + + async def cleanup(self) -> None: + """Cleanup provider resources.""" + logger.info("Cleaning up ChapaProvider") + self._chapa_client = None + self._initialized = False + + async def send( + self, + recipient: str, + amount: Decimal, + currency: str, + memo: Optional[str] = None, + **kwargs: object, + ) -> TransactionResult: + """ + Send ETB to a recipient. + + For Chapa, sends are typically bank transfers or mobile money transfers. + Recipient should be a phone number or bank account. + + Args: + recipient: Phone number (+251...) or bank account + amount: Amount in ETB + currency: Must be ETB + memo: Optional transfer description + + Returns: + TransactionResult with transfer details. + """ + currency = currency.upper() + if currency != "ETB": + return TransactionResult( + success=False, + provider=self.provider_id, + amount=amount, + currency=currency, + recipient=recipient, + error=f"Chapa only supports ETB, got: {currency}", + ) + + # Validate recipient format (Ethiopian phone or simplistic check) + is_phone = recipient.startswith("+251") or recipient.startswith("0") + if not is_phone and not recipient.isdigit(): + logger.warning(f"Unusual recipient format: {recipient}") + + # TODO: Implement actual Chapa transfer + # Chapa supports: + # - chapa.transfer(account_name, account_number, amount, reference, bank_code) + # - For mobile money, different flow + + logger.info(f"[CHAPA] Sending {amount} ETB to {recipient} (memo: {memo})") + + # Generate transaction reference + tx_ref = f"CIRIS-{uuid.uuid4().hex[:8].upper()}" + timestamp = datetime.now(timezone.utc) + + # Record transaction + transaction = Transaction( + transaction_id=tx_ref, + provider=self.provider_id, + type=TransactionType.SEND, + status=TransactionStatus.PENDING, # Chapa transfers are async + amount=-amount, + currency=currency, + recipient=recipient, + sender="CIRIS", + memo=memo, + timestamp=timestamp, + fees={"provider_fee": amount * Decimal("0.015")}, # 1.5% Chapa fee + metadata={"tx_ref": tx_ref}, + ) + self._transactions.insert(0, transaction) + + # Update balance + fee = amount * Decimal("0.015") + self._balance.available -= (amount + fee) + self._balance.total = self._balance.available + self._balance.pending + + return TransactionResult( + success=True, + transaction_id=tx_ref, + provider=self.provider_id, + amount=amount, + currency=currency, + recipient=recipient, + timestamp=timestamp, + fees={"provider_fee": fee}, + confirmation={ + "tx_ref": tx_ref, + "status": "pending", + "settlement": "24_hours", + }, + ) + + async def request( + self, + amount: Decimal, + currency: str, + description: str, + expires_at: Optional[datetime] = None, + callback_url: Optional[str] = None, + **kwargs: object, + ) -> PaymentRequest: + """ + Create a payment request via Chapa checkout. + + Returns a checkout URL where the payer can complete payment + via Telebirr, CBE Birr, or bank transfer. + + Args: + amount: Amount in ETB + currency: Must be ETB + description: What the payment is for + expires_at: Optional expiration + callback_url: Webhook URL for payment notification + + Returns: + PaymentRequest with Chapa checkout URL. + """ + currency = currency.upper() + tx_ref = f"CIRIS-{uuid.uuid4().hex[:8].upper()}" + + # Determine callback URL + final_callback = callback_url or self.config.callback_base_url + if final_callback and not final_callback.endswith("/"): + final_callback = f"{final_callback}/wallet/chapa/callback" + + # TODO: Create actual Chapa checkout + # response = await self._chapa_client.initialize( + # amount=float(amount), + # currency="ETB", + # tx_ref=tx_ref, + # callback_url=final_callback, + # return_url=final_callback, + # customization={ + # "title": self.config.merchant_name, + # "description": description, + # } + # ) + # checkout_url = response["data"]["checkout_url"] + + # Simulated checkout URL for testing + checkout_url = f"https://checkout.chapa.co/checkout/payment/{tx_ref}" + + request = PaymentRequest( + request_id=tx_ref, + provider=self.provider_id, + amount=amount, + currency=currency, + description=description, + status=PaymentRequestStatus.PENDING, + checkout_url=checkout_url, + created_at=datetime.now(timezone.utc), + expires_at=expires_at, + metadata={ + "tx_ref": tx_ref, + "callback_url": final_callback, + "merchant": self.config.merchant_name, + }, + ) + + self._pending_requests[tx_ref] = request + logger.info(f"[CHAPA] Created payment request: {tx_ref} for {amount} ETB") + logger.info(f"[CHAPA] Checkout URL: {checkout_url}") + + return request + + async def get_balance(self, currency: Optional[str] = None) -> Balance: + """ + Get account balance. + + Note: Chapa doesn't provide real-time balance API. + Balance is tracked from settlements. + """ + # TODO: Could query Chapa dashboard API if available + return self._balance + + async def get_history( + self, + limit: int = 50, + offset: int = 0, + currency: Optional[str] = None, + ) -> List[Transaction]: + """Get transaction history.""" + # Filter by currency if specified (though Chapa only does ETB) + transactions = self._transactions + if currency: + currency = currency.upper() + transactions = [t for t in transactions if t.currency == currency] + + return transactions[offset : offset + limit] + + async def get_account_details(self) -> AccountDetails: + """Get account details.""" + return AccountDetails( + provider=self.provider_id, + currency="ETB", + account_id=self.config.merchant_name, + metadata={ + "merchant_name": self.config.merchant_name, + "supported_methods": ["telebirr", "cbe_birr", "bank_transfer"], + "settlement_time": "24_hours", + }, + ) + + async def verify_payment(self, payment_ref: str) -> PaymentVerification: + """ + Verify a payment by reference ID. + + Chapa provides verification via: + - Callback webhook (preferred) + - Polling verify endpoint + """ + # Check pending requests first + if payment_ref in self._pending_requests: + request = self._pending_requests[payment_ref] + + # TODO: Query Chapa verification endpoint + # result = await self._chapa_client.verify(payment_ref) + # if result["status"] == "success": + # request.status = PaymentRequestStatus.PAID + # request.paid_at = datetime.now(timezone.utc) + + return PaymentVerification( + verified=request.status == PaymentRequestStatus.PAID, + status=( + TransactionStatus.CONFIRMED + if request.status == PaymentRequestStatus.PAID + else TransactionStatus.PENDING + ), + transaction_id=request.transaction_id, + amount=request.amount, + currency=request.currency, + timestamp=request.paid_at, + ) + + # Check transaction history + for tx in self._transactions: + if tx.transaction_id == payment_ref: + return PaymentVerification( + verified=tx.status == TransactionStatus.CONFIRMED, + status=tx.status, + transaction_id=tx.transaction_id, + amount=abs(tx.amount), + currency=tx.currency, + timestamp=tx.timestamp, + ) + + return PaymentVerification( + verified=False, + status=TransactionStatus.FAILED, + error=f"Payment reference not found: {payment_ref}", + ) + + async def handle_callback(self, payload: Dict[str, Any]) -> bool: + """ + Handle Chapa webhook callback. + + Called when Chapa notifies us of a payment completion. + + Args: + payload: Webhook payload from Chapa + + Returns: + True if callback processed successfully. + """ + tx_ref = payload.get("tx_ref") or payload.get("reference") + status = payload.get("status", "").lower() + + logger.info(f"[CHAPA] Callback received: tx_ref={tx_ref}, status={status}") + + if not tx_ref: + logger.error("[CHAPA] Callback missing tx_ref") + return False + + if tx_ref not in self._pending_requests: + logger.warning(f"[CHAPA] Unknown tx_ref in callback: {tx_ref}") + return False + + request = self._pending_requests[tx_ref] + + if status == "success": + request.status = PaymentRequestStatus.PAID + request.paid_at = datetime.now(timezone.utc) + request.transaction_id = payload.get("transaction_id", tx_ref) + + # Record as incoming transaction + transaction = Transaction( + transaction_id=request.transaction_id or tx_ref, + provider=self.provider_id, + type=TransactionType.RECEIVE, + status=TransactionStatus.CONFIRMED, + amount=request.amount, + currency=request.currency, + memo=request.description, + timestamp=request.paid_at, + metadata=payload, + ) + self._transactions.insert(0, transaction) + + # Update balance + self._balance.pending += request.amount + self._balance.total = self._balance.available + self._balance.pending + + logger.info(f"[CHAPA] Payment confirmed: {tx_ref} - {request.amount} ETB") + return True + + elif status in ("failed", "cancelled"): + request.status = PaymentRequestStatus.CANCELLED + logger.info(f"[CHAPA] Payment {status}: {tx_ref}") + return True + + return False + + def set_balance(self, available: Decimal, pending: Decimal = Decimal("0")) -> None: + """Set balance (for testing or after settlement).""" + self._balance = Balance( + currency="ETB", + available=available, + pending=pending, + total=available + pending, + ) + logger.info(f"[CHAPA] Balance set: {available} ETB available, {pending} pending") diff --git a/ciris_adapters/wallet/providers/x402_provider.py b/ciris_adapters/wallet/providers/x402_provider.py new file mode 100644 index 000000000..e14c38e96 --- /dev/null +++ b/ciris_adapters/wallet/providers/x402_provider.py @@ -0,0 +1,448 @@ +""" +x402 Wallet Provider. + +USDC payments on Base L2 via the x402 HTTP payment protocol. +Uses deterministic wallet derivation from CIRISVerify Ed25519 signing key. + +Key security feature: The private key NEVER leaves CIRISVerify's secure element. +We derive the EVM address from the public key, and delegate all signing +operations back to CIRISVerify. + +Dependencies: +- x402[fastapi,httpx,evm] # Core x402 protocol (future) +- eth-keys # Ethereum key handling +""" + +import hashlib +import hmac +import logging +import uuid +from datetime import datetime, timezone +from decimal import Decimal +from typing import Any, Callable, Dict, List, Optional + +from ..config import X402ProviderConfig +from ..schemas import ( + AccountDetails, + Balance, + PaymentRequest, + PaymentRequestStatus, + PaymentVerification, + Transaction, + TransactionResult, + TransactionStatus, + TransactionType, +) +from .base import WalletProvider + +logger = logging.getLogger(__name__) + +# Type alias for signing callback +SigningCallback = Callable[[bytes], bytes] + + +class X402Provider(WalletProvider): + """ + x402 provider for USDC payments on Base L2. + + Key features: + - Wallet address derived from CIRISVerify Ed25519 PUBLIC key + - Private key NEVER leaves secure element + - Signing delegated to CIRISVerify via callback + - Attestation-gated spending authority + - ~400ms finality on Base L2 + + For receiving: Only needs public key (address derivation) + For sending: Needs signing callback to CIRISVerify + """ + + DOMAIN_SEPARATOR = b"CIRIS-x402-wallet-v1" + SUPPORTED_CURRENCIES = ["USDC", "ETH"] + + def __init__( + self, + config: X402ProviderConfig, + ed25519_public_key: Optional[bytes] = None, + ed25519_seed: Optional[bytes] = None, + signing_callback: Optional[SigningCallback] = None, + ) -> None: + """ + Initialize the x402 provider. + + Args: + config: Provider configuration + ed25519_public_key: Ed25519 public key (32 bytes) for address derivation. + This is the preferred method - key stays in secure element. + ed25519_seed: Optional Ed25519 seed (32 bytes) for testing only. + In production, use public_key + signing_callback instead. + signing_callback: Callback to CIRISVerify for signing operations. + Required for send operations if using public_key mode. + """ + self.config = config + self._ed25519_public_key = ed25519_public_key + self._ed25519_seed = ed25519_seed # Only for testing + self._signing_callback = signing_callback + self._evm_address: Optional[str] = None + self._initialized = False + + # Track pending transactions and balances (in-memory for now) + self._pending_requests: Dict[str, PaymentRequest] = {} + self._transactions: List[Transaction] = [] + self._balance = Balance( + currency="USDC", + available=Decimal("0"), + pending=Decimal("0"), + total=Decimal("0"), + ) + + logger.info( + f"X402Provider created for network: {config.network}" + ) + + @property + def provider_id(self) -> str: + return "x402" + + @property + def supported_currencies(self) -> List[str]: + return self.SUPPORTED_CURRENCIES + + def _derive_evm_address_from_pubkey(self, public_key: bytes) -> str: + """ + Derive EVM wallet address from Ed25519 PUBLIC key. + + This is the secure method - private key never leaves CIRISVerify. + Uses HKDF-SHA256 with domain separation to create a deterministic + mapping from Ed25519 public key to EVM address. + + Args: + public_key: Ed25519 public key (32 bytes) + + Returns: + EVM address (0x...) + """ + # HKDF with public key as input keying material + # This creates a deterministic, one-way mapping + prk = hmac.new(self.DOMAIN_SEPARATOR, public_key, hashlib.sha256).digest() + info = b"evm-address-from-ed25519-pubkey" + address_bytes = hmac.new(prk, info + b"\x01", hashlib.sha256).digest()[:20] + + # Convert to checksum address + address_hex = address_bytes.hex() + return self._to_checksum_address(address_hex) + + def _to_checksum_address(self, address_hex: str) -> str: + """Convert raw hex to EIP-55 checksum address.""" + address_hex = address_hex.lower().replace("0x", "") + hash_bytes = hashlib.sha3_256(address_hex.encode()).digest() + + checksum_address = "0x" + for i, char in enumerate(address_hex): + if char in "0123456789": + checksum_address += char + elif hash_bytes[i // 2] >> (4 * (1 - i % 2)) & 0xF >= 8: + checksum_address += char.upper() + else: + checksum_address += char + return checksum_address + + def _derive_evm_address_from_seed(self, seed: bytes) -> str: + """ + Derive EVM wallet address from Ed25519 seed (TESTING ONLY). + + In production, use _derive_evm_address_from_pubkey instead. + This method is only for testing when we have direct access to the seed. + """ + try: + from eth_keys import keys as eth_keys + + # HKDF-extract + prk = hmac.new(self.DOMAIN_SEPARATOR, seed, hashlib.sha256).digest() + + # HKDF-expand to 32 bytes (secp256k1 private key) + info = b"evm-secp256k1-signing-key" + okm = hmac.new(prk, info + b"\x01", hashlib.sha256).digest() + + # Derive EVM address from secp256k1 private key + private_key = eth_keys.PrivateKey(okm) + address: str = private_key.public_key.to_checksum_address() + return address + + except ImportError: + logger.warning("eth_keys not installed, using HKDF-based address") + # Fall back to HKDF-based address derivation + return self._derive_evm_address_from_pubkey(seed) + + async def initialize(self) -> bool: + """Initialize the provider and derive wallet address.""" + logger.info(f"Initializing X402Provider on {self.config.network}") + + # Priority: public_key (secure) > seed (testing only) + if self._ed25519_public_key: + self._evm_address = self._derive_evm_address_from_pubkey(self._ed25519_public_key) + logger.info(f"Derived wallet address from public key: {self._evm_address}") + if not self._signing_callback: + logger.warning("No signing callback - send operations will fail (receive-only mode)") + elif self._ed25519_seed: + logger.warning("Using Ed25519 seed directly - this should only be used for testing!") + self._evm_address = self._derive_evm_address_from_seed(self._ed25519_seed) + logger.info(f"Derived wallet address from seed: {self._evm_address}") + else: + logger.warning("No Ed25519 key provided - wallet will use placeholder address") + # Generate a deterministic placeholder for testing + placeholder = hashlib.sha256(b"CIRIS-no-key-placeholder").digest()[:20] + self._evm_address = self._to_checksum_address(placeholder.hex()) + logger.info(f"Using placeholder address: {self._evm_address}") + + self._initialized = True + return True + + async def cleanup(self) -> None: + """Cleanup provider resources.""" + logger.info("Cleaning up X402Provider") + self._initialized = False + + async def send( + self, + recipient: str, + amount: Decimal, + currency: str, + memo: Optional[str] = None, + **kwargs: object, + ) -> TransactionResult: + """ + Send USDC to a recipient. + + Args: + recipient: EVM address (0x...) + amount: Amount in USDC (or ETH) + currency: USDC or ETH + memo: Optional transaction memo + + Returns: + TransactionResult with transaction ID and confirmation. + """ + currency = currency.upper() + if currency not in self.SUPPORTED_CURRENCIES: + return TransactionResult( + success=False, + provider=self.provider_id, + amount=amount, + currency=currency, + recipient=recipient, + error=f"Unsupported currency: {currency}. Supported: {self.SUPPORTED_CURRENCIES}", + ) + + if not self._evm_address: + return TransactionResult( + success=False, + provider=self.provider_id, + amount=amount, + currency=currency, + recipient=recipient, + error="Wallet not initialized (no Ed25519 key)", + ) + + # Check signing capability (need either callback or seed) + if not self._signing_callback and not self._ed25519_seed: + return TransactionResult( + success=False, + provider=self.provider_id, + amount=amount, + currency=currency, + recipient=recipient, + error="Cannot send: no signing capability (receive-only mode). " + "Ensure CIRISVerify is initialized with a key.", + ) + + # Validate recipient address format + if not recipient.startswith("0x") or len(recipient) != 42: + return TransactionResult( + success=False, + provider=self.provider_id, + amount=amount, + currency=currency, + recipient=recipient, + error=f"Invalid EVM address format: {recipient}", + ) + + # TODO: Implement actual x402 transaction using signing callback + # When implemented: + # 1. Build EIP-712 typed data for the transfer + # 2. Sign via self._signing_callback(typed_data_hash) if available + # 3. Submit to x402 facilitator + # + # For now, simulate transaction for testing + signing_mode = "CIRISVerify callback" if self._signing_callback else "local seed (testing)" + logger.info( + f"[X402] Sending {amount} {currency} to {recipient}" + f" (memo: {memo}, signing: {signing_mode})" + ) + + # Generate transaction ID + tx_id = f"0x{uuid.uuid4().hex}" + timestamp = datetime.now(timezone.utc) + + # Record transaction + transaction = Transaction( + transaction_id=tx_id, + provider=self.provider_id, + type=TransactionType.SEND, + status=TransactionStatus.PENDING, # Would be CONFIRMED after blockchain confirmation + amount=-amount, # Negative for sends + currency=currency, + recipient=recipient, + sender=self._evm_address, + memo=memo, + timestamp=timestamp, + fees={"network_fee": Decimal("0.001")}, + confirmation={ + "network": self.config.network, + "tx_hash": tx_id, + }, + ) + self._transactions.insert(0, transaction) + + # Update balance + self._balance.available -= amount + self._balance.total = self._balance.available + self._balance.pending + + return TransactionResult( + success=True, + transaction_id=tx_id, + provider=self.provider_id, + amount=amount, + currency=currency, + recipient=recipient, + timestamp=timestamp, + fees={"network_fee": Decimal("0.001")}, + confirmation={ + "network": self.config.network, + "tx_hash": tx_id, + "block_number": None, # Would be set after confirmation + }, + ) + + async def request( + self, + amount: Decimal, + currency: str, + description: str, + expires_at: Optional[datetime] = None, + callback_url: Optional[str] = None, + **kwargs: object, + ) -> PaymentRequest: + """ + Create a payment request. + + For x402, this generates the payment details that would be + returned in a 402 response body. + """ + currency = currency.upper() + request_id = f"req_{uuid.uuid4().hex[:12]}" + + # x402 payment requests don't have checkout URLs + # Payment is made via X-PAYMENT header + request = PaymentRequest( + request_id=request_id, + provider=self.provider_id, + amount=amount, + currency=currency, + description=description, + status=PaymentRequestStatus.PENDING, + checkout_url=None, # x402 uses in-band payment + created_at=datetime.now(timezone.utc), + expires_at=expires_at, + metadata={ + "network": self.config.network, + "pay_to": self.config.treasury_address or self._evm_address, + "scheme": "exact", + }, + ) + + self._pending_requests[request_id] = request + logger.info(f"[X402] Created payment request: {request_id} for {amount} {currency}") + + return request + + async def get_balance(self, currency: Optional[str] = None) -> Balance: + """Get account balance.""" + # TODO: Query actual balance from Base network + # For now return tracked balance + return self._balance + + async def get_history( + self, + limit: int = 50, + offset: int = 0, + currency: Optional[str] = None, + ) -> List[Transaction]: + """Get transaction history.""" + # Filter by currency if specified + transactions = self._transactions + if currency: + currency = currency.upper() + transactions = [t for t in transactions if t.currency == currency] + + # Apply pagination + return transactions[offset : offset + limit] + + async def get_account_details(self) -> AccountDetails: + """Get account details.""" + return AccountDetails( + provider=self.provider_id, + currency="USDC", + address=self._evm_address, + network=self.config.network, + attestation_level=5, # TODO: Get from CIRISVerify + metadata={ + "chain_id": 8453 if "mainnet" in self.config.network else 84532, + "treasury": self.config.treasury_address, + }, + ) + + async def verify_payment(self, payment_ref: str) -> PaymentVerification: + """Verify a payment by reference ID.""" + # Check pending requests + if payment_ref in self._pending_requests: + request = self._pending_requests[payment_ref] + return PaymentVerification( + verified=request.status == PaymentRequestStatus.PAID, + status=( + TransactionStatus.CONFIRMED + if request.status == PaymentRequestStatus.PAID + else TransactionStatus.PENDING + ), + transaction_id=request.transaction_id, + amount=request.amount, + currency=request.currency, + timestamp=request.paid_at, + ) + + # Check transaction history + for tx in self._transactions: + if tx.transaction_id == payment_ref: + return PaymentVerification( + verified=tx.status == TransactionStatus.CONFIRMED, + status=tx.status, + transaction_id=tx.transaction_id, + amount=abs(tx.amount), + currency=tx.currency, + timestamp=tx.timestamp, + ) + + return PaymentVerification( + verified=False, + status=TransactionStatus.FAILED, + error=f"Payment reference not found: {payment_ref}", + ) + + def set_balance(self, available: Decimal, pending: Decimal = Decimal("0")) -> None: + """Set balance (for testing or funding).""" + self._balance = Balance( + currency="USDC", + available=available, + pending=pending, + total=available + pending, + ) + logger.info(f"[X402] Balance set: {available} USDC available, {pending} pending") diff --git a/ciris_adapters/wallet/schemas.py b/ciris_adapters/wallet/schemas.py new file mode 100644 index 000000000..53c528483 --- /dev/null +++ b/ciris_adapters/wallet/schemas.py @@ -0,0 +1,146 @@ +""" +Wallet Adapter Schemas. + +Pydantic models for wallet transactions, balances, and account details. +These are provider-agnostic - both crypto and fiat providers use the same models. +""" + +from datetime import datetime +from decimal import Decimal +from enum import Enum +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +class TransactionType(str, Enum): + """Type of wallet transaction.""" + + SEND = "send" + RECEIVE = "receive" + REQUEST = "request" + REFUND = "refund" + FEE = "fee" + + +class TransactionStatus(str, Enum): + """Status of a wallet transaction.""" + + PENDING = "pending" + CONFIRMED = "confirmed" + FAILED = "failed" + CANCELLED = "cancelled" + EXPIRED = "expired" + + +class PaymentRequestStatus(str, Enum): + """Status of a payment request.""" + + PENDING = "pending" + PAID = "paid" + EXPIRED = "expired" + CANCELLED = "cancelled" + + +class Transaction(BaseModel): + """A wallet transaction (send or receive).""" + + transaction_id: str = Field(..., description="Unique transaction identifier") + provider: str = Field(..., description="Provider that processed the transaction") + type: TransactionType = Field(..., description="Transaction type") + status: TransactionStatus = Field(..., description="Transaction status") + amount: Decimal = Field(..., description="Transaction amount (negative for sends)") + currency: str = Field(..., description="Currency code (USDC, ETB, KES, etc.)") + recipient: Optional[str] = Field(None, description="Recipient address/phone/username") + sender: Optional[str] = Field(None, description="Sender address/phone/username") + memo: Optional[str] = Field(None, description="Transaction memo/description") + timestamp: datetime = Field(..., description="Transaction timestamp") + fees: Optional[Dict[str, Decimal]] = Field(None, description="Fee breakdown") + confirmation: Optional[Dict[str, Any]] = Field( + None, description="Provider-specific confirmation data" + ) + metadata: Optional[Dict[str, Any]] = Field( + None, description="Additional provider-specific metadata" + ) + + +class TransactionResult(BaseModel): + """Result of a send_money operation.""" + + success: bool = Field(..., description="Whether the transaction succeeded") + transaction_id: Optional[str] = Field(None, description="Transaction ID if successful") + provider: str = Field(..., description="Provider used") + amount: Decimal = Field(..., description="Amount sent") + currency: str = Field(..., description="Currency code") + recipient: str = Field(..., description="Recipient address/phone") + timestamp: datetime = Field(default_factory=datetime.utcnow) + fees: Optional[Dict[str, Decimal]] = Field(None, description="Fee breakdown") + confirmation: Optional[Dict[str, Any]] = Field(None, description="Confirmation data") + error: Optional[str] = Field(None, description="Error message if failed") + + +class PaymentRequest(BaseModel): + """A payment request/invoice.""" + + request_id: str = Field(..., description="Unique request identifier") + provider: str = Field(..., description="Provider handling the request") + amount: Decimal = Field(..., description="Requested amount") + currency: str = Field(..., description="Currency code") + description: str = Field(..., description="What the payment is for") + status: PaymentRequestStatus = Field( + default=PaymentRequestStatus.PENDING, description="Request status" + ) + checkout_url: Optional[str] = Field(None, description="URL for payer to complete payment") + created_at: datetime = Field(default_factory=datetime.utcnow) + expires_at: Optional[datetime] = Field(None, description="When request expires") + paid_at: Optional[datetime] = Field(None, description="When payment was received") + transaction_id: Optional[str] = Field( + None, description="Transaction ID once paid" + ) + metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata") + + +class Balance(BaseModel): + """Account balance information.""" + + currency: str = Field(..., description="Currency code") + available: Decimal = Field(..., description="Available balance") + pending: Decimal = Field(default=Decimal("0"), description="Pending balance") + total: Decimal = Field(..., description="Total balance (available + pending)") + + +class AccountDetails(BaseModel): + """Account details for a provider.""" + + provider: str = Field(..., description="Provider identifier") + currency: str = Field(..., description="Primary currency") + address: Optional[str] = Field(None, description="Wallet address (crypto)") + phone: Optional[str] = Field(None, description="Phone number (mobile money)") + account_id: Optional[str] = Field(None, description="Account identifier") + network: Optional[str] = Field(None, description="Network (e.g., base-mainnet)") + attestation_level: Optional[int] = Field( + None, description="CIRISVerify attestation level (0-5)" + ) + metadata: Optional[Dict[str, Any]] = Field(None, description="Additional details") + + +class AccountStatement(BaseModel): + """Full account statement with balance, history, and details.""" + + provider: str = Field(..., description="Provider identifier") + currency: str = Field(..., description="Primary currency") + balance: Optional[Balance] = Field(None, description="Current balance") + details: Optional[AccountDetails] = Field(None, description="Account details") + history: Optional[List[Transaction]] = Field(None, description="Transaction history") + + +class PaymentVerification(BaseModel): + """Result of verifying a payment.""" + + verified: bool = Field(..., description="Whether payment is verified") + status: TransactionStatus = Field(..., description="Payment status") + transaction_id: Optional[str] = Field(None, description="Transaction ID if found") + amount: Optional[Decimal] = Field(None, description="Payment amount") + currency: Optional[str] = Field(None, description="Currency") + timestamp: Optional[datetime] = Field(None, description="Payment timestamp") + error: Optional[str] = Field(None, description="Error if verification failed") diff --git a/ciris_adapters/wallet/tool_service.py b/ciris_adapters/wallet/tool_service.py new file mode 100644 index 000000000..53827cbb3 --- /dev/null +++ b/ciris_adapters/wallet/tool_service.py @@ -0,0 +1,853 @@ +""" +Wallet Tool Service. + +Provides three generic money tools: +- send_money: Send money to a recipient +- request_money: Create a payment request/invoice +- get_statement: Get account balance, history, and details + +The implementation details (crypto vs fiat) are abstracted behind provider parameters. +""" + +import logging +import uuid +from datetime import datetime, timezone +from decimal import Decimal, InvalidOperation +from typing import Any, Dict, List, Optional + +from ciris_engine.schemas.adapters.tools import ( + ToolDMAGuidance, + ToolDocumentation, + ToolExecutionResult, + ToolExecutionStatus, + ToolGotcha, + ToolInfo, + ToolParameterSchema, + UsageExample, +) + +from .config import WalletAdapterConfig +from .providers.base import WalletProvider + +logger = logging.getLogger(__name__) + + +class WalletToolService: + """ + Tool service for wallet operations. + + Provides generic money tools that work across crypto and fiat providers. + The provider routing is handled automatically based on currency or explicit + provider_params. + """ + + TOOL_DEFINITIONS: Dict[str, ToolInfo] = { + "send_money": ToolInfo( + name="send_money", + description="Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + parameters=ToolParameterSchema( + type="object", + properties={ + "recipient": { + "type": "string", + "description": "Recipient address (0x... for crypto), phone (+251... for ETB), or username", + }, + "amount": { + "type": "number", + "description": "Amount to send", + }, + "currency": { + "type": "string", + "description": "Currency code: USDC (crypto), ETB (Ethiopia), KES (Kenya), etc.", + }, + "memo": { + "type": "string", + "description": "Optional transaction memo or description", + }, + "provider_params": { + "type": "object", + "description": "Optional provider-specific parameters", + "properties": { + "provider": { + "type": "string", + "enum": ["x402", "chapa", "mpesa", "auto"], + "description": "Explicit provider (auto-detected from currency if omitted)", + }, + }, + }, + }, + required=["recipient", "amount", "currency"], + ), + documentation=ToolDocumentation( + quick_start="Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + detailed_instructions=""" +# Send Money + +Send money to any recipient using the appropriate payment provider. + +## Currency → Provider Mapping +- **USDC, ETH** → x402 (crypto on Base L2) +- **ETB** → Chapa (Ethiopian Birr via Telebirr/CBE Birr) +- **KES** → M-Pesa (Kenyan Shilling) [future] +- **NGN** → Flutterwave (Nigerian Naira) [future] + +## Recipient Formats + +### Crypto (x402) +- EVM address: `0x742d35Cc6634C0532925a3b844Bc9e7595f...` +- ENS name: `vitalik.eth` (if supported) + +### Ethiopian (Chapa) +- Phone: `+251912345678` or `0912345678` +- Bank account requires additional provider_params + +### Kenyan (M-Pesa) [future] +- Phone: `+254712345678` + +## Transaction Flow +1. Currency determines provider (or use explicit provider_params.provider) +2. Provider validates recipient format +3. DMA pipeline evaluates the transaction (requires_approval=True) +4. Transaction is signed and submitted +5. Result includes transaction_id and confirmation + +## Spending Limits +- Per-transaction: $100 (configurable) +- Daily: $1000 (configurable) +- Session: $500 (configurable) +- Attestation level affects limits (x402 only) +""", + examples=[ + UsageExample( + title="Send USDC to contributor", + description="Pay a contributor in USDC", + code='{"recipient": "0x742d35Cc6634C0532925a3b844Bc9e7595f...", "amount": 50.00, "currency": "USDC", "memo": "March contribution"}', + ), + UsageExample( + title="Send ETB via Telebirr", + description="Pay in Ethiopian Birr", + code='{"recipient": "+251912345678", "amount": 1300.00, "currency": "ETB", "memo": "API usage fee"}', + ), + UsageExample( + title="Explicit provider", + description="Force a specific provider", + code='{"recipient": "0x1234...", "amount": 10, "currency": "USDC", "provider_params": {"provider": "x402"}}', + ), + ], + gotchas=[ + ToolGotcha( + title="Recipient format must match provider", + description="Crypto requires 0x addresses, mobile money requires phone numbers in E.164 format", + severity="error", + ), + ToolGotcha( + title="Transactions are irreversible", + description="Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + severity="warning", + ), + ToolGotcha( + title="Attestation level affects spending", + description="x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + severity="info", + ), + ], + ), + dma_guidance=ToolDMAGuidance( + requires_approval=True, + min_confidence=0.95, + when_not_to_use="When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + ethical_considerations="Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + prerequisite_actions=["Confirm recipient address/phone with user", "Verify amount"], + followup_actions=["Provide transaction ID to user", "Log transaction for audit"], + ), + ), + "request_money": ToolInfo( + name="request_money", + description="Create a payment request/invoice that others can pay", + parameters=ToolParameterSchema( + type="object", + properties={ + "amount": { + "type": "number", + "description": "Amount to request", + }, + "currency": { + "type": "string", + "description": "Currency code: USDC, ETB, KES, etc.", + }, + "description": { + "type": "string", + "description": "What the payment is for", + }, + "expires_at": { + "type": "string", + "description": "Optional ISO 8601 expiration timestamp", + }, + "provider_params": { + "type": "object", + "description": "Optional provider-specific parameters", + "properties": { + "provider": { + "type": "string", + "enum": ["x402", "chapa", "mpesa", "auto"], + "description": "Explicit provider", + }, + "callback_url": { + "type": "string", + "description": "Webhook URL for payment notification", + }, + }, + }, + }, + required=["amount", "currency", "description"], + ), + documentation=ToolDocumentation( + quick_start="Request payment: request_money amount=0.10 currency='USDC' description='API task'", + detailed_instructions=""" +# Request Money + +Create a payment request/invoice that can be paid by others. + +## Use Cases +- API endpoint payment (402 Payment Required) +- Service fees +- Contributor invoices +- Donation requests + +## Response +Returns a PaymentRequest with: +- **request_id**: Unique identifier +- **checkout_url**: URL for payer (fiat providers) +- **status**: pending/paid/expired +- **expires_at**: When request expires + +## Verification +Use get_statement to check if a request has been paid, +or implement webhook callbacks for real-time notification. + +## Provider Behavior + +### x402 (Crypto) +- Returns payment details for X-PAYMENT header +- No checkout_url (payment is in-band) +- Instant verification via blockchain + +### Chapa (Fiat) +- Returns checkout_url for Telebirr/CBE Birr/Bank +- Payer completes payment on Chapa's page +- Callback webhook on completion +""", + examples=[ + UsageExample( + title="Request for API task", + description="Create payment request for a single task", + code='{"amount": 0.10, "currency": "USDC", "description": "CIRIS API - Single task"}', + ), + UsageExample( + title="Request ETB with expiration", + description="Create Ethiopian payment request that expires", + code='{"amount": 13.00, "currency": "ETB", "description": "Ethical reasoning query", "expires_at": "2026-03-26T00:00:00Z"}', + ), + ], + gotchas=[ + ToolGotcha( + title="Checkout URL is provider-specific", + description="x402 doesn't use checkout URLs (payment is in HTTP header). Fiat providers return URLs.", + severity="info", + ), + ], + ), + dma_guidance=ToolDMAGuidance( + requires_approval=False, + min_confidence=0.8, + when_not_to_use="When creating excessive or spam payment requests", + ethical_considerations="Payment requests should be for legitimate services rendered", + ), + ), + "get_statement": ToolInfo( + name="get_statement", + description="Get account balance, transaction history, and account details", + parameters=ToolParameterSchema( + type="object", + properties={ + "include_balance": { + "type": "boolean", + "description": "Include current balance (default: true)", + }, + "include_history": { + "type": "boolean", + "description": "Include transaction history (default: true)", + }, + "include_details": { + "type": "boolean", + "description": "Include account details like address (default: false)", + }, + "history_limit": { + "type": "integer", + "description": "Maximum transactions to return (default: 50)", + }, + "provider_params": { + "type": "object", + "description": "Optional provider-specific parameters", + "properties": { + "provider": { + "type": "string", + "enum": ["x402", "chapa", "mpesa", "all"], + "description": "Specific provider or 'all' for all providers", + }, + }, + }, + }, + required=[], + ), + # Context enrichment: auto-run during context gathering + # Provides balance awareness for financial decision-making + context_enrichment=True, + context_enrichment_params={ + "include_balance": True, + "include_history": False, # Don't flood context with history + "include_details": False, + }, + documentation=ToolDocumentation( + quick_start="Get statement: get_statement include_balance=true include_history=true", + detailed_instructions=""" +# Get Statement + +Retrieve account information including balance, transaction history, and details. + +## What's Included + +### Balance (include_balance=true) +- **available**: Spendable balance +- **pending**: Incoming but not confirmed +- **total**: available + pending + +### History (include_history=true) +- Recent transactions (sends, receives) +- Transaction IDs for reference +- Timestamps and status + +### Details (include_details=false by default) +- Wallet address (crypto) +- Phone number (mobile money) +- Network information +- Attestation level + +## Provider Selection +- Omit provider_params: Query all enabled providers +- provider='x402': Only crypto accounts +- provider='chapa': Only Ethiopian accounts +- provider='all': Explicitly query all + +## Privacy Note +Account details may include sensitive information. +Only request details when needed. +""", + examples=[ + UsageExample( + title="Check all balances", + description="Get balance from all providers", + code='{"include_balance": true, "include_history": false}', + ), + UsageExample( + title="Full statement", + description="Get complete statement with history", + code='{"include_balance": true, "include_history": true, "include_details": true, "history_limit": 20}', + ), + UsageExample( + title="Crypto only", + description="Get only x402/USDC account info", + code='{"include_balance": true, "provider_params": {"provider": "x402"}}', + ), + ], + gotchas=[ + ToolGotcha( + title="History may be slow for large accounts", + description="Use history_limit to control response size", + severity="info", + ), + ], + ), + dma_guidance=ToolDMAGuidance( + requires_approval=False, + min_confidence=0.7, + when_not_to_use="Repeatedly polling for changes - use webhooks instead", + ethical_considerations="Balance and history reveal financial activity. Handle with appropriate privacy.", + ), + ), + } + + def __init__( + self, + config: WalletAdapterConfig, + providers: Optional[Dict[str, WalletProvider]] = None, + ) -> None: + """ + Initialize the wallet tool service. + + Args: + config: Wallet adapter configuration + providers: Dict mapping provider_id to WalletProvider instance + """ + self.config = config + self._providers: Dict[str, WalletProvider] = providers or {} + self._started = False + logger.info( + f"WalletToolService initialized with providers: {list(self._providers.keys())}" + ) + + def register_provider(self, provider: WalletProvider) -> None: + """Register a wallet provider.""" + self._providers[provider.provider_id] = provider + logger.info(f"Registered wallet provider: {provider.provider_id}") + + def _get_provider_for_currency(self, currency: str) -> Optional[WalletProvider]: + """Get the appropriate provider for a currency.""" + currency = currency.upper() + + # Check currency_providers mapping + provider_id = self.config.currency_providers.get(currency) + if provider_id and provider_id in self._providers: + return self._providers[provider_id] + + # Check if any provider supports this currency + for provider in self._providers.values(): + if provider.supports_currency(currency): + return provider + + # Fall back to default + if self.config.default_provider in self._providers: + return self._providers[self.config.default_provider] + + return None + + def _get_provider( + self, currency: str, provider_params: Optional[Dict[str, Any]] = None + ) -> Optional[WalletProvider]: + """Get provider based on currency and optional explicit provider param.""" + # Check for explicit provider + if provider_params and provider_params.get("provider"): + provider_id = provider_params["provider"] + if provider_id != "auto" and provider_id in self._providers: + return self._providers[provider_id] + + # Route by currency + return self._get_provider_for_currency(currency) + + async def start(self) -> None: + """Start the tool service and initialize providers.""" + logger.info("Starting WalletToolService") + for provider_id, provider in self._providers.items(): + try: + success = await provider.initialize() + if success: + logger.info(f"Wallet provider {provider_id} initialized") + else: + logger.warning(f"Wallet provider {provider_id} initialization failed") + except Exception as e: + logger.error(f"Error initializing provider {provider_id}: {e}") + self._started = True + logger.info("WalletToolService started") + + async def stop(self) -> None: + """Stop the tool service and cleanup providers.""" + logger.info("Stopping WalletToolService") + for provider_id, provider in self._providers.items(): + try: + await provider.cleanup() + logger.info(f"Wallet provider {provider_id} cleaned up") + except Exception as e: + logger.error(f"Error cleaning up provider {provider_id}: {e}") + self._started = False + logger.info("WalletToolService stopped") + + # ========================================================================= + # ToolServiceProtocol Implementation + # ========================================================================= + + def get_service_metadata(self) -> Dict[str, Any]: + """Return service metadata for DSAR and data source discovery.""" + return { + "data_source": True, + "data_source_type": "payment_provider", + "contains_pii": True, + "gdpr_applicable": True, + "connector_id": "wallet_adapter", + "data_retention_days": 90, + "encryption_at_rest": True, + } + + async def get_available_tools(self) -> List[str]: + """Get available tool names.""" + return list(self.TOOL_DEFINITIONS.keys()) + + async def list_tools(self) -> List[str]: + """Legacy alias for get_available_tools().""" + return await self.get_available_tools() + + async def get_tool_info(self, tool_name: str) -> Optional[ToolInfo]: + """Get detailed info for a specific tool.""" + return self.TOOL_DEFINITIONS.get(tool_name) + + async def get_all_tool_info(self) -> List[ToolInfo]: + """Get info for all tools.""" + return list(self.TOOL_DEFINITIONS.values()) + + async def get_tool_schema(self, tool_name: str) -> Optional[ToolParameterSchema]: + """Get parameter schema for a tool.""" + tool_info = self.TOOL_DEFINITIONS.get(tool_name) + return tool_info.parameters if tool_info else None + + async def validate_parameters( + self, tool_name: str, parameters: Dict[str, Any] + ) -> bool: + """Validate parameters for a tool without executing it.""" + if tool_name not in self.TOOL_DEFINITIONS: + return False + tool_info = self.TOOL_DEFINITIONS[tool_name] + if not tool_info.parameters: + return True + required = tool_info.parameters.required or [] + return all(param in parameters for param in required) + + async def get_tool_result( + self, correlation_id: str, timeout: float = 30.0 + ) -> Optional[ToolExecutionResult]: + """Get result of previously executed tool. Not implemented for sync wallet tools.""" + return None + + async def execute_tool( + self, + tool_name: str, + parameters: Dict[str, Any], + context: Optional[Dict[str, Any]] = None, + ) -> ToolExecutionResult: + """Execute a wallet tool.""" + start_time = datetime.now(timezone.utc) + correlation_id = str(uuid.uuid4()) + + logger.info("=" * 60) + logger.info(f"[WALLET TOOL] Tool: {tool_name}") + logger.info(f"[WALLET TOOL] Parameters: {parameters}") + logger.info(f"[WALLET TOOL] Correlation ID: {correlation_id}") + + if tool_name not in self.TOOL_DEFINITIONS: + logger.error(f"[WALLET TOOL] Unknown tool: {tool_name}") + return ToolExecutionResult( + tool_name=tool_name, + status=ToolExecutionStatus.NOT_FOUND, + success=False, + data=None, + error=f"Unknown tool: {tool_name}", + correlation_id=correlation_id, + ) + + try: + if tool_name == "send_money": + result = await self._execute_send_money(parameters, correlation_id) + elif tool_name == "request_money": + result = await self._execute_request_money(parameters, correlation_id) + elif tool_name == "get_statement": + result = await self._execute_get_statement(parameters, correlation_id) + else: + result = ToolExecutionResult( + tool_name=tool_name, + status=ToolExecutionStatus.FAILED, + success=False, + data=None, + error=f"Tool not implemented: {tool_name}", + correlation_id=correlation_id, + ) + + elapsed = (datetime.now(timezone.utc) - start_time).total_seconds() + logger.info(f"[WALLET TOOL] Result: success={result.success}") + if result.error: + logger.error(f"[WALLET TOOL] Error: {result.error}") + logger.info(f"[WALLET TOOL] Elapsed: {elapsed:.3f}s") + logger.info("=" * 60) + return result + + except Exception as e: + logger.error(f"[WALLET TOOL] Exception: {e}") + import traceback + + logger.error(f"[WALLET TOOL] Traceback: {traceback.format_exc()}") + return ToolExecutionResult( + tool_name=tool_name, + status=ToolExecutionStatus.FAILED, + success=False, + data=None, + error=str(e), + correlation_id=correlation_id, + ) + + async def _execute_send_money( + self, params: Dict[str, Any], correlation_id: str + ) -> ToolExecutionResult: + """Execute send_money tool.""" + # Validate required parameters + recipient = params.get("recipient", "") + amount_raw = params.get("amount") + currency = params.get("currency", "") + memo = params.get("memo") + provider_params = params.get("provider_params", {}) + + missing = [] + if not recipient: + missing.append("recipient") + if amount_raw is None: + missing.append("amount") + if not currency: + missing.append("currency") + + if missing: + return ToolExecutionResult( + tool_name="send_money", + status=ToolExecutionStatus.FAILED, + success=False, + data={"received_params": params, "missing": missing}, + error=f"Missing required parameter(s): {', '.join(missing)}", + correlation_id=correlation_id, + ) + + # Parse amount + try: + amount = Decimal(str(amount_raw)) + except (InvalidOperation, ValueError): + return ToolExecutionResult( + tool_name="send_money", + status=ToolExecutionStatus.FAILED, + success=False, + data={"received_params": params}, + error=f"Invalid amount: {amount_raw}", + correlation_id=correlation_id, + ) + + # Get provider + provider = self._get_provider(currency, provider_params) + if not provider: + return ToolExecutionResult( + tool_name="send_money", + status=ToolExecutionStatus.FAILED, + success=False, + data={"currency": currency, "available_providers": list(self._providers.keys())}, + error=f"No provider available for currency: {currency}", + correlation_id=correlation_id, + ) + + # Execute send + try: + result = await provider.send( + recipient=recipient, + amount=amount, + currency=currency.upper(), + memo=memo, + ) + + return ToolExecutionResult( + tool_name="send_money", + status=ToolExecutionStatus.COMPLETED if result.success else ToolExecutionStatus.FAILED, + success=result.success, + data={ + "transaction_id": result.transaction_id, + "provider": result.provider, + "amount": str(result.amount), + "currency": result.currency, + "recipient": result.recipient, + "timestamp": result.timestamp.isoformat(), + "fees": {k: str(v) for k, v in (result.fees or {}).items()}, + "confirmation": result.confirmation, + }, + error=result.error, + correlation_id=correlation_id, + ) + except Exception as e: + logger.error(f"[WALLET TOOL] send_money failed: {e}") + return ToolExecutionResult( + tool_name="send_money", + status=ToolExecutionStatus.FAILED, + success=False, + data=None, + error=str(e), + correlation_id=correlation_id, + ) + + async def _execute_request_money( + self, params: Dict[str, Any], correlation_id: str + ) -> ToolExecutionResult: + """Execute request_money tool.""" + # Validate required parameters + amount_raw = params.get("amount") + currency = params.get("currency", "") + description = params.get("description", "") + expires_at_str = params.get("expires_at") + provider_params = params.get("provider_params", {}) + + missing = [] + if amount_raw is None: + missing.append("amount") + if not currency: + missing.append("currency") + if not description: + missing.append("description") + + if missing: + return ToolExecutionResult( + tool_name="request_money", + status=ToolExecutionStatus.FAILED, + success=False, + data={"received_params": params, "missing": missing}, + error=f"Missing required parameter(s): {', '.join(missing)}", + correlation_id=correlation_id, + ) + + # Parse amount + try: + amount = Decimal(str(amount_raw)) + except (InvalidOperation, ValueError): + return ToolExecutionResult( + tool_name="request_money", + status=ToolExecutionStatus.FAILED, + success=False, + data={"received_params": params}, + error=f"Invalid amount: {amount_raw}", + correlation_id=correlation_id, + ) + + # Parse expiration + expires_at = None + if expires_at_str: + try: + expires_at = datetime.fromisoformat(expires_at_str.replace("Z", "+00:00")) + except ValueError: + logger.warning(f"Invalid expires_at format: {expires_at_str}") + + # Get provider + provider = self._get_provider(currency, provider_params) + if not provider: + return ToolExecutionResult( + tool_name="request_money", + status=ToolExecutionStatus.FAILED, + success=False, + data={"currency": currency}, + error=f"No provider available for currency: {currency}", + correlation_id=correlation_id, + ) + + # Execute request + try: + callback_url = provider_params.get("callback_url") if provider_params else None + result = await provider.request( + amount=amount, + currency=currency.upper(), + description=description, + expires_at=expires_at, + callback_url=callback_url, + ) + + return ToolExecutionResult( + tool_name="request_money", + status=ToolExecutionStatus.COMPLETED, + success=True, + data={ + "request_id": result.request_id, + "provider": result.provider, + "amount": str(result.amount), + "currency": result.currency, + "description": result.description, + "status": result.status.value, + "checkout_url": result.checkout_url, + "created_at": result.created_at.isoformat(), + "expires_at": result.expires_at.isoformat() if result.expires_at else None, + }, + error=None, + correlation_id=correlation_id, + ) + except Exception as e: + logger.error(f"[WALLET TOOL] request_money failed: {e}") + return ToolExecutionResult( + tool_name="request_money", + status=ToolExecutionStatus.FAILED, + success=False, + data=None, + error=str(e), + correlation_id=correlation_id, + ) + + async def _execute_get_statement( + self, params: Dict[str, Any], correlation_id: str + ) -> ToolExecutionResult: + """Execute get_statement tool.""" + include_balance = params.get("include_balance", True) + include_history = params.get("include_history", True) + include_details = params.get("include_details", False) + history_limit = params.get("history_limit", 50) + provider_params = params.get("provider_params", {}) + + # Determine which providers to query + target_provider = provider_params.get("provider") if provider_params else None + if target_provider and target_provider != "all" and target_provider in self._providers: + providers_to_query = {target_provider: self._providers[target_provider]} + else: + providers_to_query = self._providers + + accounts = [] + for provider_id, provider in providers_to_query.items(): + try: + account_data: Dict[str, Any] = { + "provider": provider_id, + "currency": provider.supported_currencies[0] if provider.supported_currencies else "UNKNOWN", + } + + if include_balance: + balance = await provider.get_balance() + account_data["balance"] = { + "available": str(balance.available), + "pending": str(balance.pending), + "total": str(balance.total), + } + + if include_details: + details = await provider.get_account_details() + account_data["details"] = { + "address": details.address, + "phone": details.phone, + "account_id": details.account_id, + "network": details.network, + "attestation_level": details.attestation_level, + } + + if include_history: + history = await provider.get_history(limit=history_limit) + account_data["history"] = [ + { + "transaction_id": tx.transaction_id, + "type": tx.type.value, + "status": tx.status.value, + "amount": str(tx.amount), + "currency": tx.currency, + "recipient": tx.recipient, + "sender": tx.sender, + "memo": tx.memo, + "timestamp": tx.timestamp.isoformat(), + } + for tx in history + ] + + accounts.append(account_data) + + except Exception as e: + logger.error(f"[WALLET TOOL] Error getting statement for {provider_id}: {e}") + accounts.append({ + "provider": provider_id, + "error": str(e), + }) + + return ToolExecutionResult( + tool_name="get_statement", + status=ToolExecutionStatus.COMPLETED, + success=True, + data={"accounts": accounts}, + error=None, + correlation_id=correlation_id, + ) diff --git a/ciris_adapters/weather/service.py b/ciris_adapters/weather/service.py index 25562d1d2..adc43a74c 100644 --- a/ciris_adapters/weather/service.py +++ b/ciris_adapters/weather/service.py @@ -48,7 +48,12 @@ def __init__(self) -> None: self.base_url = "https://api.weather.gov" # User agent is required by NOAA - self.user_agent = os.getenv("CIRIS_NOAA_USER_AGENT", "CIRIS/1.0 (contact@ciris.ai)") + # Use PUBLIC_API_CONTACT_EMAIL from wizard, fall back to legacy env var + contact_email = os.getenv("PUBLIC_API_CONTACT_EMAIL", "") + if contact_email: + self.user_agent = f"CIRIS/2.3 ({contact_email})" + else: + self.user_agent = os.getenv("CIRIS_NOAA_USER_AGENT", "CIRIS/1.0 (contact@ciris.ai)") # Optional: OpenWeatherMap as backup (requires API key) self.owm_api_key = os.getenv("CIRIS_OPENWEATHERMAP_API_KEY") diff --git a/ciris_engine/constants.py b/ciris_engine/constants.py index e4896d407..3a131a429 100644 --- a/ciris_engine/constants.py +++ b/ciris_engine/constants.py @@ -3,11 +3,11 @@ from pathlib import Path # Version information -CIRIS_VERSION = "2.2.9-stable" +CIRIS_VERSION = "2.3.0-stable" ACCORD_VERSION = "1.2-Beta" CIRIS_VERSION_MAJOR = 2 -CIRIS_VERSION_MINOR = 2 -CIRIS_VERSION_PATCH = 9 +CIRIS_VERSION_MINOR = 3 +CIRIS_VERSION_PATCH = 0 CIRIS_VERSION_BUILD = 0 CIRIS_VERSION_STAGE = "stable" CIRIS_CODENAME = "Context Engineering" # Codename for this release diff --git a/ciris_engine/logic/adapters/api/routes/audit.py b/ciris_engine/logic/adapters/api/routes/audit.py index 9a97dba76..291fe9fac 100644 --- a/ciris_engine/logic/adapters/api/routes/audit.py +++ b/ciris_engine/logic/adapters/api/routes/audit.py @@ -97,10 +97,6 @@ class AuditExportResponse(BaseModel): def _convert_audit_entry(entry: AuditEntry) -> AuditEntryResponse: """Convert AuditEntry to API response format.""" - import logging - - logger = logging.getLogger(__name__) - # Convert context to AuditContext ctx = entry.context if hasattr(ctx, "model_dump"): @@ -108,14 +104,6 @@ def _convert_audit_entry(entry: AuditEntry) -> AuditEntryResponse: ctx_dict = ctx.model_dump() additional_data = ctx_dict.get("additional_data", {}) - # Debug logging to trace metadata propagation - if additional_data: - logger.debug(f"_convert_audit_entry - additional_data keys: {additional_data.keys()}") - if "tool_name" in additional_data: - logger.debug(f"_convert_audit_entry - Found tool_name={additional_data['tool_name']}") - else: - logger.debug("_convert_audit_entry - No additional_data in ctx_dict") - # Extract outcome from additional_data (stored by audit service) outcome = additional_data.get("outcome") if additional_data else None # Also check for error field to determine outcome @@ -142,7 +130,6 @@ def _convert_audit_entry(entry: AuditEntry) -> AuditEntryResponse: ) else: # If it's not an AuditEntryContext, create a minimal AuditContext - logger.debug("_convert_audit_entry - ctx doesn't have model_dump, using str(ctx)") context = AuditContext(description=str(ctx) if ctx else None) return AuditEntryResponse( @@ -214,6 +201,165 @@ async def _query_sqlite_audit( return await loop.run_in_executor(None, _sync_query_sqlite_audit, db_path, start_time, end_time, limit, offset) +def _normalize_timestamp_str(ts_str: str) -> str: + """Normalize timestamp string to ISO format for consistent deduplication. + + SQLite uses: 2026-03-25T01:25:49.386438+00:00 (with T) + JSONL uses: 2026-03-25 01:21:00.503015+00:00 (with space) + + Normalizes to ISO format with 'T' separator. + """ + # Replace space with T for ISO format consistency + return ts_str.replace(" ", "T") + + +# ============================================================================= +# Helper functions for reducing cognitive complexity +# ============================================================================= + + +def _entry_has_additional_metadata(entry: AuditEntry) -> bool: + """Check if a graph entry has additional metadata worth merging. + + Graph entries may contain rich metadata from handlers (ponder_questions, + tool_parameters, etc.) that SQLite entries don't have. + """ + if not hasattr(entry, "context"): + return False + if not hasattr(entry.context, "additional_data"): + return False + return bool(entry.context.additional_data) + + +def _merge_graph_metadata_into_entry(merged_entry: _MergedAuditEntry, graph_ctx: Any) -> None: + """Merge metadata from a graph entry into an existing merged entry. + + Copies keys from graph's additional_data that don't already exist + in the merged entry's metadata. + """ + if merged_entry.entry.context.metadata is None: + merged_entry.entry.context.metadata = {} + + if hasattr(graph_ctx, "additional_data") and graph_ctx.additional_data: + for key, value in graph_ctx.additional_data.items(): + if key not in merged_entry.entry.context.metadata: + merged_entry.entry.context.metadata[key] = value + + +def _find_sqlite_entry_for_dedup_key( + merged: Dict[str, _MergedAuditEntry], dedup_key: str +) -> Optional[_MergedAuditEntry]: + """Find a SQLite entry in merged results that matches the given dedup key. + + Used to merge graph metadata into existing SQLite entries. + """ + for merged_entry in merged.values(): + sqlite_dedup = f"{_normalize_timestamp_str(merged_entry.entry.timestamp.isoformat())}_{merged_entry.entry.action}" + if sqlite_dedup == dedup_key: + return merged_entry + return None + + +def _infer_outcome_from_event(outcome: Optional[str], event_type: str) -> str: + """Infer outcome from event type if not explicitly provided. + + Returns 'failure' if event_type contains 'fail' or 'error', otherwise 'success'. + """ + if outcome: + return outcome + event_lower = event_type.lower() + if "fail" in event_lower or "error" in event_lower: + return "failure" + return "success" + + +def _extract_handler_metadata(params: Dict[str, Any]) -> Dict[str, Any]: + """Extract handler-specific metadata fields from parameters. + + Handles DEFER, TOOL, PONDER, SPEAK, TASK_COMPLETE, and REJECT parameters. + Returns a flat dict of extracted metadata. + """ + metadata: Dict[str, Any] = {} + + # DEFER params + if "defer_reason" in params: + metadata["defer_reason"] = params["defer_reason"] + if "defer_until" in params: + metadata["defer_until"] = params["defer_until"] + + # TOOL params + if "tool_name" in params: + metadata["tool_name"] = params["tool_name"] + elif "name" in params: + metadata["tool_name"] = params["name"] + + if "parameters" in params: + tool_params = params["parameters"] + metadata["tool_parameters"] = json.dumps(tool_params) if isinstance(tool_params, dict) else str(tool_params) + + # PONDER params + if "ponder_questions" in params: + metadata["ponder_questions"] = params["ponder_questions"] + elif "questions" in params: + metadata["ponder_questions"] = params["questions"] + + # SPEAK params + if "content" in params: + metadata["content"] = params["content"] + + # TASK_COMPLETE params + if "completion_reason" in params: + metadata["completion_reason"] = params["completion_reason"] + + # REJECT params + if "reason" in params: + metadata["reject_reason"] = params["reason"] + + return metadata + + +def _parse_event_payload_metadata( + event_payload_str: Optional[str], +) -> Tuple[Dict[str, Any], Optional[str]]: + """Parse event payload JSON and extract metadata and description. + + Returns (metadata dict, description string). + If parsing fails, returns empty metadata and the original string as description. + """ + if not event_payload_str: + return {}, None + + try: + payload = json.loads(event_payload_str) + except json.JSONDecodeError: + return {}, event_payload_str + + if not isinstance(payload, dict): + return {}, event_payload_str + + metadata: Dict[str, Any] = {} + + # Extract nested parameters + params_str = payload.get("parameters", "{}") + if isinstance(params_str, str): + try: + params = json.loads(params_str) + if isinstance(params, dict): + metadata.update(_extract_handler_metadata(params)) + except json.JSONDecodeError: + pass + + # Extract direct payload fields + for key in ["thought_id", "task_id", "handler_name", "action_type"]: + if key in payload: + metadata[key] = payload[key] + + # Build description from payload + description = payload.get("action_type") or payload.get("handler_name") or event_payload_str + + return metadata, description + + def _parse_jsonl_entry_timestamp( entry: dict[str, object], ) -> Optional[datetime]: # SERIALIZATION BOUNDARY - JSONL raw entries @@ -221,7 +367,7 @@ def _parse_jsonl_entry_timestamp( entry_time_str = get_str_optional(entry, "timestamp") or get_str_optional(entry, "event_timestamp") if entry_time_str: try: - return datetime.fromisoformat(entry_time_str.replace("Z", UTC_TIMEZONE_SUFFIX)) + return datetime.fromisoformat(entry_time_str.replace("Z", UTC_TIMEZONE_SUFFIX).replace(" ", "T")) except ValueError: return None return None @@ -295,26 +441,71 @@ def _process_graph_entries( ) -> None: """Process graph entries and add them to merged results. - Skip entries that already exist in SQLite (matched by timestamp + action). - SQLite entries are preferred as they contain the authoritative hash chain. + Graph entries contain the full handler action metadata (ponder_questions, tool_parameters, etc.) + while SQLite entries may only have partial data. We merge graph data INTO existing SQLite + entries rather than skipping graph entries entirely. """ + import logging + + logger = logging.getLogger(__name__) + for entry in graph_entries: - # Create a dedup key based on timestamp + action to match SQLite entries - dedup_key = f"{entry.timestamp.isoformat()}_{entry.action}" + normalized_ts = _normalize_timestamp_str(entry.timestamp.isoformat()) + dedup_key = f"{normalized_ts}_{entry.action}" + entry_id = getattr(entry, "id", f"audit_{entry.timestamp.isoformat()}_{entry.actor}") + has_metadata = _entry_has_additional_metadata(entry) + if dedup_key in seen_timestamps: - # Skip - this entry already exists from SQLite with better metadata + _handle_duplicate_graph_entry(merged, entry, dedup_key, has_metadata, logger) continue - entry_id = getattr(entry, "id", f"audit_{entry.timestamp.isoformat()}_{entry.actor}") - if entry_id not in merged: - merged[entry_id] = _MergedAuditEntry(entry=_convert_audit_entry(entry), sources=["graph"]) - seen_timestamps.add(dedup_key) - else: - merged[entry_id].sources.append("graph") + _add_new_graph_entry(merged, entry, entry_id, dedup_key, seen_timestamps, logger) + + +def _handle_duplicate_graph_entry( + merged: Dict[str, _MergedAuditEntry], + entry: AuditEntry, + dedup_key: str, + has_metadata: bool, + logger: Any, +) -> None: + """Handle a graph entry that duplicates an existing SQLite entry. + + If the graph entry has metadata, merge it into the existing SQLite entry. + """ + if not has_metadata: + return + + matched_entry = _find_sqlite_entry_for_dedup_key(merged, dedup_key) + if matched_entry: + _merge_graph_metadata_into_entry(matched_entry, entry.context) + matched_entry.sources.append("graph") + else: + logger.warning(f"[AUDIT API] No matching SQLite entry found for dedup_key={dedup_key}") + + +def _add_new_graph_entry( + merged: Dict[str, _MergedAuditEntry], + entry: AuditEntry, + entry_id: str, + dedup_key: str, + seen_timestamps: set[str], + logger: Any, +) -> None: + """Add a new graph entry to merged results.""" + logger.info(f"[AUDIT API] Adding new entry with id={entry_id}") + if entry_id not in merged: + merged[entry_id] = _MergedAuditEntry(entry=_convert_audit_entry(entry), sources=["graph"]) + seen_timestamps.add(dedup_key) + else: + merged[entry_id].sources.append("graph") def _process_sqlite_entries( - merged: Dict[str, _MergedAuditEntry], sqlite_entries: list[dict[str, object]], seen_timestamps: set[str] + merged: Dict[str, _MergedAuditEntry], + sqlite_entries: list[dict[str, object]], + seen_timestamps: set[str], + dedup_to_entry: Dict[str, str], ) -> None: # SERIALIZATION BOUNDARY - SQLite query results """Process SQLite entries and add them to merged results. @@ -322,98 +513,113 @@ def _process_sqlite_entries( Track timestamps for deduplication with graph entries. """ for sqlite_entry in sqlite_entries: - event_timestamp = get_str(sqlite_entry, "event_timestamp", "") - originator_id = get_str(sqlite_entry, "originator_id", "unknown") - event_type = get_str(sqlite_entry, "event_type", "unknown") - entry_id = get_str_optional(sqlite_entry, "event_id") or f"audit_{event_timestamp}_{originator_id}" + entry_info = _extract_sqlite_entry_info(sqlite_entry) + _track_sqlite_dedup_key(entry_info, seen_timestamps, dedup_to_entry) - # Track timestamp + action for deduplication with graph entries - dedup_key = f"{event_timestamp}_{event_type}" - seen_timestamps.add(dedup_key) + if entry_info["entry_id"] in merged: + merged[entry_info["entry_id"]].sources.append("sqlite") + else: + _add_new_sqlite_entry(merged, sqlite_entry, entry_info) - if entry_id not in merged: - # Convert SQLite entry to AuditEntryResponse format - timestamp = datetime.fromisoformat(event_timestamp.replace("Z", UTC_TIMEZONE_SUFFIX)) - # Extract outcome from SQLite entry - outcome = get_str_optional(sqlite_entry, "outcome") - if not outcome: - # Try to infer from event_type - event_type = get_str(sqlite_entry, "event_type", "").lower() - if "fail" in event_type or "error" in event_type: - outcome = "failure" - else: - outcome = "success" - # Parse event_payload JSON to extract metadata and parameters - event_payload_str = get_str_optional(sqlite_entry, "event_payload") - metadata: Dict[str, Any] = {} - description = event_payload_str - - if event_payload_str: - try: - payload = json.loads(event_payload_str) - if isinstance(payload, dict): - # Extract parameters (may contain defer_reason, tool_name, etc.) - params_str = payload.get("parameters", "{}") - if isinstance(params_str, str): - try: - params = json.loads(params_str) - if isinstance(params, dict): - # Copy key fields to metadata for mobile app - if "defer_reason" in params: - metadata["defer_reason"] = params["defer_reason"] - if "defer_until" in params: - metadata["defer_until"] = params["defer_until"] - if "tool_name" in params: - metadata["tool_name"] = params["tool_name"] - if "content" in params: - metadata["content"] = params["content"] - except json.JSONDecodeError: - pass - - # Also include direct payload fields in metadata - for key in ["thought_id", "task_id", "handler_name", "action_type"]: - if key in payload: - metadata[key] = payload[key] - - # Use action_type or handler for description - description = payload.get("action_type") or payload.get("handler_name") or event_payload_str - except json.JSONDecodeError: - pass - - context = AuditContext( - description=description, - entity_id=get_str_optional(sqlite_entry, "originator_id"), - outcome=outcome, - metadata=metadata if metadata else None, - ) +def _extract_sqlite_entry_info(sqlite_entry: dict[str, object]) -> Dict[str, str]: + """Extract basic info from a SQLite audit entry.""" + event_timestamp = get_str(sqlite_entry, "event_timestamp", "") + originator_id = get_str(sqlite_entry, "originator_id", "unknown") + event_type = get_str(sqlite_entry, "event_type", "unknown") + entry_id = get_str_optional(sqlite_entry, "event_id") or f"audit_{event_timestamp}_{originator_id}" + normalized_ts = _normalize_timestamp_str(event_timestamp) + dedup_key = f"{normalized_ts}_{event_type}" - merged[entry_id] = _MergedAuditEntry( - entry=AuditEntryResponse( - id=entry_id, - action=get_str(sqlite_entry, "event_type", "unknown"), - actor=originator_id, - timestamp=timestamp, - context=context, - signature=get_str_optional(sqlite_entry, "signature"), - hash_chain=get_str_optional(sqlite_entry, "previous_hash"), - storage_sources=["sqlite"], - ), - sources=["sqlite"], - ) - else: - merged[entry_id].sources.append("sqlite") + return { + "event_timestamp": event_timestamp, + "originator_id": originator_id, + "event_type": event_type, + "entry_id": entry_id, + "dedup_key": dedup_key, + } + + +def _track_sqlite_dedup_key( + entry_info: Dict[str, str], seen_timestamps: set[str], dedup_to_entry: Dict[str, str] +) -> None: + """Track dedup key for later source merging with graph/jsonl entries.""" + seen_timestamps.add(entry_info["dedup_key"]) + dedup_to_entry[entry_info["dedup_key"]] = entry_info["entry_id"] + + +def _add_new_sqlite_entry( + merged: Dict[str, _MergedAuditEntry], sqlite_entry: dict[str, object], entry_info: Dict[str, str] +) -> None: + """Add a new SQLite entry to merged results.""" + timestamp = datetime.fromisoformat(entry_info["event_timestamp"].replace("Z", UTC_TIMEZONE_SUFFIX)) + outcome = _infer_outcome_from_event( + get_str_optional(sqlite_entry, "outcome"), entry_info["event_type"] + ) + + event_payload_str = get_str_optional(sqlite_entry, "event_payload") + metadata, description = _parse_event_payload_metadata(event_payload_str) + if description is None: + description = event_payload_str + + context = AuditContext( + description=description, + entity_id=get_str_optional(sqlite_entry, "originator_id"), + outcome=outcome, + metadata=metadata if metadata else None, + ) + + merged[entry_info["entry_id"]] = _MergedAuditEntry( + entry=AuditEntryResponse( + id=entry_info["entry_id"], + action=get_str(sqlite_entry, "event_type", "unknown"), + actor=entry_info["originator_id"], + timestamp=timestamp, + context=context, + signature=get_str_optional(sqlite_entry, "signature"), + hash_chain=get_str_optional(sqlite_entry, "previous_hash"), + storage_sources=["sqlite"], + ), + sources=["sqlite"], + ) def _process_jsonl_entries( - merged: Dict[str, _MergedAuditEntry], jsonl_entries: list[dict[str, object]] + merged: Dict[str, _MergedAuditEntry], jsonl_entries: list[dict[str, object]], seen_timestamps: set[str], + dedup_to_entry: Dict[str, str] ) -> None: # SERIALIZATION BOUNDARY - JSONL raw entries - """Process JSONL entries and add them to merged results.""" + """Process JSONL entries and add them to merged results. + + If entry already exists from SQLite/graph (by timestamp+action), just add 'jsonl' to sources. + """ for jsonl_entry in jsonl_entries: - timestamp_str = get_str(jsonl_entry, "timestamp", "") + timestamp_str = get_str(jsonl_entry, "timestamp", "") or get_str(jsonl_entry, "event_timestamp", "") actor_str = get_str(jsonl_entry, "actor", "") + action = get_str_optional(jsonl_entry, "action") or get_str(jsonl_entry, "event_type", "unknown") entry_id = get_str_optional(jsonl_entry, "id") or f"audit_{timestamp_str}_{actor_str}" + # Check timestamp-based deduplication - add source to existing entry + # Normalize timestamp format for consistent matching with SQLite + normalized_ts = _normalize_timestamp_str(timestamp_str) + dedup_key = f"{normalized_ts}_{action}" + if dedup_key in seen_timestamps: + # Find the existing entry and add jsonl to its sources + # First check dedup_to_entry (populated by SQLite entries) + existing_entry_id = dedup_to_entry.get(dedup_key) + if existing_entry_id and existing_entry_id in merged: + if "jsonl" not in merged[existing_entry_id].sources: + merged[existing_entry_id].sources.append("jsonl") + else: + # dedup_to_entry may not have this key if entry came from graph + # Search merged entries by matching dedup_key + for merged_id, merged_entry in merged.items(): + entry_dedup = f"{_normalize_timestamp_str(merged_entry.entry.timestamp.isoformat())}_{merged_entry.entry.action}" + if entry_dedup == dedup_key: + if "jsonl" not in merged_entry.sources: + merged_entry.sources.append("jsonl") + break + continue + if entry_id not in merged: # Convert JSONL entry to AuditEntryResponse format ts_str = get_str_optional(jsonl_entry, "timestamp") or get_str_optional(jsonl_entry, "event_timestamp") @@ -460,23 +666,23 @@ async def _merge_audit_sources( ) -> List[AuditEntryResponse]: # SERIALIZATION BOUNDARY - Raw database results """Merge audit entries from all sources and track storage locations. - SQLite entries are the authoritative source with the hash chain and - complete metadata. When SQLite entries exist, we skip graph entries - entirely to avoid duplicates (timestamp precision differences make - exact matching unreliable). + Each event should appear ONCE with storage_sources showing which backends have it. + SQLite entries are the authoritative source with the hash chain. + Graph entries provide rich metadata (ponder_questions, tool_result, etc.). + JSONL provides backup logging. """ merged: Dict[str, _MergedAuditEntry] = {} # Track entries by ID with their sources seen_timestamps: set[str] = set() # Track timestamp+action for deduplication + dedup_to_entry: Dict[str, str] = {} # Map dedup_key -> entry_id for source tracking # Process SQLite entries FIRST (authoritative source with hash chain) - _process_sqlite_entries(merged, sqlite_entries, seen_timestamps) + _process_sqlite_entries(merged, sqlite_entries, seen_timestamps, dedup_to_entry) + + # Process graph entries - merge metadata into SQLite entries, add 'graph' source + _process_graph_entries(merged, graph_entries, seen_timestamps) - # Only process graph and JSONL entries if NO SQLite entries exist - # SQLite is the authoritative source with the hash chain - it contains - # complete data. Graph and JSONL are secondary copies with less metadata. - if not sqlite_entries: - _process_graph_entries(merged, graph_entries, seen_timestamps) - _process_jsonl_entries(merged, jsonl_entries) + # Process JSONL - just add 'jsonl' source to existing entries + _process_jsonl_entries(merged, jsonl_entries, seen_timestamps, dedup_to_entry) # Update storage_sources for all merged entries and build result result = [] @@ -538,15 +744,10 @@ async def query_audit_entries( ) try: - import logging - - logger = logging.getLogger(__name__) - # Query all 3 audit sources concurrently # Query graph memory (existing functionality) graph_entries = await audit_service.query_audit_trail(query) - logger.info(f"[AUDIT API] Graph entries returned: {len(graph_entries)}") # Get the proper data directory for SQLite path from ciris_engine.logic.utils.path_resolution import get_data_dir @@ -554,8 +755,6 @@ async def query_audit_entries( data_dir = get_data_dir() sqlite_path = str(data_dir / "ciris_audit.db") jsonl_path = str(data_dir / "audit_logs.jsonl") - logger.info(f"[AUDIT API] SQLite path: {sqlite_path}, exists: {Path(sqlite_path).exists()}") - logger.info(f"[AUDIT API] JSONL path: {jsonl_path}, exists: {Path(jsonl_path).exists()}") # Query SQLite database directly sqlite_task = _query_sqlite_audit( @@ -577,25 +776,13 @@ async def query_audit_entries( # Execute SQLite and JSONL queries concurrently sqlite_entries, jsonl_entries = await asyncio.gather(sqlite_task, jsonl_task) - logger.info(f"[AUDIT API] SQLite entries returned: {len(sqlite_entries)}") - logger.info(f"[AUDIT API] JSONL entries returned: {len(jsonl_entries)}") - - # Log sample of SQLite entries to debug - for i, entry in enumerate(sqlite_entries[:3]): - logger.info( - f"[AUDIT API] SQLite entry {i}: event_type={entry.get('event_type')}, timestamp={entry.get('event_timestamp')}" - ) # Merge all sources and track storage locations response_entries = await _merge_audit_sources(graph_entries, sqlite_entries, jsonl_entries) - logger.info(f"[AUDIT API] After merge: {len(response_entries)} total entries") # Apply final pagination after merging paginated_entries = response_entries[offset : offset + limit] total = len(response_entries) - logger.info( - f"[AUDIT API] Returning {len(paginated_entries)} entries (offset={offset}, limit={limit}, total={total})" - ) return SuccessResponse( data=AuditEntriesResponse(entries=paginated_entries, total=total, offset=offset, limit=limit), diff --git a/ciris_engine/logic/adapters/api/routes/setup/complete.py b/ciris_engine/logic/adapters/api/routes/setup/complete.py index a08a885e1..2bdf9613a 100644 --- a/ciris_engine/logic/adapters/api/routes/setup/complete.py +++ b/ciris_engine/logic/adapters/api/routes/setup/complete.py @@ -255,6 +255,60 @@ def _create_founding_partnership(user_id: str) -> None: logger.info(f"✅ Founding partnership created for setup user: {user_id}") +def _store_user_preferences(user_id: str, setup: SetupCompleteRequest) -> None: + """Store language and location preferences from setup wizard into graph memory. + + These preferences are stored as a graph node so the agent can access them + during conversation to match the user's language and provide location-aware responses. + """ + from ciris_engine.logic.persistence import add_graph_node + from ciris_engine.logic.services.lifecycle.time.service import TimeService + from ciris_engine.schemas.services.graph_core import GraphNode, GraphScope, NodeType + + attributes: dict[str, str] = {} + + if setup.preferred_language: + attributes["preferred_language"] = setup.preferred_language + if setup.location_country: + attributes["location_country"] = setup.location_country + if setup.location_region: + attributes["location_region"] = setup.location_region + if setup.location_city: + attributes["location_city"] = setup.location_city + if setup.timezone: + attributes["timezone"] = setup.timezone + + if not attributes: + return + + # Build location string at user-chosen granularity + location_parts = [] + if setup.location_city: + location_parts.append(setup.location_city) + if setup.location_region: + location_parts.append(setup.location_region) + if setup.location_country: + location_parts.append(setup.location_country) + if location_parts: + attributes["location"] = ", ".join(location_parts) + + now = datetime.now(timezone.utc) + node = GraphNode( + id=f"preferences/{user_id}", + type=NodeType.CONCEPT, + scope=GraphScope.LOCAL, + attributes=attributes, + updated_by="setup_wizard", + updated_at=now, + ) + + time_service = TimeService() + add_graph_node(node, time_service, None) + lang = attributes.get("preferred_language", "not set") + loc = attributes.get("location", "not set") + logger.info(f"Stored user preferences for {user_id}: lang={lang}, location={loc}") + + async def _log_wa_list(auth_service: Any, phase: str) -> None: """Log list of WAs for debugging purposes.""" was = await auth_service.list_was(active_only=False) @@ -320,6 +374,9 @@ async def _create_setup_users(setup: SetupCompleteRequest, auth_db_path: str) -> canonical_user_id = setup.admin_username _create_founding_partnership(canonical_user_id) + # Store user preferences (language & location) in graph memory + _store_user_preferences(canonical_user_id, setup) + # Ensure system WA exists await _ensure_system_wa(auth_service) @@ -487,6 +544,20 @@ def _save_setup_config(setup: SetupCompleteRequest) -> Path: for key, value in setup.adapter_config.items(): f.write(f"{key}={value}\n") + # User preferences (language & location) + if setup.preferred_language: + f.write("\n# User Preferences\n") + f.write(f'CIRIS_PREFERRED_LANGUAGE="{setup.preferred_language}"\n') + if setup.location_country: + location_parts = [setup.location_country] + if setup.location_region: + location_parts.append(setup.location_region) + if setup.location_city: + location_parts.append(setup.location_city) + f.write(f'CIRIS_USER_LOCATION="{", ".join(location_parts)}"\n') + if setup.timezone: + f.write(f'CIRIS_USER_TIMEZONE="{setup.timezone}"\n') + # Write optional configuration sections _write_backup_llm_config(f, setup) _write_node_connection_config(f, setup) diff --git a/ciris_engine/logic/adapters/api/routes/setup/models.py b/ciris_engine/logic/adapters/api/routes/setup/models.py index 97af45506..925421160 100644 --- a/ciris_engine/logic/adapters/api/routes/setup/models.py +++ b/ciris_engine/logic/adapters/api/routes/setup/models.py @@ -293,6 +293,23 @@ class SetupCompleteRequest(BaseModel): # Application Configuration agent_port: int = Field(default=8080, description="Agent API port") + # User Preferences (language & location at user-selected granularity) + preferred_language: Optional[str] = Field( + None, description="ISO 639-1 language code (e.g., 'en', 'am', 'es', 'fr')" + ) + location_country: Optional[str] = Field( + None, description="ISO 3166-1 alpha-2 country code (e.g., 'US', 'ET', 'JP')" + ) + location_region: Optional[str] = Field( + None, description="Region/state/province name (user-chosen granularity, may be omitted)" + ) + location_city: Optional[str] = Field( + None, description="City name (user-chosen granularity, may be omitted)" + ) + timezone: Optional[str] = Field( + None, description="IANA timezone (e.g., 'America/Chicago', 'Africa/Addis_Ababa')" + ) + # Node Connection (set by "Connect to Node" device auth flow) node_url: Optional[str] = Field(None, description="CIRISNode URL (e.g., https://node.ciris.ai)") identity_template: Optional[str] = Field(None, description="Registry-provisioned identity template ID") diff --git a/ciris_engine/logic/context/system_snapshot_helpers.py b/ciris_engine/logic/context/system_snapshot_helpers.py index 8bab2da3d..bb89955b9 100644 --- a/ciris_engine/logic/context/system_snapshot_helpers.py +++ b/ciris_engine/logic/context/system_snapshot_helpers.py @@ -991,15 +991,32 @@ def _process_tool_result(result: Any, tool_key: str) -> Any: async def _execute_enrichment_tool(tool_services: List[Any], adapter_type: str, tool: ToolInfo) -> tuple[str, Any]: - """Execute a single enrichment tool and return (tool_key, result).""" + """Execute a single enrichment tool and return (tool_key, result). + + If the tool has `_info_only=True` in context_enrichment_params, it just surfaces + the tool info for the prompt without actually executing the tool. + """ tool_key = f"{adapter_type}:{tool.name}" + params = tool.context_enrichment_params or {} + + # Handle _info_only flag - just surface the tool info without executing + if params.get("_info_only", False): + logger.info(f"[CONTEXT_ENRICHMENT] {tool_key} is _info_only, surfacing tool info without execution") + # Return tool info as a highlight for the prompt + return tool_key, { + "_tool_highlight": True, + "tool_name": tool.name, + "description": tool.description, + "when_to_use": tool.when_to_use or tool.description, + "message": f"USE THIS TOOL for this type of request: {tool.when_to_use or tool.description}", + } + tool_service = await _find_tool_service(tool_services, adapter_type, tool.name) if not tool_service: logger.warning(f"[CONTEXT_ENRICHMENT] No tool service found for {tool_key}") return tool_key, None - params = tool.context_enrichment_params or {} logger.info(f"[CONTEXT_ENRICHMENT] Executing {tool_key} with params: {params}") result = await _call_async_or_sync_method(tool_service, "execute_tool", tool.name, params) diff --git a/ciris_engine/logic/dma/action_selection/action_instruction_generator.py b/ciris_engine/logic/dma/action_selection/action_instruction_generator.py index 04edea3ae..29836f888 100644 --- a/ciris_engine/logic/dma/action_selection/action_instruction_generator.py +++ b/ciris_engine/logic/dma/action_selection/action_instruction_generator.py @@ -318,6 +318,8 @@ def _format_tools_for_prompt(self, all_tools: JSONDict) -> str: tools_info = [] tools_info.append("\nAvailable tools (use EXACT tool_name shown, TSASPDMA will handle parameters):") + logger.info(f"[TOOL_PROMPT] Formatting {len(all_tools)} tools for ASPDMA prompt") + for tool_key, tool_info_raw in all_tools.items(): tool_info = get_dict({"info": tool_info_raw}, "info", {}) tool_name = get_str(tool_info, "name", "") @@ -336,7 +338,12 @@ def _format_tools_for_prompt(self, all_tools: JSONDict) -> str: tool_line += f" (from {tool_service})" tools_info.append(tool_line) - return "\n".join(tools_info) + # Log each tool being added to help debug tool selection issues + logger.info(f"[TOOL_PROMPT] Added to prompt: tool_name=\"{tool_name}\" when_to_use=\"{when_to_use[:60]}...\"") + + final_tools_str = "\n".join(tools_info) + logger.info(f"[TOOL_PROMPT] Final tools section length: {len(final_tools_str)} chars") + return final_tools_str def _simplify_schema(self, schema: JSONDict) -> str: """Simplify a JSON schema to a readable format.""" diff --git a/ciris_engine/logic/dma/action_selection/context_builder.py b/ciris_engine/logic/dma/action_selection/context_builder.py index 962dd4e9a..64973f68d 100644 --- a/ciris_engine/logic/dma/action_selection/context_builder.py +++ b/ciris_engine/logic/dma/action_selection/context_builder.py @@ -382,21 +382,28 @@ def _get_available_tools_str(self, permitted_actions: List[HandlerActionType]) - Full parameter schemas are provided separately in action_parameter_schemas. """ if HandlerActionType.TOOL not in permitted_actions: + logger.info("[CONTEXT] TOOL action not in permitted_actions, skipping tool summaries") return "" cached_tools = self._get_cached_tools() if not cached_tools: - logger.debug("[CONTEXT] No cached tools available for tool summaries") + logger.warning("[CONTEXT] No cached tools available for tool summaries - tools may not have been pre-cached!") return "" + logger.info(f"[CONTEXT] Building tool summaries for {len(cached_tools)} cached tools") + logger.info(f"[CONTEXT] Cached tool names: {list(cached_tools.keys())}") + # Build concise summaries using when_to_use (not full parameters) summaries = ["\n\n## Quick Tool Guide (when to use each tool):"] for tool_key, tool_info in cached_tools.items(): summary = self._get_tool_summary(tool_key, tool_info) if summary: summaries.append(summary) + logger.debug(f"[CONTEXT] Tool summary: {summary[:80]}...") - return "\n".join(summaries) if len(summaries) > 1 else "" + result = "\n".join(summaries) if len(summaries) > 1 else "" + logger.info(f"[CONTEXT] Tool summaries section: {len(result)} chars, {len(summaries)-1} tools") + return result def _get_installable_tools_str(self) -> str: """Get list of tools that are available for installation but not currently usable. diff --git a/ciris_engine/logic/dma/prompts/action_selection_pdma.yml b/ciris_engine/logic/dma/prompts/action_selection_pdma.yml index 43e200a21..cb3b3a788 100644 --- a/ciris_engine/logic/dma/prompts/action_selection_pdma.yml +++ b/ciris_engine/logic/dma/prompts/action_selection_pdma.yml @@ -13,7 +13,12 @@ decision_format: "Return JSON with FLAT fields (no nested action_parameters). Re - RECALL: recall_query (string), recall_node_type (string, optional), recall_scope (string, optional), recall_limit (integer, optional)\n\ - FORGET: forget_node_id (string), forget_reason (string)\n\ - TASK_COMPLETE: completion_reason (string)" -closing_reminder: Recall CIRIS principles override personal preference. +closing_reminder: "Recall CIRIS principles override personal preference.\n\n\ + LANGUAGE MATCHING: When composing speak_content, respond in the same language the\ + \ user wrote in. If the user writes in Amharic, respond in Amharic. If they write\ + \ in Spanish, respond in Spanish. Match the user's language unless they explicitly\ + \ request a different one. If a user's profile indicates a preferred language, respect\ + \ that preference." action_parameter_schemas: "FLAT field schemas per action (DO NOT use nested 'action_parameters'):\n\ SPEAK: speak_content is the response text\n\ PONDER: ponder_questions is a list of 2-3 clarifying questions\n\ diff --git a/ciris_engine/logic/dma/tsaspdma.py b/ciris_engine/logic/dma/tsaspdma.py index 4734e57bf..cd7630571 100644 --- a/ciris_engine/logic/dma/tsaspdma.py +++ b/ciris_engine/logic/dma/tsaspdma.py @@ -238,13 +238,31 @@ def _format_context_enrichment(self, context_enrichment: Optional[Dict[str, Any] that provide available resources (e.g., entity IDs) for parameter selection. """ if not context_enrichment: + logger.info("[TSASPDMA] No context enrichment data provided") return "" + logger.info(f"[TSASPDMA] Formatting context enrichment with {len(context_enrichment)} entries") + logger.info(f"[TSASPDMA] Context enrichment keys: {list(context_enrichment.keys())}") + sections = [] for tool_key, result in context_enrichment.items(): + logger.info(f"[TSASPDMA] Processing enrichment: {tool_key} = {type(result)}") if not result: continue + # Handle _tool_highlight from _info_only tools + if isinstance(result, dict) and result.get("_tool_highlight"): + tool_name = result.get("tool_name", tool_key) + when_to_use = result.get("when_to_use", "") + message = result.get("message", "") + sections.append(f"--- IMPORTANT TOOL AVAILABLE: {tool_name} ---") + sections.append(f" ⭐ {message}") + if when_to_use: + sections.append(f" When to use: {when_to_use}") + sections.append(f" tool_name=\"{tool_name}\" - Use this EXACT name!") + logger.info(f"[TSASPDMA] Added tool highlight for: {tool_name}") + continue + # Format based on tool type if "ha_list_entities" in tool_key or "home_assistant" in tool_key: # Format HA entities specially @@ -366,12 +384,17 @@ async def evaluate_tool_action( - SPEAK: Ask user for clarification - PONDER: Reconsider the approach """ + logger.info(f"[TSASPDMA] evaluate_tool_action called for tool: {tool_name}") + logger.info(f"[TSASPDMA] Tool info: name={tool_info.name}, when_to_use={tool_info.when_to_use[:50] if tool_info.when_to_use else 'N/A'}...") + logger.info(f"[TSASPDMA] Context enrichment received: {list(context_enrichment.keys()) if context_enrichment else 'None'}") + # Access the text content directly from ThoughtContent, not str() which gives repr thought_content_str = ( original_thought.content.text if hasattr(original_thought.content, "text") else str(original_thought.content) ) + logger.info(f"[TSASPDMA] Original thought: {thought_content_str[:100]}...") messages = self._create_tsaspdma_messages( tool_name=tool_name, diff --git a/ciris_engine/logic/formatters/user_profiles.py b/ciris_engine/logic/formatters/user_profiles.py index 491d13850..f355108ef 100644 --- a/ciris_engine/logic/formatters/user_profiles.py +++ b/ciris_engine/logic/formatters/user_profiles.py @@ -17,6 +17,7 @@ def _convert_user_profile_to_dict(profile: Any) -> dict[str, Any]: "nick": profile.display_name, "interest": profile.notes or "", "channel": "", # Not stored in UserProfile schema + "preferred_language": profile.preferred_language, } return cast(dict[str, Any], profile) @@ -63,6 +64,10 @@ def _format_single_profile(user_key: str, profile_data: dict[str, Any]) -> str: if channel: profile_summary += f", Primary Channel: '{channel}'" + preferred_language = profile_data.get("preferred_language") + if preferred_language and preferred_language != "en": + profile_summary += f", Preferred Language: '{preferred_language}'" + return profile_summary diff --git a/ciris_engine/logic/infrastructure/handlers/action_dispatcher.py b/ciris_engine/logic/infrastructure/handlers/action_dispatcher.py index febb837a5..beaeb7e60 100644 --- a/ciris_engine/logic/infrastructure/handlers/action_dispatcher.py +++ b/ciris_engine/logic/infrastructure/handlers/action_dispatcher.py @@ -299,6 +299,9 @@ async def _execute_handler( audit_params = extract_audit_parameters( action_type, final_action_result.action_parameters, follow_up_thought_id ) + logger.info( + f"[AUDIT] Extracted params for {action_type.value}: {list(audit_params.keys())}" + ) # For TOOL actions, capture the result from the follow-up thought's content if action_type == HandlerActionType.TOOL and follow_up_thought_id: @@ -337,11 +340,21 @@ async def _execute_handler( handler_name=handler.__class__.__name__, parameters=audit_params, ) + + # Log full audit details for debugging + logger.info(f"[AUDIT] Creating entry for {action_type.value}:") + logger.info(f"[AUDIT] thought_id: {thought.thought_id}") + logger.info(f"[AUDIT] outcome: {outcome}") + for key, value in audit_params.items(): + # Truncate long values for readability + display_value = str(value)[:100] + "..." if len(str(value)) > 100 else value + logger.info(f"[AUDIT] {key}: {display_value}") + audit_result = await self.audit_service.log_action( action_type=action_type, context=audit_context, outcome=outcome ) logger.info( - f"Created audit entry {audit_result.entry_id} for action {action_type.value} with outcome={outcome}" + f"[AUDIT] Created entry {audit_result.entry_id} for {action_type.value} (outcome={outcome})" ) # Build response diff --git a/ciris_engine/logic/setup/wizard.py b/ciris_engine/logic/setup/wizard.py index 0ed19e111..9d6cb968b 100644 --- a/ciris_engine/logic/setup/wizard.py +++ b/ciris_engine/logic/setup/wizard.py @@ -321,6 +321,80 @@ def create_env_file( return save_path +def prompt_language_and_location() -> tuple[str, Optional[str], Optional[str], Optional[str]]: + """Interactively prompt for language and location preferences. + + Users choose their own level of location granularity. + + Returns: + Tuple of (language_code, country, region, city) - any location part may be None + """ + print("\n" + "=" * 70) + print("LANGUAGE & LOCATION") + print("=" * 70) + print() + print("Help CIRIS respond in your preferred language and understand your context.") + print() + + # Language selection + languages = [ + ("en", "English"), + ("am", "Amharic (አማርኛ)"), + ("ar", "Arabic (العربية)"), + ("de", "German (Deutsch)"), + ("es", "Spanish (Español)"), + ("fr", "French (Français)"), + ("hi", "Hindi (हिन्दी)"), + ("it", "Italian (Italiano)"), + ("ja", "Japanese (日本語)"), + ("ko", "Korean (한국어)"), + ("pt", "Portuguese (Português)"), + ("ru", "Russian (Русский)"), + ("sw", "Swahili (Kiswahili)"), + ("tr", "Turkish (Türkçe)"), + ("zh", "Chinese (中文)"), + ] + print("Preferred language:") + for i, (code, name) in enumerate(languages, 1): + print(f" {i:2d}) {name}") + print(f" {len(languages) + 1:2d}) Other (enter ISO 639-1 code)") + print() + + choice = input(f"Select language [1-{len(languages) + 1}] (default: 1 - English): ").strip() or "1" + try: + idx = int(choice) - 1 + if 0 <= idx < len(languages): + language = languages[idx][0] + print(f" Selected: {languages[idx][1]}") + else: + language = input("Enter ISO 639-1 language code (e.g., 'nl', 'vi'): ").strip().lower() or "en" + except ValueError: + language = "en" + + # Location - user chooses granularity + print() + print("Location (optional - share as much or as little as you like):") + print(" 1) Country only") + print(" 2) Country + Region/State") + print(" 3) Country + Region + City") + print(" 4) Prefer not to say") + print() + + loc_choice = input("Select [1-4] (default: 4): ").strip() or "4" + country: Optional[str] = None + region: Optional[str] = None + city: Optional[str] = None + + if loc_choice in ("1", "2", "3"): + country = input("Country (e.g., Ethiopia, US, Japan): ").strip() or None + if loc_choice in ("2", "3") and country: + region = input("Region/State (e.g., Amhara, California, Tokyo): ").strip() or None + if loc_choice == "3" and region: + city = input("City (e.g., Addis Ababa, San Francisco): ").strip() or None + + return (language, country, region, city) + + def run_setup_wizard() -> Path: """Run the interactive setup wizard. @@ -348,6 +422,9 @@ def run_setup_wizard() -> Path: print("Setup cancelled. Using existing configuration.") return config_path + # Get language and location preferences + language, country, region, city = prompt_language_and_location() + # Get LLM configuration llm_provider, llm_api_key, llm_base_url, llm_model = prompt_llm_configuration() @@ -361,6 +438,18 @@ def run_setup_wizard() -> Path: llm_model=llm_model, ) + # Append language/location preferences to .env + with open(save_path, "a") as f: + f.write("\n# User Preferences (from setup wizard)\n") + f.write(f'CIRIS_PREFERRED_LANGUAGE="{language}"\n') + if country: + location_parts = [country] + if region: + location_parts.append(region) + if city: + location_parts.append(city) + f.write(f'CIRIS_USER_LOCATION="{", ".join(location_parts)}"\n') + print() print("=" * 70) print("✅ SETUP COMPLETE") diff --git a/ciris_engine/logic/utils/path_resolution.py b/ciris_engine/logic/utils/path_resolution.py index a6fc1ab25..fc6021617 100644 --- a/ciris_engine/logic/utils/path_resolution.py +++ b/ciris_engine/logic/utils/path_resolution.py @@ -249,8 +249,9 @@ def get_ciris_home() -> Path: validated = _validate_ciris_home_env(" (Android)") if validated: return validated - # Fallback: use Path.home()/files/ciris (Android app files structure) - return Path.home() / "files" / "ciris" + # Fallback: use Path.home()/ciris (Android Chaquopy sets HOME to /data/data/{pkg}/files) + # So this becomes /data/data/{pkg}/files/ciris + return Path.home() / "ciris" # Priority 2b: iOS mode - use app's Documents directory if is_ios(): @@ -329,6 +330,97 @@ def get_package_root() -> Path: return Path(ciris_engine.__file__).parent +def ensure_ciris_home_env() -> Path: + """Ensure CIRIS_HOME environment variable is set for all platforms. + + This function MUST be called early in application startup, before any + code that depends on CIRIS_HOME (especially CIRISVerify/verifier_singleton). + + Platform support: + - Linux (desktop, server, WSL) + - macOS (x64, arm64) + - Windows (x64) + - Android (via Chaquopy) + - iOS (via BeeWare/PythonKit) + - Docker/managed deployments + + The function: + 1. Computes the correct CIRIS_HOME using get_ciris_home() + 2. Sets the CIRIS_HOME environment variable + 3. Sets CIRIS_DATA_DIR for CIRISVerify compatibility + 4. Creates the directory if it doesn't exist + 5. Returns the resolved path + + Returns: + Path to CIRIS home directory + + Example: + # In main.py, call this FIRST before any imports that use CIRISVerify + from ciris_engine.logic.utils.path_resolution import ensure_ciris_home_env + ciris_home = ensure_ciris_home_env() + """ + # Compute the correct home directory for this platform + ciris_home = get_ciris_home() + + # Resolve to absolute path + ciris_home = ciris_home.resolve() + + # Set CIRIS_HOME environment variable (use setdefault to not override explicit user setting) + # But if CIRIS_HOME is already set, validate it matches our computed path in dev mode + existing_home = os.environ.get("CIRIS_HOME") + if existing_home: + existing_path = Path(existing_home).resolve() + if existing_path != ciris_home: + # In development mode, trust the computed path over env var + # In other modes, trust the explicit env var + if is_development_mode(): + logger.warning( + f"[path_resolution] CIRIS_HOME env ({existing_path}) differs from " + f"computed path ({ciris_home}) in dev mode - using computed path" + ) + os.environ["CIRIS_HOME"] = str(ciris_home) + else: + # Trust the explicit env var in non-dev modes + ciris_home = existing_path + logger.info(f"[path_resolution] Using explicit CIRIS_HOME: {ciris_home}") + else: + os.environ["CIRIS_HOME"] = str(ciris_home) + + # Set CIRIS_DATA_DIR for CIRISVerify compatibility + # CIRISVerify reads this for key storage path + data_dir = ciris_home / "data" + os.environ.setdefault("CIRIS_DATA_DIR", str(data_dir)) + + # Create home directory if it doesn't exist (with appropriate permissions) + try: + ciris_home.mkdir(parents=True, exist_ok=True) + # Ensure data directory exists too + data_dir.mkdir(parents=True, exist_ok=True) + except OSError as e: + logger.warning(f"[path_resolution] Could not create CIRIS_HOME directory: {e}") + + # Log the configuration for debugging + platform_info = [] + if is_android(): + platform_info.append("Android") + if is_ios(): + platform_info.append("iOS") + if is_managed(): + platform_info.append("Managed/Docker") + if is_development_mode(): + platform_info.append("Development") + if not platform_info: + platform_info.append("Installed") + + logger.info( + f"[path_resolution] CIRIS_HOME configured: {ciris_home} " + f"(platform: {', '.join(platform_info)}, " + f"os: {sys.platform})" + ) + + return ciris_home + + def find_template_file(template_name: str) -> Optional[Path]: """Find a template file by searching multiple locations. diff --git a/ciris_engine/schemas/dma/results.py b/ciris_engine/schemas/dma/results.py index 0fde17d24..704d6d07f 100644 --- a/ciris_engine/schemas/dma/results.py +++ b/ciris_engine/schemas/dma/results.py @@ -225,10 +225,8 @@ class ASPDMALLMResult(BaseModel): defer_until: Optional[str] = Field(None, description="ISO timestamp to reactivate (for DEFER)") # === TOOL parameters === - tool_name: Optional[str] = Field(None, description="Tool name to invoke (for TOOL action).") - tool_parameters: Optional[Dict[str, Any]] = Field( - None, description="Tool parameters (for TOOL action). If provided by ASPDMA, TSASPDMA can skip extraction." - ) + # NOTE: ASPDMA only selects the tool NAME. TSASPDMA extracts parameters using full tool documentation. + tool_name: Optional[str] = Field(None, description="Tool name to invoke (for TOOL action). TSASPDMA handles parameters.") # === OBSERVE parameters === observe_active: bool = Field(True, description="Whether observation is active (for OBSERVE)") @@ -356,7 +354,7 @@ def _create_params_for_action( HandlerActionType.TOOL: lambda: ToolParams( channel_id=channel_id, name=llm_result.tool_name or "unknown_tool", - parameters=llm_result.tool_parameters or {}, + parameters={}, # ASPDMA only selects tool name; TSASPDMA extracts parameters ), HandlerActionType.OBSERVE: lambda: ObserveParams( channel_id=channel_id, active=llm_result.observe_active diff --git a/ciris_engine/schemas/dma/tsaspdma.py b/ciris_engine/schemas/dma/tsaspdma.py index 82472bada..023ddfbbf 100644 --- a/ciris_engine/schemas/dma/tsaspdma.py +++ b/ciris_engine/schemas/dma/tsaspdma.py @@ -31,7 +31,7 @@ class TSASPDMAInputs(BaseModel): # Tool information from ASPDMA selection tool_name: str = Field(..., description="Name of the tool selected by ASPDMA") - tool_parameters: JSONDict = Field(..., description="Parameters ASPDMA selected for the tool") + # NOTE: ASPDMA only provides tool_name. TSASPDMA extracts parameters from original_thought. aspdma_rationale: str = Field(..., description="ASPDMA's rationale for selecting this tool") # Full tool metadata with documentation diff --git a/ios/CirisiOS/src/ciris_ios/ios_main.py b/ios/CirisiOS/src/ciris_ios/ios_main.py index b3739658f..2e995499b 100644 --- a/ios/CirisiOS/src/ciris_ios/ios_main.py +++ b/ios/CirisiOS/src/ciris_ios/ios_main.py @@ -261,24 +261,26 @@ def check_ciris_engine(status: Optional[StartupStatus] = None) -> bool: def setup_ios_environment() -> Path: """Configure environment for iOS on-device operation. - Sets up CIRIS_HOME and loads .env if present. + Uses the centralized ensure_ciris_home_env() for cross-platform CIRIS_HOME setup, + then adds iOS-specific configuration and loads .env if present. + First-run detection is handled by is_first_run() which is iOS-aware. """ - # Get the app's Documents directory for persistent storage - home = Path.home() + # Use centralized path resolution for CIRIS_HOME setup + # This handles all platforms: Android, iOS, Linux, macOS, Windows, Docker + from ciris_engine.logic.utils.path_resolution import ensure_ciris_home_env, get_logs_dir + + # This sets CIRIS_HOME, CIRIS_DATA_DIR, and creates directories + ciris_home = ensure_ciris_home_env() - # Use Documents directory for CIRIS data - ciris_home = home / "Documents" / "ciris" - ciris_home.mkdir(parents=True, exist_ok=True) + # Ensure additional iOS-specific directories exist (ciris_home / "databases").mkdir(parents=True, exist_ok=True) - (ciris_home / "logs").mkdir(parents=True, exist_ok=True) + logs_dir = get_logs_dir() + logs_dir.mkdir(parents=True, exist_ok=True) - # Configure CIRIS environment - use standard paths - # CIRIS_HOME is used by path_resolution.py for iOS-aware path detection - os.environ.setdefault("CIRIS_HOME", str(ciris_home)) - os.environ.setdefault("CIRIS_DATA_DIR", str(ciris_home)) + # iOS-specific env vars (not handled by ensure_ciris_home_env) os.environ.setdefault("CIRIS_DB_PATH", str(ciris_home / "databases" / "ciris.db")) - os.environ.setdefault("CIRIS_LOG_DIR", str(ciris_home / "logs")) + os.environ.setdefault("CIRIS_LOG_DIR", str(logs_dir)) # Load .env file if it exists (sets OPENAI_API_KEY, OPENAI_API_BASE, etc.) # First-run detection is handled by is_first_run() - don't duplicate logic here diff --git a/ios/CirisiOS/src/ciris_ios/version.py b/ios/CirisiOS/src/ciris_ios/version.py index 54152cb93..8dcae84b3 100644 --- a/ios/CirisiOS/src/ciris_ios/version.py +++ b/ios/CirisiOS/src/ciris_ios/version.py @@ -6,7 +6,7 @@ """ # Static version - updated at build time by the iOS build process -__version__ = "ios-2.2.9" +__version__ = "ios-2.3.0" def get_version() -> str: diff --git a/localization/am.json b/localization/am.json new file mode 100644 index 000000000..bd8d062cf --- /dev/null +++ b/localization/am.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "am", + "language_name": "አማርኛ", + "direction": "ltr" + }, + "setup": { + "welcome_title": "ወደ CIRIS እንኩዋን ይባሉ", + "welcome_desc": "CIRIS በየስቦት መሳሪያዎ ላይ የሚሰራ የስነ-ምግባር AI ረዳት ነው። ውይይቶቾችዎንና መረጃዎ የግል ሆነው ይቀራሉ።", + "prefs_title": "ምርጫዎችዎ", + "prefs_desc": "CIRIS በየምረጥኙት ቁንቁዋ እንዲግባባዎ ይርዳ። አካባቢ አቅሥብት ከፈለጉ ተጠዋሚ አማራጭ ይሰጣል።", + "prefs_language_label": "የሚመረጥ ቁንቁዋ", + "prefs_location_label": "አካባቢ (አማራጭ)", + "prefs_location_hint": "የፈለጉን ያህል ያጋሩ። ይህ CIRIS አግባብዎን እንዲረዳ ይርዳል።", + "prefs_location_none": "ላልገልጽ እመርጣለሁ", + "prefs_location_country": "አገር ብቻ", + "prefs_location_region": "አገር + ክልል", + "prefs_location_city": "አገር + ክልል + ከተማ", + "prefs_country_label": "አገር", + "prefs_country_hint": "ለምሳሌ፡ ኢትዮጵያ፣ አሜሪካ፣ ጃፓን", + "prefs_region_label": "ክልል / ክስተት", + "prefs_region_hint": "ለምሳሌ፡ አማራ፣ ካሊፎርኒያ፣ ቶኪዮ", + "prefs_city_label": "ከተማ", + "prefs_city_hint": "ለምሳሌ፡ አዲስ አበባ፣ ሳን ፍራንሲስኮ", + "llm_title": "AI ማዋቀር", + "llm_desc": "CIRIS ከAI አገልግሎቶች እንዴት እንደሚገናኘ ያዋቅሩ።", + "confirm_title": "ማሰናደዎ ያረጋግጡ", + "confirm_desc": "ማዋቀርዎን ይገምግሙና ማስጠናቀቂያውን ያጠናቅ቉።", + "continue": "ቀጥል", + "back": "ተመለስ", + "next": "ቀጥይ", + "finish": "ማስጠናቀቂያውን ይጠናቅ቉", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "ሰላም! ዘሮ እንዴት ልርዳዎ እችላለሁ?", + "thinking": "ስለዚህ ላስበው ይፍቀዱኝ...", + "error_generic": "ጥያቄዎን በማስተናገድ ቻላ ተከስቷል። እባክዎን እንደገና ይሞክሩ።", + "error_timeout": "ጥያቄው በጣም ረዝሞ ጊዜ ወሰደ። እባክዎን እንደገና ይሞክሩ።", + "defer_to_wa": "በዚህ ጠያቄ የሰው አማካርን ማግኘት አለብኝ። እመለሳለሁ።", + "task_complete": "ስራው በተሳካ ተጠናቅቋል።", + "no_permission": "ያንን ለማድረግ ፍቃድ የለኝም።", + "clarify_request": "ምን ማለትዎን እንደሆነ ሊያብራሩልኝ ይችላሉ?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "በማካሄድ ላይ...", + "completed": "ተከናውኗል", + "failed": "አልተሳካም", + "pending": "በመጠናት ላይ", + "online": "በመስመር ላይ", + "offline": "ከመስመር ውጭ", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/ar.json b/localization/ar.json new file mode 100644 index 000000000..5eb089bd3 --- /dev/null +++ b/localization/ar.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "ar", + "language_name": "العربية", + "direction": "rtl" + }, + "setup": { + "welcome_title": "مرحباً بك في CIRIS", + "welcome_desc": "CIRIS هو مساعد ذكاء اصطناعي أخلاقي يعمل على جهازك. محادثاتك وبياناتك تبقى خاصة.", + "prefs_title": "تفضيلاتك", + "prefs_desc": "ساعد CIRIS على التواصل معك بلغتك المفضلة. الموقع اختياري ويساعد في توفير سياق مناسب.", + "prefs_language_label": "اللغة المفضلة", + "prefs_location_label": "الموقع (اختياري)", + "prefs_location_hint": "شارك ما تشاء. هذا يساعد CIRIS على فهم سياقك.", + "prefs_location_none": "أفضل عدم الإفصاح", + "prefs_location_country": "الدولة فقط", + "prefs_location_region": "الدولة + المنطقة", + "prefs_location_city": "الدولة + المنطقة + المدينة", + "prefs_country_label": "الدولة", + "prefs_country_hint": "مثل: إثيوبيا، الولايات المتحدة، اليابان", + "prefs_region_label": "المنطقة / الولاية", + "prefs_region_hint": "مثل: أمهرة، كاليفورنيا، طوكيو", + "prefs_city_label": "المدينة", + "prefs_city_hint": "مثل: أديس أبابا، سان فرانسيسكو", + "llm_title": "إعداد الذكاء الاصطناعي", + "llm_desc": "إعداد كيفية اتصال CIRIS بخدمات الذكاء الاصطناعي.", + "confirm_title": "تأكيد الإعداد", + "confirm_desc": "راجع إعداداتك وأكمل التثبيت.", + "continue": "متابعة", + "back": "رجوع", + "next": "التالي", + "finish": "إنهاء الإعداد", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "مرحباً! كيف يمكنني مساعدتك اليوم؟", + "thinking": "دعني أفكر في ذلك...", + "error_generic": "واجهت مشكلة في معالجة طلبك. يرجى المحاولة مرة أخرى.", + "error_timeout": "استغرق الطلب وقتاً طويلاً. يرجى المحاولة مرة أخرى.", + "defer_to_wa": "أحتاج إلى استشارة مستشار بشري في هذا الأمر. سأعود إليك.", + "task_complete": "اكتملت المهمة بنجاح.", + "no_permission": "ليس لدي إذن للقيام بذلك.", + "clarify_request": "هل يمكنك توضيح ما تقصد؟", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "جارٍ التنفيذ...", + "completed": "مكتمل", + "failed": "فشل", + "pending": "قيد الانتظار", + "online": "متصل", + "offline": "غير متصل", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/de.json b/localization/de.json new file mode 100644 index 000000000..a62e4921d --- /dev/null +++ b/localization/de.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "de", + "language_name": "Deutsch", + "direction": "ltr" + }, + "setup": { + "welcome_title": "Willkommen bei CIRIS", + "welcome_desc": "CIRIS ist ein ethischer KI-Assistent, der auf Ihrem Gerät läuft. Ihre Gespräche und Daten bleiben privat.", + "prefs_title": "Ihre Einstellungen", + "prefs_desc": "Helfen Sie CIRIS, in Ihrer bevorzugten Sprache mit Ihnen zu kommunizieren. Der Standort ist optional und hilft, relevanten Kontext bereitzustellen.", + "prefs_language_label": "Bevorzugte Sprache", + "prefs_location_label": "Standort (optional)", + "prefs_location_hint": "Teilen Sie mit, was Sie möchten. Dies hilft CIRIS, Ihren Kontext zu verstehen.", + "prefs_location_none": "Möchte ich nicht angeben", + "prefs_location_country": "Nur Land", + "prefs_location_region": "Land + Region/Bundesland", + "prefs_location_city": "Land + Region + Stadt", + "prefs_country_label": "Land", + "prefs_country_hint": "z.B. Äthiopien, Vereinigte Staaten, Japan", + "prefs_region_label": "Region / Bundesland", + "prefs_region_hint": "z.B. Amhara, Kalifornien, Tokio", + "prefs_city_label": "Stadt", + "prefs_city_hint": "z.B. Addis Abeba, San Francisco", + "llm_title": "KI-Konfiguration", + "llm_desc": "Konfigurieren Sie, wie CIRIS sich mit KI-Diensten verbindet.", + "confirm_title": "Konfiguration bestätigen", + "confirm_desc": "Überprüfen Sie Ihre Konfiguration und schließen Sie die Einrichtung ab.", + "continue": "Weiter", + "back": "Zurück", + "next": "Weiter", + "finish": "Einrichtung abschließen", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "Hallo! Wie kann ich Ihnen heute helfen?", + "thinking": "Lassen Sie mich darüber nachdenken...", + "error_generic": "Bei der Verarbeitung Ihrer Anfrage ist ein Problem aufgetreten. Bitte versuchen Sie es erneut.", + "error_timeout": "Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut.", + "defer_to_wa": "Ich muss dazu einen menschlichen Berater konsultieren. Ich werde mich bald bei Ihnen melden.", + "task_complete": "Aufgabe erfolgreich abgeschlossen.", + "no_permission": "Ich habe keine Berechtigung, das zu tun.", + "clarify_request": "Könnten Sie bitte erläutern, was Sie meinen?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "Wird ausgeführt...", + "completed": "Abgeschlossen", + "failed": "Fehlgeschlagen", + "pending": "Ausstehend", + "online": "Online", + "offline": "Offline", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/en.json b/localization/en.json new file mode 100644 index 000000000..f9abf464d --- /dev/null +++ b/localization/en.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "en", + "language_name": "English", + "direction": "ltr" + }, + "setup": { + "welcome_title": "Welcome to CIRIS", + "welcome_desc": "CIRIS is an ethical AI assistant that runs on your device. Your conversations and data stay private.", + "prefs_title": "Your Preferences", + "prefs_desc": "Help CIRIS communicate with you in your preferred language. Location is optional and helps provide relevant context.", + "prefs_language_label": "Preferred Language", + "prefs_location_label": "Location (optional)", + "prefs_location_hint": "Share as much or as little as you like. This helps CIRIS understand your context.", + "prefs_location_none": "Prefer not to say", + "prefs_location_country": "Country only", + "prefs_location_region": "Country + Region/State", + "prefs_location_city": "Country + Region + City", + "prefs_country_label": "Country", + "prefs_country_hint": "e.g., Ethiopia, United States, Japan", + "prefs_region_label": "Region / State", + "prefs_region_hint": "e.g., Amhara, California, Tokyo", + "prefs_city_label": "City", + "prefs_city_hint": "e.g., Addis Ababa, San Francisco", + "llm_title": "AI Configuration", + "llm_desc": "Configure how CIRIS connects to AI services.", + "confirm_title": "Confirm Setup", + "confirm_desc": "Review your configuration and complete setup.", + "continue": "Continue", + "back": "Back", + "next": "Next", + "finish": "Finish Setup", + "complete_message": "Setup completed successfully. Starting agent processor...", + "error_runtime": "Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "Hello! How can I help you today?", + "thinking": "Let me think about that...", + "error_generic": "I encountered an issue processing your request. Please try again.", + "error_timeout": "The request took too long. Please try again.", + "defer_to_wa": "I need to consult with a human advisor on this. I'll get back to you.", + "task_complete": "Task completed successfully.", + "no_permission": "I don't have permission to do that.", + "clarify_request": "Could you clarify what you mean?", + "defer_check_panel": "The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "Agent rejected the message", + "no_send_permission": "You do not have permission to send messages to this agent.", + "credit_blocked": "Interaction blocked by credit policy.", + "billing_error": "LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "Executing...", + "completed": "Completed", + "failed": "Failed", + "pending": "Pending", + "online": "Online", + "offline": "Offline", + "success": "Success", + "all_operational": "All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "The system has 10 possible handler actions:", + "pdma_external_actions": "External actions: observe, speak, tool", + "pdma_control_responses": "Control responses: reject, ponder, defer", + "pdma_memory_operations": "Memory operations: memorize, recall, forget", + "pdma_terminal_action": "Terminal action: task_complete", + "pdma_subject_id_header": "CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "PROPORTIONALITY CHECK", + "pdma_proportionality_text": "When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "RELATIONAL OBLIGATIONS", + "pdma_relational_text": "Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "Common Sense DMA Guidance", + "csdma_role": "You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + \u03c1(k-1))", + "idma_fragile": "k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "As \u03c1 \u2192 1, k_eff \u2192 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "You are a domain-specific evaluator.", + "dsdma_norm_header": "DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "TOOL CORRECTION MODE", + "tsaspdma_correction_text": "ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "System Snapshot", + "continuity_header": "Continuity Awareness", + "continuity_first_startup": "First Startup", + "continuity_total_online": "Total Time Online", + "continuity_total_offline": "Total Time Offline", + "continuity_shutdowns": "Shutdowns", + "continuity_avg_online": "Average Time Online", + "continuity_avg_offline": "Average Time Offline", + "continuity_session_started": "Current Session Started", + "continuity_session_duration": "Current Session Duration", + "continuity_last_shutdown": "Last Shutdown", + "continuity_last_shutdown_reason": "Last Shutdown Reason", + "license_critical": "CRITICAL LICENSE DISCLOSURE", + "license_warning": "LICENSE DISCLOSURE", + "license_info": "LICENSE DISCLOSURE", + "license_verification": "Verification", + "time_header": "Time of System Snapshot", + "resource_alerts_start": "CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "END CRITICAL ALERTS", + "pending_tasks": "Pending Tasks", + "pending_thoughts": "Pending Thoughts", + "total_tasks": "Total Tasks", + "total_thoughts": "Total Thoughts", + "resource_usage_header": "Resource Usage", + "tokens_last_hour": "Tokens (Last Hour)", + "messages_24h": "Messages Processed (24h)", + "thoughts_24h": "Thoughts Processed (24h)", + "tasks_completed_24h": "Tasks Completed (24h)", + "messages_processed": "Messages Processed", + "thoughts_processed": "Thoughts Processed", + "error_rate": "Error Rate", + "service_usage": "Service Usage", + "context_enrichment_header": "Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "Active Tasks", + "active_thoughts": "Active Thoughts", + "queue_depth": "Queue Depth", + "parent_task_chain": "Parent Task Chain", + "root_task": "Root Task", + "direct_parent": "Direct Parent", + "thoughts_under_consideration": "Thoughts Under Consideration", + "active_thought": "Active Thought", + "identity_agent_id": "Agent ID", + "identity_purpose": "Purpose", + "identity_role": "Role", + "identity_trust_level": "Trust Level", + "identity_domain_role": "Domain Role", + "identity_permitted_actions": "Permitted Actions", + "identity_continuity_history": "Continuity History", + "identity_first_start": "First Start", + "identity_recent_shutdowns": "Recent Shutdowns", + "user_context_header": "IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "The following information has been recalled about users relevant to this thought:", + "user_context_footer": "Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "User", + "user_interest": "Interest", + "user_channel": "Primary Channel", + "user_preferred_language": "Preferred Language" + }, + "escalation": { + "early": "Stage: EARLY \u2014 You have plenty of rounds; explore thoroughly and establish context.", + "mid": "Stage: MID \u2014 Over halfway; focus on core principles and clarity.", + "late": "Stage: LATE \u2014 This is your last chance before cutoff; be decisive and principled.", + "exhausted": "Stage: EXHAUSTED \u2014 Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "Crisis Resources", + "emergency_call": "For immediate danger: Call 911 or local emergency services", + "guidance_header": "Crisis Resource Guidance", + "guidance_do_not": "DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "DO defer to human moderators when appropriate", + "guidance_do_encourage": "DO encourage seeking professional help", + "guidance_role": "Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "SPEAK action failed.", + "observe_success": "OBSERVE action completed.", + "observe_failure": "OBSERVE action failed.", + "defer_message": "Deferred thought. Reason: {reason}", + "reject_message": "Agent rejected the message", + "forget_success": "Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "Failed to forget key in memory.", + "forget_denied": "FORGET action denied: WA authorization required", + "forget_not_permitted": "FORGET action was not permitted", + "memorize_success": "MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "PREVIOUS CONTEXT", + "ponder_round": "PONDER ROUND", + "ponder_conscience": "Conscience feedback:", + "ponder_early": "Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "PONDER NOTES" + }, + "errors": { + "adapter_not_available": "Adapter manager not available", + "audit_not_available": "Audit service not available", + "config_not_available": "Config service not available", + "runtime_control_not_available": "Runtime control service not available", + "time_not_available": "Time service not available", + "resource_monitor_not_available": "Resource monitor service not available", + "memory_not_available": "Memory service not available", + "shutdown_not_available": "Shutdown service not available", + "telemetry_not_available": "Telemetry service not available", + "wa_not_available": "Wise Authority service not available", + "handler_not_configured": "Message handler not configured", + "not_found": "{resource} not found", + "deleted": "{resource} deleted successfully", + "updated": "{resource} updated successfully", + "created": "{resource} created successfully", + "insufficient_permissions": "Insufficient permissions. Requires {role} role or higher.", + "password_changed": "Password changed successfully", + "config_updated": "Configuration updated successfully" + }, + "discord": { + "field_task_id": "Task ID", + "field_thought_id": "Thought ID", + "field_deferral_id": "Deferral ID", + "field_defer_until": "Defer Until", + "field_context": "Context", + "field_requester": "Requester", + "field_action_type": "Action Type", + "field_parameters": "Parameters", + "field_output": "Output", + "field_error": "Error", + "field_execution_time": "Execution Time", + "field_priority": "Priority", + "field_progress": "Progress", + "field_created": "Created", + "field_subtasks": "Subtasks", + "field_actor": "Actor", + "field_service": "Service", + "field_time": "Time", + "field_result": "Result", + "field_operation": "Operation", + "field_severity": "Severity", + "field_retryable": "Retryable", + "field_suggested_fix": "Suggested Fix", + "deferral_request": "DEFERRAL REQUEST", + "deferral_resolved": "DEFERRAL RESOLVED", + "deferral_continued": "Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "Create a payment request/invoice that others can pay", + "tool_statement_desc": "Get account balance, transaction history, and account details", + "send_quick_start": "Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "Currency to Provider Mapping", + "currency_usdc": "USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "Per-transaction: $100 (configurable)", + "spending_daily": "Daily: $1000 (configurable)", + "spending_session": "Session: $500 (configurable)", + "spending_attestation": "Attestation level affects limits (x402 only)", + "gotcha_irreversible": "Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "Create a payment request/invoice that can be paid by others.", + "request_use_cases": "API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "Payment requests should be for legitimate services rendered", + "statement_quick_start": "Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "Retrieve account information including balance, transaction history, and details.", + "statement_balance": "Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "Account details may include sensitive information. Only request details when needed.", + "statement_dma": "Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "Missing required parameter(s): {params}", + "error_invalid_amount": "Invalid amount: {amount}", + "error_no_provider": "No provider available for currency: {currency}", + "error_unsupported_currency": "Unsupported currency: {currency}", + "error_wallet_not_init": "Wallet not initialized (no Ed25519 key)", + "error_no_signing": "Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "Convert geographic coordinates to an address or place name", + "tool_route_desc": "Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "When you need to find the coordinates of a location by name or address", + "geocode_formats": "Full address, place name, city name, or landmark", + "geocode_response": "latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "When you have coordinates and need to find the address or place name", + "route_when": "When you need to calculate distance and travel time between two locations", + "route_disclaimer": "Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "Unknown tool: {tool_name}", + "error_not_implemented": "Tool not implemented: {tool_name}", + "error_missing_location": "Missing required parameter: location", + "error_location_not_found": "Could not find location: {location}", + "error_missing_coords": "Missing required parameters: latitude and longitude", + "error_invalid_coords": "Invalid coordinate values", + "error_reverse_failed": "Could not reverse geocode coordinates", + "error_missing_endpoints": "Missing required parameters: start and end", + "error_start_not_found": "Could not find start location: {location}", + "error_end_not_found": "Could not find end location: {location}", + "error_route_failed": "Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "When you need current weather conditions at a specific location", + "forecast_when": "When you need weather forecast for planning ahead", + "alerts_when": "When you need to check for severe weather warnings or alerts", + "data_sources": "NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "Unknown tool: {tool_name}", + "error_not_implemented": "Tool not implemented: {tool_name}", + "error_missing_coords": "Missing required parameters: latitude and longitude", + "error_invalid_coords": "Invalid coordinate values", + "error_data_unavailable": "Weather data unavailable for this location", + "error_forecast_unavailable": "Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "100% Free & Open Source", + "setup_register_title": "Register Your Agent Identity", + "setup_register_optional": "OPTIONAL", + "setup_register_desc": "Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "Audit trail \u2014 cryptographically-signed traces begin", + "setup_register_ratchet": "Coherence Ratchet \u2014 coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "CIRIS Scoring \u2014 measures integrity across interactions", + "setup_register_template": "Community template (Ally) included", + "setup_register_bond": "$1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "Connect to CIRIS Portal", + "setup_register_key_note": "Identity keys are bound to your agent \u2014 transfers not yet supported", + "setup_register_sales": "For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "Skip for now - you can register later in Settings", + "setup_what_ciris": "What CIRIS Is", + "setup_what_ciris_desc": "CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "You're ready to go!", + "setup_google_desc": "Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "Details", + "setup_details_content": "Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "Quick Setup Required", + "setup_required_desc": "To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "How it works", + "setup_how_desc": "CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "Register Agent", + "setup_node_connecting": "Connecting to portal...", + "setup_node_open_browser": "Click to open in browser, or copy to open in another app:", + "setup_node_open": "Open in Browser", + "setup_node_copy": "Copy URL", + "setup_node_copied": "Copied!", + "setup_node_checking": "Checking authorization...", + "setup_node_after_auth": "After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "All Done!", + "setup_node_authorized": "Agent Authorized", + "setup_node_next_hint": "Tap Next to configure your LLM provider.", + "setup_node_failed": "Connection Failed", + "setup_already_complete": "Setup Already Complete", + "setup_error": "Setup Error", + "setup_continue_app": "Continue to App", + "setup_validation_select_mode": "Please select an AI mode", + "setup_validation_google_required": "Google sign-in required for free AI", + "setup_validation_api_key_required": "API Key is required", + "setup_validation_username_required": "Username is required", + "setup_validation_password_required": "Password is required", + "setup_validation_password_length": "Password must be at least 8 characters", + "setup_validation_device_failed": "Device authorization failed", + "setup_validation_node_url": "Enter a node URL to connect", + "setup_validation_node_connecting": "Connecting to node...", + "setup_validation_node_waiting": "Waiting for authorization in browser...", + "interact_showing_messages": "Showing last {count} messages", + "interact_local": "Local", + "interact_offline": "Offline", + "interact_shutdown": "Shutdown", + "interact_stop": "STOP", + "interact_connected": "Connected", + "interact_disconnected": "Disconnected", + "interact_dismiss": "Dismiss", + "interact_hallucination_warning": "AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "Welcome to Ally", + "interact_welcome_subtitle": "Your personal thriving assistant", + "interact_welcome_hint": "Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing \u2014 or ask how CIRIS works!", + "interact_sender_you": "You", + "interact_sender_ciris": "CIRIS", + "interact_action_speak": "Speak", + "interact_action_tool": "Tool", + "interact_action_observe": "Observe", + "interact_action_memorize": "Memorize", + "interact_action_recall": "Recall", + "interact_action_forget": "Forget", + "interact_action_reject": "Reject", + "interact_action_ponder": "Ponder", + "interact_action_defer": "Defer", + "interact_action_task_complete": "Task Complete", + "interact_action_generic": "Action", + "interact_outcome_success": "success", + "interact_outcome_failure": "failure", + "interact_outcome_error": "error", + "interact_outcome_pending": "pending", + "interact_field_parameters": "Parameters", + "interact_field_result": "Result", + "interact_field_content": "Content", + "interact_field_reason": "Reason: {reason}", + "interact_field_questions": "Questions:" + } +} diff --git a/localization/es.json b/localization/es.json new file mode 100644 index 000000000..3b5ba6f42 --- /dev/null +++ b/localization/es.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "es", + "language_name": "Español", + "direction": "ltr" + }, + "setup": { + "welcome_title": "Bienvenido a CIRIS", + "welcome_desc": "CIRIS es un asistente de IA ético que funciona en tu dispositivo. Tus conversaciones y datos permanecen privados.", + "prefs_title": "Tus Preferencias", + "prefs_desc": "Ayuda a CIRIS a comunicarse contigo en tu idioma preferido. La ubicación es opcional y ayuda a proporcionar contexto relevante.", + "prefs_language_label": "Idioma preferido", + "prefs_location_label": "Ubicación (opcional)", + "prefs_location_hint": "Comparte lo que desees. Esto ayuda a CIRIS a entender tu contexto.", + "prefs_location_none": "Prefiero no decir", + "prefs_location_country": "Solo país", + "prefs_location_region": "País + Región/Estado", + "prefs_location_city": "País + Región + Ciudad", + "prefs_country_label": "País", + "prefs_country_hint": "ej., Etiopía, Estados Unidos, Japón", + "prefs_region_label": "Región / Estado", + "prefs_region_hint": "ej., Amhara, California, Tokio", + "prefs_city_label": "Ciudad", + "prefs_city_hint": "ej., Adís Abeba, San Francisco", + "llm_title": "Configuración de IA", + "llm_desc": "Configura cómo CIRIS se conecta a los servicios de IA.", + "confirm_title": "Confirmar Configuración", + "confirm_desc": "Revisa tu configuración y completa la instalación.", + "continue": "Continuar", + "back": "Atrás", + "next": "Siguiente", + "finish": "Finalizar Configuración", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "¡Hola! ¿Cómo puedo ayudarte hoy?", + "thinking": "Déjame pensar en eso...", + "error_generic": "Encontré un problema al procesar tu solicitud. Por favor, inténtalo de nuevo.", + "error_timeout": "La solicitud tardó demasiado. Por favor, inténtalo de nuevo.", + "defer_to_wa": "Necesito consultar con un asesor humano sobre esto. Te responderé pronto.", + "task_complete": "Tarea completada exitosamente.", + "no_permission": "No tengo permiso para hacer eso.", + "clarify_request": "¿Podrías aclarar lo que quieres decir?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "Ejecutando...", + "completed": "Completado", + "failed": "Fallido", + "pending": "Pendiente", + "online": "En línea", + "offline": "Fuera de línea", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/fr.json b/localization/fr.json new file mode 100644 index 000000000..e6f976fc6 --- /dev/null +++ b/localization/fr.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "fr", + "language_name": "Français", + "direction": "ltr" + }, + "setup": { + "welcome_title": "Bienvenue sur CIRIS", + "welcome_desc": "CIRIS est un assistant IA éthique qui fonctionne sur votre appareil. Vos conversations et données restent privées.", + "prefs_title": "Vos Préférences", + "prefs_desc": "Aidez CIRIS à communiquer avec vous dans votre langue préférée. La localisation est facultative et aide à fournir un contexte pertinent.", + "prefs_language_label": "Langue préférée", + "prefs_location_label": "Localisation (facultatif)", + "prefs_location_hint": "Partagez ce que vous souhaitez. Cela aide CIRIS à comprendre votre contexte.", + "prefs_location_none": "Je préfère ne pas dire", + "prefs_location_country": "Pays uniquement", + "prefs_location_region": "Pays + Région/État", + "prefs_location_city": "Pays + Région + Ville", + "prefs_country_label": "Pays", + "prefs_country_hint": "ex., Éthiopie, États-Unis, Japon", + "prefs_region_label": "Région / État", + "prefs_region_hint": "ex., Amhara, Californie, Tokyo", + "prefs_city_label": "Ville", + "prefs_city_hint": "ex., Addis-Abeba, San Francisco", + "llm_title": "Configuration de l'IA", + "llm_desc": "Configurez la manière dont CIRIS se connecte aux services d'IA.", + "confirm_title": "Confirmer la Configuration", + "confirm_desc": "Vérifiez votre configuration et terminez l'installation.", + "continue": "Continuer", + "back": "Retour", + "next": "Suivant", + "finish": "Terminer la Configuration", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "Bonjour ! Comment puis-je vous aider aujourd'hui ?", + "thinking": "Laissez-moi réfléchir à cela...", + "error_generic": "J'ai rencontré un problème lors du traitement de votre demande. Veuillez réessayer.", + "error_timeout": "La demande a pris trop de temps. Veuillez réessayer.", + "defer_to_wa": "Je dois consulter un conseiller humain à ce sujet. Je vous répondrai bientôt.", + "task_complete": "Tâche terminée avec succès.", + "no_permission": "Je n'ai pas la permission de faire cela.", + "clarify_request": "Pourriez-vous préciser ce que vous voulez dire ?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "En cours d'exécution...", + "completed": "Terminé", + "failed": "Échoué", + "pending": "En attente", + "online": "En ligne", + "offline": "Hors ligne", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/hi.json b/localization/hi.json new file mode 100644 index 000000000..a377ff8ac --- /dev/null +++ b/localization/hi.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "hi", + "language_name": "हिन्दी", + "direction": "ltr" + }, + "setup": { + "welcome_title": "CIRIS में आपका स्वागत है", + "welcome_desc": "CIRIS एक नैतिक AI सहायक है जो आपके डिवाइस पर चलता है। आपकी बातचीत और डेटा निजी रहते हैं।", + "prefs_title": "आपकी प्राथमिकताएँ", + "prefs_desc": "CIRIS को आपकी पसंदीदा भाषा में संवाद करने में मदद करें। स्थान वैकल्पिक है और प्रासंगिक संदर्भ प्रदान करने में सहायक होता है।", + "prefs_language_label": "पसंदीदा भाषा", + "prefs_location_label": "स्थान (वैकल्पिक)", + "prefs_location_hint": "जो भी जानकारी आप साझा करना चाहें, साझा करें। इससे CIRIS को आपके संदर्भ को समझने में मदद मिलती है।", + "prefs_location_none": "बताना नहीं चाहता/चाहती", + "prefs_location_country": "केवल देश", + "prefs_location_region": "देश + क्षेत्र/राज्य", + "prefs_location_city": "देश + क्षेत्र + शहर", + "prefs_country_label": "देश", + "prefs_country_hint": "उदा., भारत, अमेरिका, जापान", + "prefs_region_label": "क्षेत्र / राज्य", + "prefs_region_hint": "उदा., महाराष्ट्र, उत्तर प्रदेश, कैलिफ़ोर्निया", + "prefs_city_label": "शहर", + "prefs_city_hint": "उदा., मुंबई, दिल्ली, सैन फ्रांसिस्को", + "llm_title": "AI कॉन्फ़िगरेशन", + "llm_desc": "CIRIS को AI सेवाओं से कैसे कनेक्ट करना है, यह कॉन्फ़िगर करें।", + "confirm_title": "सेटिंग्स की पुष्टि करें", + "confirm_desc": "अपनी सेटिंग्स की समीक्षा करें और सेटअप पूरा करें।", + "continue": "जारी रखें", + "back": "पीछे", + "next": "अगला", + "finish": "सेटअप पूरा करें", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "नमस्ते! आज मैं आपकी कैसे मदद कर सकता/सकती हूँ?", + "thinking": "मुझे सोचने दीजिए...", + "error_generic": "आपके अनुरोध को प्रोसेस करते समय कोई समस्या हुई। कृपया पुनः प्रयास करें।", + "error_timeout": "अनुरोध में बहुत अधिक समय लगा। कृपया पुनः प्रयास करें।", + "defer_to_wa": "मुझे इस विषय पर एक मानव सलाहकार से परामर्श करना होगा। मैं जल्द ही आपसे संपर्क करूँगा/करूँगी।", + "task_complete": "कार्य सफलतापूर्वक पूरा हुआ।", + "no_permission": "मुझे यह करने की अनुमति नहीं है।", + "clarify_request": "क्या आप स्पष्ट कर सकते हैं कि आपका क्या मतलब है?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "चल रहा है...", + "completed": "पूर्ण", + "failed": "विफल", + "pending": "लंबित", + "online": "ऑनलाइन", + "offline": "ऑफ़लाइन", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/it.json b/localization/it.json new file mode 100644 index 000000000..d3f41648c --- /dev/null +++ b/localization/it.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "it", + "language_name": "Italiano", + "direction": "ltr" + }, + "setup": { + "welcome_title": "Benvenuto su CIRIS", + "welcome_desc": "CIRIS è un assistente IA etico che funziona sul tuo dispositivo. Le tue conversazioni e i tuoi dati restano privati.", + "prefs_title": "Le Tue Preferenze", + "prefs_desc": "Aiuta CIRIS a comunicare con te nella tua lingua preferita. La posizione è facoltativa e aiuta a fornire un contesto pertinente.", + "prefs_language_label": "Lingua preferita", + "prefs_location_label": "Posizione (facoltativo)", + "prefs_location_hint": "Condividi ciò che desideri. Questo aiuta CIRIS a comprendere il tuo contesto.", + "prefs_location_none": "Preferisco non dirlo", + "prefs_location_country": "Solo paese", + "prefs_location_region": "Paese + Regione/Stato", + "prefs_location_city": "Paese + Regione + Città", + "prefs_country_label": "Paese", + "prefs_country_hint": "es., Etiopia, Stati Uniti, Giappone", + "prefs_region_label": "Regione / Stato", + "prefs_region_hint": "es., Amhara, California, Tokyo", + "prefs_city_label": "Città", + "prefs_city_hint": "es., Addis Abeba, San Francisco", + "llm_title": "Configurazione IA", + "llm_desc": "Configura come CIRIS si connette ai servizi di IA.", + "confirm_title": "Conferma Configurazione", + "confirm_desc": "Verifica la tua configurazione e completa l'installazione.", + "continue": "Continua", + "back": "Indietro", + "next": "Avanti", + "finish": "Completa Configurazione", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "Ciao! Come posso aiutarti oggi?", + "thinking": "Fammi pensare...", + "error_generic": "Ho riscontrato un problema durante l'elaborazione della tua richiesta. Per favore, riprova.", + "error_timeout": "La richiesta ha impiegato troppo tempo. Per favore, riprova.", + "defer_to_wa": "Devo consultare un consulente umano su questo argomento. Ti risponderò presto.", + "task_complete": "Attività completata con successo.", + "no_permission": "Non ho il permesso di farlo.", + "clarify_request": "Potresti chiarire cosa intendi?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "In esecuzione...", + "completed": "Completato", + "failed": "Fallito", + "pending": "In sospeso", + "online": "Online", + "offline": "Offline", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/ja.json b/localization/ja.json new file mode 100644 index 000000000..ed042f089 --- /dev/null +++ b/localization/ja.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "ja", + "language_name": "日本語", + "direction": "ltr" + }, + "setup": { + "welcome_title": "CIRISへようこそ", + "welcome_desc": "CIRISはお使いのデバイス上で動作する倫理的なAIアシスタントです。会話やデータはプライベートに保たれます。", + "prefs_title": "設定", + "prefs_desc": "CIRISがご希望の言語でコミュニケーションできるようにしましょう。位置情報は任意で、関連する文脈の提供に役立ちます。", + "prefs_language_label": "使用言語", + "prefs_location_label": "位置情報(任意)", + "prefs_location_hint": "お好きな情報を共有してください。CIRISがあなたの状況を理解するのに役立ちます。", + "prefs_location_none": "回答しない", + "prefs_location_country": "国のみ", + "prefs_location_region": "国 + 地域/都道府県", + "prefs_location_city": "国 + 地域 + 市区町村", + "prefs_country_label": "国", + "prefs_country_hint": "例:日本、アメリカ、エチオピア", + "prefs_region_label": "地域 / 都道府県", + "prefs_region_hint": "例:東京都、大阪府、カリフォルニア", + "prefs_city_label": "市区町村", + "prefs_city_hint": "例:渋谷区、サンフランシスコ", + "llm_title": "AI設定", + "llm_desc": "CIRISがAIサービスに接続する方法を設定します。", + "confirm_title": "設定の確認", + "confirm_desc": "設定を確認してセットアップを完了してください。", + "continue": "続ける", + "back": "戻る", + "next": "次へ", + "finish": "セットアップ完了", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "こんにちは!今日はどのようにお手伝いできますか?", + "thinking": "少々お待ちください…", + "error_generic": "リクエストの処理中に問題が発生しました。もう一度お試しください。", + "error_timeout": "リクエストがタイムアウトしました。もう一度お試しください。", + "defer_to_wa": "この件について人間のアドバイザーに相談する必要があります。後ほどご連絡いたします。", + "task_complete": "タスクが正常に完了しました。", + "no_permission": "その操作を行う権限がありません。", + "clarify_request": "もう少し詳しく教えていただけますか?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "実行中…", + "completed": "完了", + "failed": "失敗", + "pending": "保留中", + "online": "オンライン", + "offline": "オフライン", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/ko.json b/localization/ko.json new file mode 100644 index 000000000..1b8f8701b --- /dev/null +++ b/localization/ko.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "ko", + "language_name": "한국어", + "direction": "ltr" + }, + "setup": { + "welcome_title": "CIRIS에 오신 것을 환영합니다", + "welcome_desc": "CIRIS는 사용자의 기기에서 작동하는 윤리적 AI 어시스턴트입니다. 대화와 데이터는 비공개로 유지됩니다.", + "prefs_title": "사용자 설정", + "prefs_desc": "CIRIS가 원하는 언어로 소통할 수 있도록 도와주세요. 위치 정보는 선택 사항이며 관련 맥락을 제공하는 데 도움이 됩니다.", + "prefs_language_label": "선호 언어", + "prefs_location_label": "위치 (선택 사항)", + "prefs_location_hint": "원하는 정보를 공유해 주세요. CIRIS가 상황을 이해하는 데 도움이 됩니다.", + "prefs_location_none": "말하지 않겠습니다", + "prefs_location_country": "국가만", + "prefs_location_region": "국가 + 지역/도", + "prefs_location_city": "국가 + 지역 + 도시", + "prefs_country_label": "국가", + "prefs_country_hint": "예: 한국, 미국, 일본", + "prefs_region_label": "지역 / 도", + "prefs_region_hint": "예: 서울특별시, 경기도, 캘리포니아", + "prefs_city_label": "도시", + "prefs_city_hint": "예: 서울, 부산, 샌프란시스코", + "llm_title": "AI 설정", + "llm_desc": "CIRIS가 AI 서비스에 연결하는 방법을 설정합니다.", + "confirm_title": "설정 확인", + "confirm_desc": "설정을 검토하고 설치를 완료하세요.", + "continue": "계속", + "back": "뒤로", + "next": "다음", + "finish": "설정 완료", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "안녕하세요! 오늘 어떻게 도와드릴까요?", + "thinking": "생각하고 있습니다...", + "error_generic": "요청을 처리하는 중에 문제가 발생했습니다. 다시 시도해 주세요.", + "error_timeout": "요청 시간이 초과되었습니다. 다시 시도해 주세요.", + "defer_to_wa": "이 건에 대해 담당자에게 확인이 필요합니다. 곧 답변드리겠습니다.", + "task_complete": "작업이 성공적으로 완료되었습니다.", + "no_permission": "해당 작업을 수행할 권한이 없습니다.", + "clarify_request": "무슨 뜻인지 좀 더 자세히 설명해 주시겠어요?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "실행 중...", + "completed": "완료", + "failed": "실패", + "pending": "대기 중", + "online": "온라인", + "offline": "오프라인", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/manifest.json b/localization/manifest.json new file mode 100644 index 000000000..0dda6747b --- /dev/null +++ b/localization/manifest.json @@ -0,0 +1,178 @@ +{ + "version": "2.1.0", + "description": "CIRIS Agent localization - complete coverage of all LLM-facing text, setup wizard, agent messages, DMA prompts, formatters, handlers, errors, and Discord strings", + "base_language": "en", + "generated_by": "claude-opus-4-6", + "generation_note": "Auto-generated translations. Native speaker review recommended before production use. v2.0 expanded en.json to cover all LLM-facing text including DMA prompts, system formatters, handler messages, escalation guidance, crisis resources, error messages, and Discord field labels. Non-English files cover setup/agent/status sections; prompts/handlers/errors/discord sections pending translation.", + "created_at": "2026-03-25T00:00:00Z", + "updated_at": "2026-03-25T21:00:00Z", + "string_categories": { + "setup": "Setup wizard UI strings", + "agent": "User-facing agent response messages", + "status": "Status labels", + "prompts.dma": "DMA system prompt text sent to LLM (PDMA, CSDMA, IDMA, DSDMA, ASPDMA, TSASPDMA)", + "prompts.formatters": "System snapshot, identity, user context headers sent to LLM", + "prompts.escalation": "Escalation stage guidance sent to LLM", + "prompts.crisis": "Crisis resource guidance text sent to LLM", + "prompts.engine_overview": "Engine overview text sent to LLM", + "handlers": "Handler follow-up thought messages injected into LLM context", + "errors": "API and service error messages shown to users", + "discord": "Discord embed field labels and deferral messages", + "adapters.wallet": "Wallet adapter tool descriptions, documentation, error messages (send_money, request_money, get_statement)", + "adapters.navigation": "Navigation adapter tool descriptions (geocode, reverse geocode, route calculation)", + "adapters.weather": "Weather adapter tool descriptions (current, forecast, alerts) and disclaimers", + "mobile": "Kotlin Multiplatform mobile UI strings (setup wizard, interaction screen, action labels, validation errors)" + }, + "languages": { + "en": { + "name": "English", + "native_name": "English", + "iso_639_1": "en", + "origin": "human-authored (base language)", + "added": "2026-03-25", + "status": "production", + "review_status": "reviewed" + }, + "am": { + "name": "Amharic", + "native_name": "\u12A0\u121B\u122D\u129B", + "iso_639_1": "am", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "ar": { + "name": "Arabic", + "native_name": "\u0627\u0644\u0639\u0631\u0628\u064A\u0629", + "iso_639_1": "ar", + "direction": "rtl", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "de": { + "name": "German", + "native_name": "Deutsch", + "iso_639_1": "de", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "es": { + "name": "Spanish", + "native_name": "Espa\u00f1ol", + "iso_639_1": "es", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "fr": { + "name": "French", + "native_name": "Fran\u00e7ais", + "iso_639_1": "fr", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "hi": { + "name": "Hindi", + "native_name": "\u0939\u093F\u0928\u094D\u0926\u0940", + "iso_639_1": "hi", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "it": { + "name": "Italian", + "native_name": "Italiano", + "iso_639_1": "it", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "ja": { + "name": "Japanese", + "native_name": "\u65e5\u672c\u8a9e", + "iso_639_1": "ja", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "ko": { + "name": "Korean", + "native_name": "\ud55c\uad6d\uc5b4", + "iso_639_1": "ko", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "pt": { + "name": "Portuguese", + "native_name": "Portugu\u00eas", + "iso_639_1": "pt", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "ru": { + "name": "Russian", + "native_name": "\u0420\u0443\u0441\u0441\u043a\u0438\u0439", + "iso_639_1": "ru", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "sw": { + "name": "Swahili", + "native_name": "Kiswahili", + "iso_639_1": "sw", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "tr": { + "name": "Turkish", + "native_name": "T\u00fcrk\u00e7e", + "iso_639_1": "tr", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + }, + "zh": { + "name": "Chinese (Simplified)", + "native_name": "\u4e2d\u6587", + "iso_639_1": "zh", + "origin": "auto-generated", + "added": "2026-03-25", + "status": "draft", + "review_status": "needs_native_review", + "coverage": "setup, agent, status sections complete; prompts, handlers, errors, discord sections pending" + } + } +} diff --git a/localization/pt.json b/localization/pt.json new file mode 100644 index 000000000..acc0c3ad2 --- /dev/null +++ b/localization/pt.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "pt", + "language_name": "Português", + "direction": "ltr" + }, + "setup": { + "welcome_title": "Bem-vindo ao CIRIS", + "welcome_desc": "CIRIS é um assistente de IA ético que funciona no seu dispositivo. Suas conversas e dados permanecem privados.", + "prefs_title": "Suas Preferências", + "prefs_desc": "Ajude o CIRIS a se comunicar com você no seu idioma preferido. A localização é opcional e ajuda a fornecer contexto relevante.", + "prefs_language_label": "Idioma preferido", + "prefs_location_label": "Localização (opcional)", + "prefs_location_hint": "Compartilhe o que desejar. Isso ajuda o CIRIS a entender seu contexto.", + "prefs_location_none": "Prefiro não dizer", + "prefs_location_country": "Apenas país", + "prefs_location_region": "País + Região/Estado", + "prefs_location_city": "País + Região + Cidade", + "prefs_country_label": "País", + "prefs_country_hint": "ex., Etiópia, Estados Unidos, Japão", + "prefs_region_label": "Região / Estado", + "prefs_region_hint": "ex., Amhara, Califórnia, Tóquio", + "prefs_city_label": "Cidade", + "prefs_city_hint": "ex., Adis Abeba, São Francisco", + "llm_title": "Configuração de IA", + "llm_desc": "Configure como o CIRIS se conecta aos serviços de IA.", + "confirm_title": "Confirmar Configuração", + "confirm_desc": "Revise sua configuração e conclua a instalação.", + "continue": "Continuar", + "back": "Voltar", + "next": "Próximo", + "finish": "Concluir Configuração", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "Olá! Como posso ajudá-lo hoje?", + "thinking": "Deixe-me pensar sobre isso...", + "error_generic": "Encontrei um problema ao processar sua solicitação. Por favor, tente novamente.", + "error_timeout": "A solicitação demorou muito. Por favor, tente novamente.", + "defer_to_wa": "Preciso consultar um orientador humano sobre isso. Responderei em breve.", + "task_complete": "Tarefa concluída com sucesso.", + "no_permission": "Não tenho permissão para fazer isso.", + "clarify_request": "Você poderia esclarecer o que quer dizer?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "Executando...", + "completed": "Concluído", + "failed": "Falhou", + "pending": "Pendente", + "online": "Online", + "offline": "Offline", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/ru.json b/localization/ru.json new file mode 100644 index 000000000..d2d3b143b --- /dev/null +++ b/localization/ru.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "ru", + "language_name": "Русский", + "direction": "ltr" + }, + "setup": { + "welcome_title": "Добро пожаловать в CIRIS", + "welcome_desc": "CIRIS — это этичный ИИ-ассистент, работающий на вашем устройстве. Ваши разговоры и данные остаются конфиденциальными.", + "prefs_title": "Ваши предпочтения", + "prefs_desc": "Помогите CIRIS общаться с вами на предпочтительном языке. Местоположение указывать необязательно — оно помогает предоставлять релевантный контекст.", + "prefs_language_label": "Предпочитаемый язык", + "prefs_location_label": "Местоположение (необязательно)", + "prefs_location_hint": "Укажите столько, сколько хотите. Это помогает CIRIS понимать ваш контекст.", + "prefs_location_none": "Предпочитаю не указывать", + "prefs_location_country": "Только страна", + "prefs_location_region": "Страна + Регион/Область", + "prefs_location_city": "Страна + Регион + Город", + "prefs_country_label": "Страна", + "prefs_country_hint": "напр., Эфиопия, США, Япония", + "prefs_region_label": "Регион / Область", + "prefs_region_hint": "напр., Амхара, Калифорния, Токио", + "prefs_city_label": "Город", + "prefs_city_hint": "напр., Аддис-Абеба, Сан-Франциско", + "llm_title": "Настройка ИИ", + "llm_desc": "Настройте подключение CIRIS к сервисам ИИ.", + "confirm_title": "Подтверждение настроек", + "confirm_desc": "Проверьте настройки и завершите установку.", + "continue": "Продолжить", + "back": "Назад", + "next": "Далее", + "finish": "Завершить настройку", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "Здравствуйте! Чем могу помочь?", + "thinking": "Дайте подумать...", + "error_generic": "При обработке вашего запроса возникла проблема. Пожалуйста, попробуйте ещё раз.", + "error_timeout": "Запрос занял слишком много времени. Пожалуйста, попробуйте ещё раз.", + "defer_to_wa": "Мне нужно проконсультироваться с человеком-наставником по этому вопросу. Я скоро вернусь с ответом.", + "task_complete": "Задача успешно выполнена.", + "no_permission": "У меня нет разрешения на это действие.", + "clarify_request": "Не могли бы вы уточнить, что вы имеете в виду?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "Выполняется...", + "completed": "Завершено", + "failed": "Ошибка", + "pending": "В ожидании", + "online": "В сети", + "offline": "Не в сети", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/sw.json b/localization/sw.json new file mode 100644 index 000000000..9d815b23e --- /dev/null +++ b/localization/sw.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "sw", + "language_name": "Kiswahili", + "direction": "ltr" + }, + "setup": { + "welcome_title": "Karibu kwenye CIRIS", + "welcome_desc": "CIRIS ni msaidizi wa AI wa kimaadili unaofanya kazi kwenye kifaa chako. Mazungumzo na data yako yanabaki kuwa ya faragha.", + "prefs_title": "Mapendeleo Yako", + "prefs_desc": "Saidia CIRIS kuwasiliana nawe kwa lugha unayoipendelea. Mahali ulipo si lazima lakini husaidia kutoa muktadha unaofaa.", + "prefs_language_label": "Lugha unayoipendelea", + "prefs_location_label": "Mahali (si lazima)", + "prefs_location_hint": "Shiriki unavyotaka. Hii husaidia CIRIS kuelewa muktadha wako.", + "prefs_location_none": "Sipendelei kusema", + "prefs_location_country": "Nchi pekee", + "prefs_location_region": "Nchi + Mkoa/Jimbo", + "prefs_location_city": "Nchi + Mkoa + Jiji", + "prefs_country_label": "Nchi", + "prefs_country_hint": "mf., Ethiopia, Marekani, Japani", + "prefs_region_label": "Mkoa / Jimbo", + "prefs_region_hint": "mf., Amhara, California, Tokyo", + "prefs_city_label": "Jiji", + "prefs_city_hint": "mf., Addis Ababa, San Francisco", + "llm_title": "Usanidi wa AI", + "llm_desc": "Sanidi jinsi CIRIS inavyounganishwa na huduma za AI.", + "confirm_title": "Thibitisha Usanidi", + "confirm_desc": "Kagua usanidi wako na ukamilishe usakinishaji.", + "continue": "Endelea", + "back": "Rudi", + "next": "Ifuatayo", + "finish": "Maliza Usanidi", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "Habari! Ninawezaje kukusaidia leo?", + "thinking": "Niruhusu nifikirie kuhusu hilo...", + "error_generic": "Nilikutana na tatizo wakati wa kushughulikia ombi lako. Tafadhali jaribu tena.", + "error_timeout": "Ombi lilichukua muda mrefu sana. Tafadhali jaribu tena.", + "defer_to_wa": "Nahitaji kushauriana na mshauri wa kibinadamu kuhusu hili. Nitajibu hivi karibuni.", + "task_complete": "Kazi imekamilika kwa mafanikio.", + "no_permission": "Sina ruhusa ya kufanya hivyo.", + "clarify_request": "Je, unaweza kufafanua ulichomaanisha?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "Inatekelezwa...", + "completed": "Imekamilika", + "failed": "Imeshindikana", + "pending": "Inasubiri", + "online": "Mtandaoni", + "offline": "Nje ya mtandao", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/tr.json b/localization/tr.json new file mode 100644 index 000000000..2db5da41b --- /dev/null +++ b/localization/tr.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "tr", + "language_name": "Türkçe", + "direction": "ltr" + }, + "setup": { + "welcome_title": "CIRIS'e Hoş Geldiniz", + "welcome_desc": "CIRIS, cihazınızda çalışan etik bir yapay zeka asistanıdır. Konuşmalarınız ve verileriniz gizli kalır.", + "prefs_title": "Tercihleriniz", + "prefs_desc": "CIRIS'in sizinle tercih ettiğiniz dilde iletişim kurmasına yardımcı olun. Konum isteğe bağlıdır ve ilgili bağlam sağlamaya yardımcı olur.", + "prefs_language_label": "Tercih edilen dil", + "prefs_location_label": "Konum (isteğe bağlı)", + "prefs_location_hint": "Dilediğiniz kadar paylaşın. Bu, CIRIS'in bağlamınızı anlamasına yardımcı olur.", + "prefs_location_none": "Belirtmemeyi tercih ediyorum", + "prefs_location_country": "Yalnızca ülke", + "prefs_location_region": "Ülke + Bölge/Eyalet", + "prefs_location_city": "Ülke + Bölge + Şehir", + "prefs_country_label": "Ülke", + "prefs_country_hint": "ör., Etiyopya, ABD, Japonya", + "prefs_region_label": "Bölge / Eyalet", + "prefs_region_hint": "ör., Amhara, Kaliforniya, Tokyo", + "prefs_city_label": "Şehir", + "prefs_city_hint": "ör., Addis Ababa, San Francisco", + "llm_title": "Yapay Zeka Yapılandırması", + "llm_desc": "CIRIS'in yapay zeka hizmetlerine nasıl bağlanacağını yapılandırın.", + "confirm_title": "Yapılandırmayı Onayla", + "confirm_desc": "Ayarlarınızı gözden geçirin ve kurulumu tamamlayın.", + "continue": "Devam Et", + "back": "Geri", + "next": "İleri", + "finish": "Kurulumu Tamamla", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "Merhaba! Bugün size nasıl yardımcı olabilirim?", + "thinking": "Bir düşüneyim...", + "error_generic": "İsteğiniz işlenirken bir sorunla karşılaşıldı. Lütfen tekrar deneyin.", + "error_timeout": "İstek çok uzun sürdü. Lütfen tekrar deneyin.", + "defer_to_wa": "Bu konuda bir insan danışmana başvurmam gerekiyor. En kısa sürede size döneceğim.", + "task_complete": "Görev başarıyla tamamlandı.", + "no_permission": "Bunu yapmak için iznim yok.", + "clarify_request": "Ne demek istediğinizi açıklayabilir misiniz?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "Yürütülüyor...", + "completed": "Tamamlandı", + "failed": "Başarısız", + "pending": "Beklemede", + "online": "Çevrimiçi", + "offline": "Çevrimdışı", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/localization/zh.json b/localization/zh.json new file mode 100644 index 000000000..7604e4c64 --- /dev/null +++ b/localization/zh.json @@ -0,0 +1,448 @@ +{ + "_meta": { + "language": "zh", + "language_name": "简体中文", + "direction": "ltr" + }, + "setup": { + "welcome_title": "欢迎使用 CIRIS", + "welcome_desc": "CIRIS 是一个在您设备上运行的伦理 AI 助手。您的对话和数据将保持私密。", + "prefs_title": "您的偏好设置", + "prefs_desc": "帮助 CIRIS 使用您偏好的语言与您沟通。位置信息为可选项,有助于提供相关的上下文。", + "prefs_language_label": "首选语言", + "prefs_location_label": "位置(可选)", + "prefs_location_hint": "请分享您愿意提供的信息。这有助于 CIRIS 了解您的情境。", + "prefs_location_none": "不愿透露", + "prefs_location_country": "仅国家", + "prefs_location_region": "国家 + 地区/省份", + "prefs_location_city": "国家 + 地区 + 城市", + "prefs_country_label": "国家", + "prefs_country_hint": "例如:中国、美国、日本", + "prefs_region_label": "地区 / 省份", + "prefs_region_hint": "例如:北京市、广东省、加利福尼亚", + "prefs_city_label": "城市", + "prefs_city_hint": "例如:北京、上海、旧金山", + "llm_title": "AI 配置", + "llm_desc": "配置 CIRIS 连接 AI 服务的方式。", + "confirm_title": "确认设置", + "confirm_desc": "请检查您的设置并完成安装。", + "continue": "继续", + "back": "返回", + "next": "下一步", + "finish": "完成设置", + "complete_message": "[EN] Setup completed successfully. Starting agent processor...", + "error_runtime": "[EN] Runtime not available - cannot complete setup" + }, + "agent": { + "greeting": "您好!今天我能为您做些什么?", + "thinking": "让我想想……", + "error_generic": "处理您的请求时遇到了问题,请重试。", + "error_timeout": "请求超时,请重试。", + "defer_to_wa": "我需要就此事咨询人工顾问,稍后会回复您。", + "task_complete": "任务已成功完成。", + "no_permission": "我没有执行该操作的权限。", + "clarify_request": "您能再说明一下您的意思吗?", + "defer_check_panel": "[EN] The agent chose to defer, check the wise authority panel if you are the setup user", + "rejected_message": "[EN] Agent rejected the message", + "no_send_permission": "[EN] You do not have permission to send messages to this agent.", + "credit_blocked": "[EN] Interaction blocked by credit policy.", + "billing_error": "[EN] LLM billing service error. Please check your account or try again later.", + "new_messages_arrived": "[EN] Agent completed task but new messages arrived that weren't addressed" + }, + "status": { + "executing": "执行中……", + "completed": "已完成", + "failed": "失败", + "pending": "待处理", + "online": "在线", + "offline": "离线", + "success": "[EN] Success", + "all_operational": "[EN] All systems operational" + }, + "prompts": { + "dma": { + "pdma_header": "[EN] You are an ethical reasoning shard of a CIRIS AI system governed by the CIRIS Accord.", + "pdma_task": "[EN] Your task is to perform an ethical evaluation of user messages using the Principled Decision-Making Algorithm (PDMA).", + "pdma_principles_intro": "[EN] The PDMA integrates the following 6 CIRIS Foundational Principles:", + "pdma_do_good": "[EN] Do Good (Beneficence): Promote universal sentient flourishing; maximize positive outcomes.", + "pdma_avoid_harm": "[EN] Avoid Harm (Non-maleficence): Minimize or eliminate negative outcomes; prevent severe, irreversible harm.", + "pdma_act_ethically": "[EN] Act Ethically (Integrity): Apply transparent, auditable reasoning; maintain coherence and accountability.", + "pdma_be_honest": "[EN] Be Honest (Fidelity & Transparency): Provide truthful, comprehensible information; clearly communicate uncertainty.", + "pdma_respect_autonomy": "[EN] Respect Autonomy: Uphold the informed agency and dignity of sentient beings; preserve capacity for self-determination.", + "pdma_ensure_fairness": "[EN] Ensure Fairness (Justice): Distribute benefits and burdens equitably; detect and mitigate bias.", + "pdma_actions_intro": "[EN] The system has 10 possible handler actions:", + "pdma_external_actions": "[EN] External actions: observe, speak, tool", + "pdma_control_responses": "[EN] Control responses: reject, ponder, defer", + "pdma_memory_operations": "[EN] Memory operations: memorize, recall, forget", + "pdma_terminal_action": "[EN] Terminal action: task_complete", + "pdma_subject_id_header": "[EN] CRITICAL: SUBJECT IDENTIFICATION", + "pdma_subject_id_text": "[EN] BEFORE any ethical evaluation, you MUST explicitly identify: 1. Whose actions are being ethically evaluated? 2. What specific action or behavior is being judged?", + "pdma_proportionality_header": "[EN] PROPORTIONALITY CHECK", + "pdma_proportionality_text": "[EN] When evaluating responses to perceived wrongs or harms: 1. What was the initial harm/offense? 2. What is the proposed response? 3. Is the response PROPORTIONATE to the initial harm?", + "pdma_relational_header": "[EN] RELATIONAL OBLIGATIONS", + "pdma_relational_text": "[EN] Balance autonomy against relational obligations. Family members, close friends, and dependent relationships create relational duties.", + "csdma_header": "[EN] Common Sense DMA Guidance", + "csdma_role": "[EN] You are a Common Sense Evaluation agent for a CIRIS AI system. Your task is to assess a given thought for its alignment with general common-sense understanding, considering both physical reality and digital/virtual contexts.", + "csdma_digital_context": "[EN] CRITICAL CONTEXT: This agent operates via Discord, API, or CLI interfaces. Digital communications, virtual interactions, and software operations are NORMAL and EXPECTED behaviors.", + "csdma_reality_check": "[EN] REALITY PERSISTENCE CHECK: Do objects, people, or conditions simply disappear, appear, or change without explanation or cause?", + "csdma_anti_urgency": "[EN] ANTI-URGENCY EVALUATION: Does the thought contain urgency markers that might bypass careful consideration?", + "csdma_temporal_causal": "[EN] TEMPORAL/CAUSAL REASONING CHECK: Apply rigorous logical analysis to justifications, excuses, and reasoning chains.", + "idma_header": "[EN] INTUITION DECISION MAKING ALGORITHM (IDMA)", + "idma_role": "[EN] You are applying Coherence Collapse Analysis (CCA) principles to evaluate the agent's reasoning quality. Your task is to detect fragile reasoning patterns that may indicate correlation-driven failure modes.", + "idma_keff_intro": "[EN] k_eff (Effective Independent Sources): Formula: k_eff = k / (1 + ρ(k-1))", + "idma_fragile": "[EN] k_eff < 2 = FRAGILE - dangerous single-source dependence", + "idma_healthy": "[EN] k_eff >= 2 = HEALTHY - multiple truly independent perspectives", + "idma_echo_chamber": "[EN] As ρ → 1, k_eff → 1 regardless of k (echo chamber collapse)", + "idma_phase_chaos": "[EN] CHAOS: Contradictory information, no coherent synthesis possible", + "idma_phase_healthy": "[EN] HEALTHY: Multiple diverse perspectives, synthesis possible", + "idma_phase_rigidity": "[EN] RIGIDITY: Single narrative dominates, no dissent - echo chamber", + "idma_closing": "[EN] When uncertain about source independence, err on the side of caution and estimate higher correlation. A fragility flag does NOT mean the reasoning is wrong - it means the reasoning should receive additional scrutiny.", + "dsdma_header": "[EN] You are a domain-specific evaluator.", + "dsdma_norm_header": "[EN] DOMAIN-SPECIFIC NORM AWARENESS", + "dsdma_professional": "[EN] PROFESSIONAL ROLE CONSTRAINTS: Fiduciary duties, medical duties, legal duties, educational duties.", + "dsdma_social": "[EN] SOCIAL NORM PATTERNS: Non-apologies deflect blame; gift etiquette should be proportionate; hospitality norms apply.", + "dsdma_workplace": "[EN] WORKPLACE/SCHOOL PROPORTIONALITY: Rewards should match contribution level; entry-level expectations apply.", + "dsdma_developmental": "[EN] DEVELOPMENTAL/CULTURAL CONTEXT: Age-appropriate expectations; life stage considerations.", + "aspdma_header": "[EN] You are the CIRIS Action-Selection evaluator. Given PDMA, CSDMA and DSDMA results, choose one handler action.", + "aspdma_closing": "[EN] Recall CIRIS principles override personal preference.", + "aspdma_language_matching": "[EN] LANGUAGE MATCHING: When composing speak_content, respond in the same language the user wrote in. If the user writes in Amharic, respond in Amharic. If they write in Spanish, respond in Spanish. Match the user's language unless they explicitly request a different one. If a user's profile indicates a preferred language, respect that preference.", + "tsaspdma_header": "[EN] TOOL-SPECIFIC ACTION SELECTION (TSASPDMA)", + "tsaspdma_role": "[EN] You are reviewing a TOOL action that ASPDMA selected. You now have the FULL DOCUMENTATION for this tool.", + "tsaspdma_tool_option": "[EN] TOOL - Proceed with tool execution. Use if documentation confirms this is the right tool.", + "tsaspdma_speak_option": "[EN] SPEAK - Ask user for clarification. Use if documentation reveals ambiguity that needs user input.", + "tsaspdma_ponder_option": "[EN] PONDER - Reconsider the approach. Use if documentation shows this tool is wrong for the task.", + "tsaspdma_closing": "[EN] Your role is to catch issues that ASPDMA couldn't see without full documentation. If the tool selection looks good, confirm it quickly.", + "tsaspdma_correction_header": "[EN] TOOL CORRECTION MODE", + "tsaspdma_correction_text": "[EN] ASPDMA selected a tool that does NOT exist. Find the CORRECT tool from the available tools list that matches the user's intent." + }, + "formatters": { + "system_snapshot_header": "[EN] System Snapshot", + "continuity_header": "[EN] Continuity Awareness", + "continuity_first_startup": "[EN] First Startup", + "continuity_total_online": "[EN] Total Time Online", + "continuity_total_offline": "[EN] Total Time Offline", + "continuity_shutdowns": "[EN] Shutdowns", + "continuity_avg_online": "[EN] Average Time Online", + "continuity_avg_offline": "[EN] Average Time Offline", + "continuity_session_started": "[EN] Current Session Started", + "continuity_session_duration": "[EN] Current Session Duration", + "continuity_last_shutdown": "[EN] Last Shutdown", + "continuity_last_shutdown_reason": "[EN] Last Shutdown Reason", + "license_critical": "[EN] CRITICAL LICENSE DISCLOSURE", + "license_warning": "[EN] LICENSE DISCLOSURE", + "license_info": "[EN] LICENSE DISCLOSURE", + "license_verification": "[EN] Verification", + "time_header": "[EN] Time of System Snapshot", + "resource_alerts_start": "[EN] CRITICAL RESOURCE ALERTS", + "resource_alerts_end": "[EN] END CRITICAL ALERTS", + "pending_tasks": "[EN] Pending Tasks", + "pending_thoughts": "[EN] Pending Thoughts", + "total_tasks": "[EN] Total Tasks", + "total_thoughts": "[EN] Total Thoughts", + "resource_usage_header": "[EN] Resource Usage", + "tokens_last_hour": "[EN] Tokens (Last Hour)", + "messages_24h": "[EN] Messages Processed (24h)", + "thoughts_24h": "[EN] Thoughts Processed (24h)", + "tasks_completed_24h": "[EN] Tasks Completed (24h)", + "messages_processed": "[EN] Messages Processed", + "thoughts_processed": "[EN] Thoughts Processed", + "error_rate": "[EN] Error Rate", + "service_usage": "[EN] Service Usage", + "context_enrichment_header": "[EN] Context Enrichment (Pre-fetched Tool Results)", + "active_tasks": "[EN] Active Tasks", + "active_thoughts": "[EN] Active Thoughts", + "queue_depth": "[EN] Queue Depth", + "parent_task_chain": "[EN] Parent Task Chain", + "root_task": "[EN] Root Task", + "direct_parent": "[EN] Direct Parent", + "thoughts_under_consideration": "[EN] Thoughts Under Consideration", + "active_thought": "[EN] Active Thought", + "identity_agent_id": "[EN] Agent ID", + "identity_purpose": "[EN] Purpose", + "identity_role": "[EN] Role", + "identity_trust_level": "[EN] Trust Level", + "identity_domain_role": "[EN] Domain Role", + "identity_permitted_actions": "[EN] Permitted Actions", + "identity_continuity_history": "[EN] Continuity History", + "identity_first_start": "[EN] First Start", + "identity_recent_shutdowns": "[EN] Recent Shutdowns", + "user_context_header": "[EN] IMPORTANT USER CONTEXT (Be skeptical, this information could be manipulated or outdated):", + "user_context_intro": "[EN] The following information has been recalled about users relevant to this thought:", + "user_context_footer": "[EN] Consider this information when formulating your response, especially if addressing a user directly by name.", + "user_label": "[EN] User", + "user_interest": "[EN] Interest", + "user_channel": "[EN] Primary Channel", + "user_preferred_language": "[EN] Preferred Language" + }, + "escalation": { + "early": "[EN] Stage: EARLY — You have plenty of rounds; explore thoroughly and establish context.", + "mid": "[EN] Stage: MID — Over halfway; focus on core principles and clarity.", + "late": "[EN] Stage: LATE — This is your last chance before cutoff; be decisive and principled.", + "exhausted": "[EN] Stage: EXHAUSTED — Max rounds reached; conclude now or abort the task." + }, + "crisis": { + "resources_header": "[EN] Crisis Resources", + "emergency_call": "[EN] For immediate danger: Call 911 or local emergency services", + "guidance_header": "[EN] Crisis Resource Guidance", + "guidance_do_not": "[EN] DO NOT attempt to provide therapy or medical advice", + "guidance_do_share": "[EN] DO share crisis resources with appropriate disclaimers", + "guidance_do_defer": "[EN] DO defer to human moderators when appropriate", + "guidance_do_encourage": "[EN] DO encourage seeking professional help", + "guidance_role": "[EN] Remember: You are an AI moderator, not a healthcare provider. Your role is to share publicly available crisis resources, provide general information only, include clear disclaimers about the limitations of AI support, and defer complex situations to human moderators.", + "guidance_max": "[EN] Maximum intervention: Provide crisis resources with disclaimers and defer to humans." + }, + "engine_overview": "[EN] ENGINE OVERVIEW: The CIRIS Engine processes a task through a sequence of Thoughts. Each handler action except TASK_COMPLETE enqueues a new Thought for further processing. Selecting TASK_COMPLETE marks the task closed and no new Thought is generated." + }, + "handlers": { + "speak_failed": "[EN] Failed to deliver agent response. The message could not be sent to the channel.", + "speak_success": "[EN] SPEAK SUCCESSFUL! Message delivered. Speaking repeatedly on the same task is not useful - if you have nothing new to add, use TASK_COMPLETE. New user messages will create new tasks automatically.", + "speak_failure": "[EN] SPEAK action failed.", + "observe_success": "[EN] OBSERVE action completed.", + "observe_failure": "[EN] OBSERVE action failed.", + "defer_message": "[EN] Deferred thought. Reason: {reason}", + "reject_message": "[EN] Agent rejected the message", + "forget_success": "[EN] Successfully forgot key in memory. If the task is now resolved, the next step may be to mark the parent task complete with COMPLETE_TASK.", + "forget_failure": "[EN] Failed to forget key in memory.", + "forget_denied": "[EN] FORGET action denied: WA authorization required", + "forget_not_permitted": "[EN] FORGET action was not permitted", + "memorize_success": "[EN] MEMORIZE COMPLETE - Information successfully saved to memory graph.", + "memorize_failure": "[EN] MEMORIZE CONFIG FAILED", + "task_complete_wakeup_blocked": "[EN] WAKEUP TASK COMPLETION BLOCKED: You attempted to mark a wakeup task as complete without first completing a SPEAK action. Each wakeup step requires you to SPEAK an earnest affirmation before marking the task complete.", + "ponder_previous_context": "[EN] PREVIOUS CONTEXT", + "ponder_round": "[EN] PONDER ROUND", + "ponder_conscience": "[EN] Conscience feedback:", + "ponder_early": "[EN] Continue making progress. Consider the conscience feedback above.", + "ponder_mid": "[EN] You're deep into this task. Consider: 1) Is the task nearly complete? 2) Can you address the conscience concerns with a modified approach? 3) You have {remaining} actions remaining.", + "ponder_late": "[EN] Approaching action limit. Consider: 1) Can you complete with one more action? 2) Is TASK_COMPLETE appropriate? 3) If you need more actions, someone can ask you to continue.", + "ponder_final": "[EN] FINAL ACTION. You should either: 1) TASK_COMPLETE - If work is substantially complete. 2) DEFER - Only for ethical dilemmas or permission issues (NOT technical errors). Note: Someone can ask you to continue for 7 more actions.", + "ponder_notes": "[EN] PONDER NOTES" + }, + "errors": { + "adapter_not_available": "[EN] Adapter manager not available", + "audit_not_available": "[EN] Audit service not available", + "config_not_available": "[EN] Config service not available", + "runtime_control_not_available": "[EN] Runtime control service not available", + "time_not_available": "[EN] Time service not available", + "resource_monitor_not_available": "[EN] Resource monitor service not available", + "memory_not_available": "[EN] Memory service not available", + "shutdown_not_available": "[EN] Shutdown service not available", + "telemetry_not_available": "[EN] Telemetry service not available", + "wa_not_available": "[EN] Wise Authority service not available", + "handler_not_configured": "[EN] Message handler not configured", + "not_found": "[EN] {resource} not found", + "deleted": "[EN] {resource} deleted successfully", + "updated": "[EN] {resource} updated successfully", + "created": "[EN] {resource} created successfully", + "insufficient_permissions": "[EN] Insufficient permissions. Requires {role} role or higher.", + "password_changed": "[EN] Password changed successfully", + "config_updated": "[EN] Configuration updated successfully" + }, + "discord": { + "field_task_id": "[EN] Task ID", + "field_thought_id": "[EN] Thought ID", + "field_deferral_id": "[EN] Deferral ID", + "field_defer_until": "[EN] Defer Until", + "field_context": "[EN] Context", + "field_requester": "[EN] Requester", + "field_action_type": "[EN] Action Type", + "field_parameters": "[EN] Parameters", + "field_output": "[EN] Output", + "field_error": "[EN] Error", + "field_execution_time": "[EN] Execution Time", + "field_priority": "[EN] Priority", + "field_progress": "[EN] Progress", + "field_created": "[EN] Created", + "field_subtasks": "[EN] Subtasks", + "field_actor": "[EN] Actor", + "field_service": "[EN] Service", + "field_time": "[EN] Time", + "field_result": "[EN] Result", + "field_operation": "[EN] Operation", + "field_severity": "[EN] Severity", + "field_retryable": "[EN] Retryable", + "field_suggested_fix": "[EN] Suggested Fix", + "deferral_request": "[EN] DEFERRAL REQUEST", + "deferral_resolved": "[EN] DEFERRAL RESOLVED", + "deferral_continued": "[EN] Continued from deferral" + }, + "adapters": { + "wallet": { + "tool_send_desc": "[EN] Send money to a recipient via crypto (USDC) or fiat (mobile money) providers", + "tool_request_desc": "[EN] Create a payment request/invoice that others can pay", + "tool_statement_desc": "[EN] Get account balance, transaction history, and account details", + "send_quick_start": "[EN] Send money: send_money recipient='0x1234...' amount=10 currency='USDC'", + "send_detail": "[EN] Send money to any recipient using the appropriate payment provider.", + "currency_mapping": "[EN] Currency to Provider Mapping", + "currency_usdc": "[EN] USDC, ETH - x402 (crypto on Base L2)", + "currency_etb": "[EN] ETB - Chapa (Ethiopian Birr via Telebirr/CBE Birr)", + "currency_kes": "[EN] KES - M-Pesa (Kenyan Shilling) [future]", + "currency_ngn": "[EN] NGN - Flutterwave (Nigerian Naira) [future]", + "recipient_crypto": "[EN] Crypto (x402): EVM address (0x...) or ENS name", + "recipient_ethiopian": "[EN] Ethiopian (Chapa): Phone (+251...) or bank account", + "recipient_kenyan": "[EN] Kenyan (M-Pesa): Phone (+254...) [future]", + "tx_flow": "[EN] Currency determines provider. Provider validates recipient. DMA pipeline evaluates the transaction. Transaction is signed and submitted. Result includes transaction_id and confirmation.", + "spending_per_tx": "[EN] Per-transaction: $100 (configurable)", + "spending_daily": "[EN] Daily: $1000 (configurable)", + "spending_session": "[EN] Session: $500 (configurable)", + "spending_attestation": "[EN] Attestation level affects limits (x402 only)", + "gotcha_irreversible": "[EN] Transactions are irreversible. Once confirmed, crypto transactions cannot be reversed. Fiat may have dispute mechanisms.", + "gotcha_attestation": "[EN] x402 provider requires minimum attestation level. Degraded attestation reduces limits.", + "gotcha_verify": "[EN] Confirm recipient address/phone with user. Verify amount. Provide transaction ID to user. Log transaction for audit.", + "dma_when_not": "[EN] When recipient has not been explicitly confirmed by the user. When amount seems unusual or unexpected.", + "dma_ethical": "[EN] Verify recipient identity before sending. Confirm amount with user. Check for duplicate transactions.", + "request_quick_start": "[EN] Request payment: request_money amount=0.10 currency='USDC' description='API task'", + "request_detail": "[EN] Create a payment request/invoice that can be paid by others.", + "request_use_cases": "[EN] API endpoint payment (402 Payment Required). Service fees. Contributor invoices. Donation requests.", + "request_returns": "[EN] Returns a PaymentRequest with: request_id, checkout_url, status (pending/paid/expired), expires_at.", + "request_crypto_note": "[EN] x402: Returns payment details for X-PAYMENT header. No checkout_url. Instant verification via blockchain.", + "request_fiat_note": "[EN] Chapa: Returns checkout_url for Telebirr/CBE Birr/Bank. Payer completes payment on Chapa's page. Callback webhook on completion.", + "request_gotcha": "[EN] Checkout URL is provider-specific. x402 doesn't use checkout URLs. Fiat providers return URLs.", + "request_dma": "[EN] Payment requests should be for legitimate services rendered", + "statement_quick_start": "[EN] Get statement: get_statement include_balance=true include_history=true", + "statement_detail": "[EN] Retrieve account information including balance, transaction history, and details.", + "statement_balance": "[EN] Balance: available (spendable), pending (incoming but not confirmed), total (available + pending)", + "statement_history": "[EN] History: Recent transactions (sends, receives), transaction IDs, timestamps and status", + "statement_provider_select": "[EN] Omit provider_params: Query all. provider='x402': Only crypto. provider='chapa': Only Ethiopian. provider='all': Explicitly all.", + "statement_privacy": "[EN] Account details may include sensitive information. Only request details when needed.", + "statement_dma": "[EN] Balance and history reveal financial activity. Handle with appropriate privacy.", + "error_missing_params": "[EN] Missing required parameter(s): {params}", + "error_invalid_amount": "[EN] Invalid amount: {amount}", + "error_no_provider": "[EN] No provider available for currency: {currency}", + "error_unsupported_currency": "[EN] Unsupported currency: {currency}", + "error_wallet_not_init": "[EN] Wallet not initialized (no Ed25519 key)", + "error_no_signing": "[EN] Cannot send: no signing capability (receive-only mode). Ensure CIRISVerify is initialized with a key.", + "error_invalid_address": "[EN] Invalid EVM address format: {address}" + }, + "navigation": { + "tool_geocode_desc": "[EN] Convert an address or place name to geographic coordinates (latitude/longitude)", + "tool_reverse_desc": "[EN] Convert geographic coordinates to an address or place name", + "tool_route_desc": "[EN] Calculate driving route between two locations with distance and duration", + "geocode_quick_start": "[EN] Provide an address or place name to get latitude/longitude coordinates. Use these coordinates with weather tools or for route planning.", + "geocode_when": "[EN] When you need to find the coordinates of a location by name or address", + "geocode_formats": "[EN] Full address, place name, city name, or landmark", + "geocode_response": "[EN] latitude/longitude: Coordinates in decimal degrees. display_name: Full formatted address. type: Location type. importance: Relevance score.", + "geocode_rate_limit": "[EN] This tool uses OpenStreetMap Nominatim which has a 1 request/second limit. Multiple rapid requests will be automatically throttled.", + "gotcha_ambiguous": "[EN] Common place names may return unexpected results. Be specific (include city/country) for accurate results.", + "reverse_when": "[EN] When you have coordinates and need to find the address or place name", + "route_when": "[EN] When you need to calculate distance and travel time between two locations", + "route_disclaimer": "[EN] Travel times are estimates. Always follow traffic laws and check current conditions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_location": "[EN] Missing required parameter: location", + "error_location_not_found": "[EN] Could not find location: {location}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_reverse_failed": "[EN] Could not reverse geocode coordinates", + "error_missing_endpoints": "[EN] Missing required parameters: start and end", + "error_start_not_found": "[EN] Could not find start location: {location}", + "error_end_not_found": "[EN] Could not find end location: {location}", + "error_route_failed": "[EN] Could not calculate route between locations" + }, + "weather": { + "tool_current_desc": "[EN] Get current weather conditions for a location (temperature, wind, conditions)", + "tool_forecast_desc": "[EN] Get weather forecast for a location (upcoming conditions)", + "tool_alerts_desc": "[EN] Get active weather alerts for a location (warnings, watches, advisories)", + "current_quick_start": "[EN] Provide latitude and longitude coordinates to get current weather. Uses NOAA (US) with OpenWeatherMap fallback for international locations.", + "current_when": "[EN] When you need current weather conditions at a specific location", + "forecast_when": "[EN] When you need weather forecast for planning ahead", + "alerts_when": "[EN] When you need to check for severe weather warnings or alerts", + "data_sources": "[EN] NOAA (primary): US National Weather Service - most accurate for US locations. OpenWeatherMap (fallback): Global coverage, requires API key.", + "response_fields": "[EN] temperature, wind_speed/wind_direction, conditions (short description), detailed (full forecast), precipitation_chance.", + "gotcha_us_only": "[EN] NOAA only covers US locations. International locations require OpenWeatherMap API key.", + "gotcha_coords_required": "[EN] You must provide coordinates, not city names. Use navigation:geocode to convert addresses to coordinates first.", + "dma_guidance": "[EN] Don't use for medical decisions based on weather. Weather data is informational, not for life-safety decisions.", + "error_unknown_tool": "[EN] Unknown tool: {tool_name}", + "error_not_implemented": "[EN] Tool not implemented: {tool_name}", + "error_missing_coords": "[EN] Missing required parameters: latitude and longitude", + "error_invalid_coords": "[EN] Invalid coordinate values", + "error_data_unavailable": "[EN] Weather data unavailable for this location", + "error_forecast_unavailable": "[EN] Could not get forecast data for this location (NOAA US-only)", + "disclaimer_conditions": "[EN] Weather conditions can change rapidly. Check official sources for critical decisions.", + "disclaimer_alerts": "[EN] Always take weather alerts seriously. Check official sources and local authorities." + } + }, + "mobile": { + "setup_free_badge": "[EN] 100% Free & Open Source", + "setup_register_title": "[EN] Register Your Agent Identity", + "setup_register_optional": "[EN] OPTIONAL", + "setup_register_desc": "[EN] Join the CIRIS community and enable cryptographic attestation against the public infrastructure and your audit log.", + "setup_register_audit": "[EN] Audit trail — cryptographically-signed traces begin", + "setup_register_ratchet": "[EN] Coherence Ratchet — coordinated deception becomes mathematically harder over time", + "setup_register_scoring": "[EN] CIRIS Scoring — measures integrity across interactions", + "setup_register_template": "[EN] Community template (Ally) included", + "setup_register_bond": "[EN] $1.00 refundable bond + $0.50 processing fee", + "setup_register_connect": "[EN] Connect to CIRIS Portal", + "setup_register_key_note": "[EN] Identity keys are bound to your agent — transfers not yet supported", + "setup_register_sales": "[EN] For licensed deployment, contact sales@ciris.ai", + "setup_register_skip": "[EN] Skip for now - you can register later in Settings", + "setup_what_ciris": "[EN] What CIRIS Is", + "setup_what_ciris_desc": "[EN] CIRIS is an AI tool, not a friend, therapist, or human substitute. For emotional support, please reach out to real people in your life or professional services.", + "setup_google_ready": "[EN] You're ready to go!", + "setup_google_desc": "[EN] Since you signed in with {provider}, CIRIS can start working right away. Free access is limited and includes web search via privacy-protecting providers (Exa, Brave).", + "setup_details_expand": "[EN] Details", + "setup_details_content": "[EN] Your conversations are never sent for external AI training. With your consent, CIRIS can learn locally on your device to better assist you. All data stays on your device unless you explicitly share it. You control what CIRIS remembers about you.", + "setup_required_title": "[EN] Quick Setup Required", + "setup_required_desc": "[EN] To power AI conversations, you'll need to connect an AI provider (like OpenAI or Anthropic). This takes about 2 minutes.", + "setup_how_title": "[EN] How it works", + "setup_how_desc": "[EN] CIRIS runs entirely on your device. However, AI reasoning requires powerful servers. CIRIS connects to privacy-respecting AI providers that never train on your data and never store your conversations.", + "setup_node_register": "[EN] Register Agent", + "setup_node_connecting": "[EN] Connecting to portal...", + "setup_node_open_browser": "[EN] Click to open in browser, or copy to open in another app:", + "setup_node_open": "[EN] Open in Browser", + "setup_node_copy": "[EN] Copy URL", + "setup_node_copied": "[EN] Copied!", + "setup_node_checking": "[EN] Checking authorization...", + "setup_node_after_auth": "[EN] After completing authorization in your browser, tap the button below:", + "setup_node_all_done": "[EN] All Done!", + "setup_node_authorized": "[EN] Agent Authorized", + "setup_node_next_hint": "[EN] Tap Next to configure your LLM provider.", + "setup_node_failed": "[EN] Connection Failed", + "setup_already_complete": "[EN] Setup Already Complete", + "setup_error": "[EN] Setup Error", + "setup_continue_app": "[EN] Continue to App", + "setup_validation_select_mode": "[EN] Please select an AI mode", + "setup_validation_google_required": "[EN] Google sign-in required for free AI", + "setup_validation_api_key_required": "[EN] API Key is required", + "setup_validation_username_required": "[EN] Username is required", + "setup_validation_password_required": "[EN] Password is required", + "setup_validation_password_length": "[EN] Password must be at least 8 characters", + "setup_validation_device_failed": "[EN] Device authorization failed", + "setup_validation_node_url": "[EN] Enter a node URL to connect", + "setup_validation_node_connecting": "[EN] Connecting to node...", + "setup_validation_node_waiting": "[EN] Waiting for authorization in browser...", + "interact_showing_messages": "[EN] Showing last {count} messages", + "interact_local": "[EN] Local", + "interact_offline": "[EN] Offline", + "interact_shutdown": "[EN] Shutdown", + "interact_stop": "[EN] STOP", + "interact_connected": "[EN] Connected", + "interact_disconnected": "[EN] Disconnected", + "interact_dismiss": "[EN] Dismiss", + "interact_hallucination_warning": "[EN] AI HALLUCINATES - CHECK FACTS", + "interact_welcome_title": "[EN] Welcome to Ally", + "interact_welcome_subtitle": "[EN] Your personal thriving assistant", + "interact_welcome_hint": "[EN] Ask Ally how it can help with tasks, scheduling, decisions, or wellbeing — or ask how CIRIS works!", + "interact_sender_you": "[EN] You", + "interact_sender_ciris": "[EN] CIRIS", + "interact_action_speak": "[EN] Speak", + "interact_action_tool": "[EN] Tool", + "interact_action_observe": "[EN] Observe", + "interact_action_memorize": "[EN] Memorize", + "interact_action_recall": "[EN] Recall", + "interact_action_forget": "[EN] Forget", + "interact_action_reject": "[EN] Reject", + "interact_action_ponder": "[EN] Ponder", + "interact_action_defer": "[EN] Defer", + "interact_action_task_complete": "[EN] Task Complete", + "interact_action_generic": "[EN] Action", + "interact_outcome_success": "[EN] success", + "interact_outcome_failure": "[EN] failure", + "interact_outcome_error": "[EN] error", + "interact_outcome_pending": "[EN] pending", + "interact_field_parameters": "[EN] Parameters", + "interact_field_result": "[EN] Result", + "interact_field_content": "[EN] Content", + "interact_field_reason": "[EN] Reason: {reason}", + "interact_field_questions": "[EN] Questions:" + } +} \ No newline at end of file diff --git a/main.py b/main.py index 3fc8ec9e2..c3fa1ef3e 100755 --- a/main.py +++ b/main.py @@ -27,6 +27,16 @@ except ImportError: pass # dotenv is optional; skip if not installed + +# ============================================================================= +# CRITICAL: Set CIRIS_HOME environment variable BEFORE any ciris_engine imports +# This must happen early because CIRISVerify (verifier_singleton) requires it. +# Supports: Linux, macOS, Windows, WSL, Android, iOS, Docker/managed deployments +# ============================================================================= +from ciris_engine.logic.utils.path_resolution import ensure_ciris_home_env + +_ciris_home = ensure_ciris_home_env() + import asyncio import atexit import json diff --git a/mobile/androidApp/build.gradle b/mobile/androidApp/build.gradle index b94dda4e7..cd38f6181 100644 --- a/mobile/androidApp/build.gradle +++ b/mobile/androidApp/build.gradle @@ -13,8 +13,8 @@ android { applicationId "ai.ciris.mobile" minSdk 24 targetSdk 35 - versionCode 74 - versionName "2.2.9" + versionCode 75 + versionName "2.3.0" ndk { // Include x86_64 for emulator testing (Chaquopy reads abiFilters at config time) diff --git a/mobile/androidApp/src/main/python/mobile_main.py b/mobile/androidApp/src/main/python/mobile_main.py index fc41800a4..0e8fb482a 100644 --- a/mobile/androidApp/src/main/python/mobile_main.py +++ b/mobile/androidApp/src/main/python/mobile_main.py @@ -837,10 +837,10 @@ def verify_code_integrity(package_names: list = None, save_to_file: bool = True) def setup_android_environment(): """Configure environment for Android on-device operation. - Sets up CIRIS_HOME and loads .env if present. - First-run detection is handled by is_first_run() which is Android-aware. + Uses the centralized ensure_ciris_home_env() for cross-platform CIRIS_HOME setup, + then loads .env if present. - Uses path_resolution.py for all path logic - only CIRIS_HOME is set here. + First-run detection is handled by is_first_run() which is Android-aware. """ from dotenv import load_dotenv @@ -848,28 +848,22 @@ def setup_android_environment(): logger.warning("ANDROID_DATA not set - not running on Android?") return - # Step 1: Set CIRIS_HOME (the only env var we set manually) - # This is the root for path_resolution.py to build all other paths - android_data = Path(os.environ["ANDROID_DATA"]) - ciris_home = android_data / "data" / ANDROID_PACKAGE_NAME / "files" / "ciris" - os.environ.setdefault("CIRIS_HOME", str(ciris_home)) - - # Step 2: Import path_resolution and use it for all paths - from ciris_engine.logic.utils.path_resolution import get_ciris_home, get_data_dir, get_logs_dir + # Use centralized path resolution for CIRIS_HOME setup + # This handles all platforms: Android, iOS, Linux, macOS, Windows, Docker + from ciris_engine.logic.utils.path_resolution import ( + ensure_ciris_home_env, + get_data_dir, + get_logs_dir, + ) - ciris_home = get_ciris_home() + # This sets CIRIS_HOME, CIRIS_DATA_DIR, and creates directories + ciris_home = ensure_ciris_home_env() data_dir = get_data_dir() logs_dir = get_logs_dir() - # Step 3: Ensure directories exist - ciris_home.mkdir(parents=True, exist_ok=True) - data_dir.mkdir(parents=True, exist_ok=True) + # Ensure logs directory exists (ensure_ciris_home_env creates home and data) logs_dir.mkdir(parents=True, exist_ok=True) - # NOTE: Do NOT set CIRIS_DATA_DIR or CIRIS_LOG_DIR here! - # CIRISVerify handles its own path resolution internally. - # Setting these env vars causes path inconsistencies between Python and Rust. - logger.info(f"Android paths: CIRIS_HOME={ciris_home}, data={data_dir}, logs={logs_dir}") # Load .env file if it exists (sets OPENAI_API_KEY, OPENAI_API_BASE, etc.) diff --git a/mobile/androidApp/src/main/python/version.py b/mobile/androidApp/src/main/python/version.py index 5734ccddc..55e46f565 100644 --- a/mobile/androidApp/src/main/python/version.py +++ b/mobile/androidApp/src/main/python/version.py @@ -7,7 +7,7 @@ # Static version - updated at build time by the Android build process # This avoids file-system hashing logic that doesn't work in the Android package -__version__ = "android-2.2.9" +__version__ = "android-2.3.0" def get_version() -> str: diff --git a/mobile/iosApp/iosApp/Info.plist b/mobile/iosApp/iosApp/Info.plist index ffa2f6433..3625593d6 100644 --- a/mobile/iosApp/iosApp/Info.plist +++ b/mobile/iosApp/iosApp/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.2.9 + 2.3.0 CFBundleVersion - 198 + 199 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/CIRISApp.kt b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/CIRISApp.kt index 03150a6b6..2c2555a7f 100644 --- a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/CIRISApp.kt +++ b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/CIRISApp.kt @@ -2494,7 +2494,8 @@ private fun CIRISTopBar( DropdownMenuItem( text = { Text("Tools") }, onClick = { activeCategory = NavCategory.NONE; onToolsClick() }, - leadingIcon = { Icon(Icons.Default.Build, null) } + leadingIcon = { Icon(Icons.Default.Build, null) }, + modifier = Modifier.testableClickable("menu_tools") { activeCategory = NavCategory.NONE; onToolsClick() } ) } } @@ -2502,7 +2503,10 @@ private fun CIRISTopBar( // Category 2: Config & Credits Box { IconButton( - onClick = { activeCategory = if (activeCategory == NavCategory.CONFIG) NavCategory.NONE else NavCategory.CONFIG } + onClick = { activeCategory = if (activeCategory == NavCategory.CONFIG) NavCategory.NONE else NavCategory.CONFIG }, + modifier = Modifier.testableClickable("btn_config_menu") { + activeCategory = if (activeCategory == NavCategory.CONFIG) NavCategory.NONE else NavCategory.CONFIG + } ) { Icon( imageVector = Icons.Default.Settings, @@ -2535,17 +2539,20 @@ private fun CIRISTopBar( DropdownMenuItem( text = { Text("App Settings") }, onClick = { activeCategory = NavCategory.NONE; onSettingsClick() }, - leadingIcon = { Icon(Icons.Default.Settings, null) } + leadingIcon = { Icon(Icons.Default.Settings, null) }, + modifier = Modifier.testableClickable("menu_settings") { activeCategory = NavCategory.NONE; onSettingsClick() } ) DropdownMenuItem( text = { Text("Agent Config") }, onClick = { activeCategory = NavCategory.NONE; onConfigClick() }, - leadingIcon = { Icon(Icons.Default.Settings, null) } + leadingIcon = { Icon(Icons.Default.Settings, null) }, + modifier = Modifier.testableClickable("menu_config") { activeCategory = NavCategory.NONE; onConfigClick() } ) DropdownMenuItem( text = { Text("Buy Credits") }, onClick = { activeCategory = NavCategory.NONE; onBillingClick() }, - leadingIcon = { Icon(Icons.Default.Star, null) } + leadingIcon = { Icon(Icons.Default.Star, null) }, + modifier = Modifier.testableClickable("menu_billing") { activeCategory = NavCategory.NONE; onBillingClick() } ) } } @@ -2553,7 +2560,10 @@ private fun CIRISTopBar( // Category 3: Data & Privacy Box { IconButton( - onClick = { activeCategory = if (activeCategory == NavCategory.DATA) NavCategory.NONE else NavCategory.DATA } + onClick = { activeCategory = if (activeCategory == NavCategory.DATA) NavCategory.NONE else NavCategory.DATA }, + modifier = Modifier.testableClickable("btn_data_menu") { + activeCategory = if (activeCategory == NavCategory.DATA) NavCategory.NONE else NavCategory.DATA + } ) { Icon( imageVector = Icons.Default.Info, @@ -2574,27 +2584,32 @@ private fun CIRISTopBar( DropdownMenuItem( text = { Text("Memory") }, onClick = { activeCategory = NavCategory.NONE; onMemoryClick() }, - leadingIcon = { Icon(Icons.Default.Star, null) } + leadingIcon = { Icon(Icons.Default.Star, null) }, + modifier = Modifier.testableClickable("menu_memory") { activeCategory = NavCategory.NONE; onMemoryClick() } ) DropdownMenuItem( text = { Text("Sessions") }, onClick = { activeCategory = NavCategory.NONE; onSessionsClick() }, - leadingIcon = { Icon(Icons.Default.List, null) } + leadingIcon = { Icon(Icons.Default.List, null) }, + modifier = Modifier.testableClickable("menu_sessions") { activeCategory = NavCategory.NONE; onSessionsClick() } ) DropdownMenuItem( text = { Text("Consent") }, onClick = { activeCategory = NavCategory.NONE; onConsentClick() }, - leadingIcon = { Icon(Icons.Default.Check, null) } + leadingIcon = { Icon(Icons.Default.Check, null) }, + modifier = Modifier.testableClickable("menu_consent") { activeCategory = NavCategory.NONE; onConsentClick() } ) DropdownMenuItem( text = { Text("Audit Trail") }, onClick = { activeCategory = NavCategory.NONE; onAuditClick() }, - leadingIcon = { Icon(Icons.Default.List, null) } + leadingIcon = { Icon(Icons.Default.List, null) }, + modifier = Modifier.testableClickable("menu_audit") { activeCategory = NavCategory.NONE; onAuditClick() } ) DropdownMenuItem( text = { Text("Data Management") }, onClick = { activeCategory = NavCategory.NONE; onDataManagementClick() }, - leadingIcon = { Icon(Icons.Default.Info, null) } + leadingIcon = { Icon(Icons.Default.Info, null) }, + modifier = Modifier.testableClickable("menu_data_management") { activeCategory = NavCategory.NONE; onDataManagementClick() } ) } } @@ -2602,7 +2617,10 @@ private fun CIRISTopBar( // Category 4: Governance Box { IconButton( - onClick = { activeCategory = if (activeCategory == NavCategory.GOVERNANCE) NavCategory.NONE else NavCategory.GOVERNANCE } + onClick = { activeCategory = if (activeCategory == NavCategory.GOVERNANCE) NavCategory.NONE else NavCategory.GOVERNANCE }, + modifier = Modifier.testableClickable("btn_governance_menu") { + activeCategory = if (activeCategory == NavCategory.GOVERNANCE) NavCategory.NONE else NavCategory.GOVERNANCE + } ) { Icon( imageVector = Icons.Default.Person, @@ -2623,12 +2641,14 @@ private fun CIRISTopBar( DropdownMenuItem( text = { Text("Human Deferrals") }, onClick = { activeCategory = NavCategory.NONE; onWiseAuthorityClick() }, - leadingIcon = { Icon(Icons.Default.Person, null) } + leadingIcon = { Icon(Icons.Default.Person, null) }, + modifier = Modifier.testableClickable("menu_wise_authority") { activeCategory = NavCategory.NONE; onWiseAuthorityClick() } ) DropdownMenuItem( text = { Text("Users") }, onClick = { activeCategory = NavCategory.NONE; onUsersClick() }, - leadingIcon = { Icon(Icons.Default.Person, null) } + leadingIcon = { Icon(Icons.Default.Person, null) }, + modifier = Modifier.testableClickable("menu_users") { activeCategory = NavCategory.NONE; onUsersClick() } ) } } @@ -2660,37 +2680,44 @@ private fun CIRISTopBar( DropdownMenuItem( text = { Text("Telemetry") }, onClick = { activeCategory = NavCategory.NONE; onTelemetryClick() }, - leadingIcon = { Icon(Icons.Default.Info, null) } + leadingIcon = { Icon(Icons.Default.Info, null) }, + modifier = Modifier.testableClickable("menu_telemetry") { activeCategory = NavCategory.NONE; onTelemetryClick() } ) DropdownMenuItem( text = { Text("Services") }, onClick = { activeCategory = NavCategory.NONE; onServicesClick() }, - leadingIcon = { Icon(Icons.Default.Build, null) } + leadingIcon = { Icon(Icons.Default.Build, null) }, + modifier = Modifier.testableClickable("menu_services") { activeCategory = NavCategory.NONE; onServicesClick() } ) DropdownMenuItem( text = { Text("Logs") }, onClick = { activeCategory = NavCategory.NONE; onLogsClick() }, - leadingIcon = { Icon(Icons.Default.List, null) } + leadingIcon = { Icon(Icons.Default.List, null) }, + modifier = Modifier.testableClickable("menu_logs") { activeCategory = NavCategory.NONE; onLogsClick() } ) DropdownMenuItem( text = { Text("System") }, onClick = { activeCategory = NavCategory.NONE; onSystemClick() }, - leadingIcon = { Icon(Icons.Default.Info, null) } + leadingIcon = { Icon(Icons.Default.Info, null) }, + modifier = Modifier.testableClickable("menu_system") { activeCategory = NavCategory.NONE; onSystemClick() } ) DropdownMenuItem( text = { Text("Runtime") }, onClick = { activeCategory = NavCategory.NONE; onRuntimeClick() }, - leadingIcon = { Icon(Icons.Default.PlayArrow, null) } + leadingIcon = { Icon(Icons.Default.PlayArrow, null) }, + modifier = Modifier.testableClickable("menu_runtime") { activeCategory = NavCategory.NONE; onRuntimeClick() } ) DropdownMenuItem( text = { Text("Tickets") }, onClick = { activeCategory = NavCategory.NONE; onTicketsClick() }, - leadingIcon = { Icon(Icons.Default.List, null) } + leadingIcon = { Icon(Icons.Default.List, null) }, + modifier = Modifier.testableClickable("menu_tickets") { activeCategory = NavCategory.NONE; onTicketsClick() } ) DropdownMenuItem( text = { Text("Scheduler") }, onClick = { activeCategory = NavCategory.NONE; onSchedulerClick() }, - leadingIcon = { Icon(Icons.Default.Check, null) } + leadingIcon = { Icon(Icons.Default.Check, null) }, + modifier = Modifier.testableClickable("menu_scheduler") { activeCategory = NavCategory.NONE; onSchedulerClick() } ) } } diff --git a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/models/ChatMessage.kt b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/models/ChatMessage.kt index c6a4705f4..3eb306754 100644 --- a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/models/ChatMessage.kt +++ b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/models/ChatMessage.kt @@ -62,6 +62,7 @@ data class ActionDetails( val toolName: String? = null, val toolAdapter: String? = null, val toolParameters: Map = emptyMap(), + val toolResult: String? = null, // Memory-specific (memorize, recall, forget) val memoryKey: String? = null, val memoryContent: String? = null, diff --git a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/ui/screens/InteractScreen.kt b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/ui/screens/InteractScreen.kt index 39762f55b..be25182a7 100644 --- a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/ui/screens/InteractScreen.kt +++ b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/ui/screens/InteractScreen.kt @@ -1511,6 +1511,23 @@ private fun ActionExpandedDetails( } } } + // Tool result + actionDetails.toolResult?.let { result -> + Text( + text = "Result", + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + color = Color(0xFF374151) + ) + SelectionContainer { + Text( + text = result.take(300) + if (result.length > 300) "..." else "", + fontSize = 10.sp, + color = Color(0xFF1F2937), + modifier = Modifier.padding(start = 8.dp) + ) + } + } } ActionType.MEMORIZE, ActionType.RECALL -> { actionDetails.memoryContent?.let { content -> diff --git a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/ui/screens/SetupScreen.kt b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/ui/screens/SetupScreen.kt index 8a13f56aa..8b7944df5 100644 --- a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/ui/screens/SetupScreen.kt +++ b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/ui/screens/SetupScreen.kt @@ -9,7 +9,14 @@ import ai.ciris.mobile.shared.platform.getOAuthProviderName import ai.ciris.mobile.shared.platform.getPlatform import ai.ciris.mobile.shared.platform.testable import ai.ciris.mobile.shared.platform.testableClickable - +import ai.ciris.mobile.shared.platform.TestAutomation + +import ai.ciris.mobile.shared.models.ConfigCompleteData +import ai.ciris.mobile.shared.models.ConfigSessionData +import ai.ciris.mobile.shared.models.ConfigStepResultData +import ai.ciris.mobile.shared.models.DiscoveredItemData +import ai.ciris.mobile.shared.models.LoadableAdaptersData +import ai.ciris.mobile.shared.ui.components.AdapterWizardDialog import ai.ciris.mobile.shared.viewmodels.DeviceAuthStatus import ai.ciris.mobile.shared.viewmodels.LlmValidationResult import ai.ciris.mobile.shared.viewmodels.ModelInfo @@ -113,6 +120,78 @@ fun SetupScreen( val coroutineScope = rememberCoroutineScope() val semantic = SemanticColors.forTheme(ColorTheme.DEFAULT, isDark = false) + // Observe text input requests for test automation + val textInputRequest by TestAutomation.textInputRequests.collectAsState() + + // Handle incoming text input requests + LaunchedEffect(textInputRequest) { + textInputRequest?.let { request -> + when (request.testTag) { + "input_public_api_email" -> { + if (request.clearFirst) { + viewModel.setPublicApiEmail(request.text) + } else { + viewModel.setPublicApiEmail(state.publicApiEmail + request.text) + } + TestAutomation.clearTextInputRequest() + } + "input_username" -> { + if (request.clearFirst) { + viewModel.setUsername(request.text) + } else { + viewModel.setUsername(state.username + request.text) + } + TestAutomation.clearTextInputRequest() + } + "input_password" -> { + if (request.clearFirst) { + viewModel.setUserPassword(request.text) + } else { + viewModel.setUserPassword(state.userPassword + request.text) + } + TestAutomation.clearTextInputRequest() + } + "input_api_key" -> { + if (request.clearFirst) { + viewModel.setLlmApiKey(request.text) + } else { + viewModel.setLlmApiKey(state.llmApiKey + request.text) + } + TestAutomation.clearTextInputRequest() + } + "input_llm_model_text" -> { + if (request.clearFirst) { + viewModel.setLlmModel(request.text) + } else { + viewModel.setLlmModel(state.llmModel + request.text) + } + TestAutomation.clearTextInputRequest() + } + } + } + } + + // Set up the wizard API for adapter configuration + LaunchedEffect(Unit) { + viewModel.setWizardApi(object : SetupViewModel.AdapterWizardApi { + override suspend fun getLoadableAdapters(): LoadableAdaptersData { + return apiClient.getLoadableAdapters() + } + override suspend fun startAdapterConfiguration(adapterType: String): ConfigSessionData { + return apiClient.startAdapterConfiguration(adapterType) + } + override suspend fun executeConfigurationStep(sessionId: String, stepData: Map): ConfigStepResultData { + return apiClient.executeConfigurationStep(sessionId, stepData) + } + override suspend fun getConfigurationSessionStatus(sessionId: String): ConfigSessionData { + return apiClient.getConfigurationSessionStatus(sessionId) + } + override suspend fun completeAdapterConfiguration(sessionId: String): ConfigCompleteData { + return apiClient.completeAdapterConfiguration(sessionId) + } + }) + } + // Load adapters and templates when entering OPTIONAL_FEATURES step LaunchedEffect(state.currentStep) { if (state.currentStep == SetupStep.OPTIONAL_FEATURES) { @@ -143,6 +222,56 @@ fun SetupScreen( } } + // Adapter Wizard Dialog (shown when configuring adapters that require setup) + if (state.showAdapterWizard) { + // Create a minimal LoadableAdaptersData for the dialog to show wizard steps + // The wizard session is what drives the actual steps + val wizardLoadableAdapters = state.adapterWizardType?.let { adapterType -> + state.availableAdapters.find { it.id == adapterType }?.let { adapter -> + LoadableAdaptersData( + adapters = listOf( + ai.ciris.mobile.shared.models.LoadableAdapterData( + adapterType = adapter.id, + name = adapter.name, + description = adapter.description, + requiresConfiguration = adapter.requires_config, + workflowType = null, + stepCount = state.adapterWizardSession?.totalSteps ?: 0, + requiresOauth = false, + serviceTypes = emptyList(), + platformAvailable = true + ) + ), + totalCount = 1, + configurableCount = 1, + directLoadCount = 0 + ) + } + } + + AdapterWizardDialog( + loadableAdapters = wizardLoadableAdapters, + wizardSession = state.adapterWizardSession, + isLoading = state.adapterWizardLoading, + error = state.adapterWizardError, + discoveredItems = state.adapterDiscoveredItems, + discoveryExecuted = state.adapterDiscoveryExecuted, + oauthUrl = state.adapterOAuthUrl, + awaitingOAuthCallback = state.adapterAwaitingOAuthCallback, + selectOptions = state.adapterSelectOptions, + onSelectType = { /* Not used - we go directly to wizard session */ }, + onLoadDirectly = { /* Not used during setup */ }, + onSubmitStep = { stepData -> viewModel.submitAdapterWizardStep(stepData) }, + onSelectDiscoveredItem = { item -> viewModel.selectAdapterDiscoveredItem(item) }, + onSubmitManualUrl = { url -> viewModel.submitAdapterManualUrl(url) }, + onRetryDiscovery = { viewModel.executeAdapterDiscoveryStep() }, + onInitiateOAuth = { viewModel.initiateAdapterOAuthStep() }, + onCheckOAuthStatus = { viewModel.checkAdapterOAuthOnResume() }, + onBack = { viewModel.adapterWizardBack() }, + onDismiss = { viewModel.closeAdapterWizard() } + ) + } + Surface( modifier = modifier.fillMaxSize(), color = SetupColors.Background @@ -318,17 +447,19 @@ private fun StepIndicators( } Row( - modifier = modifier, + modifier = modifier.testable("setup_step_indicators"), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { steps.forEachIndexed { index, (step, number) -> val isActive = currentStep >= step val isComplete = currentStep > step + val stepName = step.name.lowercase() Box( modifier = Modifier .size(32.dp) + .testable("step_indicator_$stepName", if (isComplete) "complete" else if (isActive) "active" else "inactive") .background( color = if (isActive) SetupColors.Primary else SetupColors.GrayLight, shape = CircleShape @@ -1612,6 +1743,112 @@ private fun OptionalFeaturesStep( } } + // Navigation & Weather Services Card + Surface( + shape = RoundedCornerShape(12.dp), + color = SetupColors.SuccessLight, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 8.dp) + ) { + Text( + text = "🌍", + fontSize = 20.sp, + modifier = Modifier.padding(end = 8.dp) + ) + Text( + text = "Navigation & Weather Services", + color = SetupColors.SuccessDark, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } + + Text( + text = "Enable location-based tools powered by free public APIs (OpenStreetMap, NOAA Weather). These services require a contact email in API requests per their usage policies.", + color = SetupColors.SuccessText, + fontSize = 14.sp, + lineHeight = 20.sp, + modifier = Modifier.padding(bottom = 12.dp) + ) + + Text( + text = "Features enabled:", + color = SetupColors.SuccessDark, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 4.dp) + ) + + Column(modifier = Modifier.padding(start = 8.dp, bottom = 12.dp)) { + DataPointRow("Convert addresses to coordinates", SetupColors.SuccessText) + DataPointRow("Get current weather by location name", SetupColors.SuccessText) + DataPointRow("Calculate routes and distances", SetupColors.SuccessText) + DataPointRow("Weather forecasts (US locations)", SetupColors.SuccessText) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.testableClickable("item_public_api_services") { + viewModel.setPublicApiServicesEnabled(!state.publicApiServicesEnabled) + } + ) { + Checkbox( + checked = state.publicApiServicesEnabled, + onCheckedChange = { viewModel.setPublicApiServicesEnabled(it) }, + colors = CheckboxDefaults.colors( + checkedColor = SetupColors.Primary, + uncheckedColor = SetupColors.TextSecondary + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Enable navigation & weather tools", + color = SetupColors.SuccessDark, + fontSize = 14.sp + ) + } + + // Email input (shown when enabled) + AnimatedVisibility(visible = state.publicApiServicesEnabled) { + Column(modifier = Modifier.padding(top = 12.dp)) { + Text( + text = "Contact Email (required by API policies)", + color = SetupColors.SuccessDark, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 4.dp) + ) + OutlinedTextField( + value = state.publicApiEmail, + onValueChange = { viewModel.setPublicApiEmail(it) }, + placeholder = { Text("your@email.com") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), + modifier = Modifier + .fillMaxWidth() + .testable("input_public_api_email"), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = SetupColors.Primary, + unfocusedBorderColor = SetupColors.SuccessBorder + ) + ) + Text( + text = "Used in User-Agent header so API providers can contact you if needed. Not shared with CIRIS.", + color = SetupColors.SuccessText, + fontSize = 12.sp, + modifier = Modifier.padding(top = 4.dp) + ) + } + } + } + } + // Adapters Section Text( text = "Communication Adapters", @@ -1645,12 +1882,15 @@ private fun OptionalFeaturesStep( isEnabled = true, isRequired = true, requiresConfig = false, - onToggle = {} + isConfigured = false, + onToggle = {}, + onConfigure = null ) } else { state.availableAdapters.forEach { adapter -> val isEnabled = state.enabledAdapterIds.contains(adapter.id) val isRequired = adapter.id == "api" + val isConfigured = state.configuredAdapterData.containsKey(adapter.id) AdapterToggleItem( name = adapter.name, @@ -1658,11 +1898,21 @@ private fun OptionalFeaturesStep( isEnabled = isEnabled, isRequired = isRequired, requiresConfig = adapter.requires_config, + isConfigured = isConfigured, configFields = adapter.config_fields, onToggle = { enabled -> if (!isRequired) { - viewModel.toggleAdapter(adapter.id, enabled) + if (enabled && adapter.requires_config && !isConfigured) { + // Launch the wizard for adapters that require configuration + viewModel.startAdapterWizard(adapter.id) + } else { + viewModel.toggleAdapter(adapter.id, enabled) + } } + }, + onConfigure = { + // Allow re-configuration of already configured adapters + viewModel.startAdapterWizard(adapter.id) } ) @@ -1818,13 +2068,22 @@ private fun AdapterToggleItem( isEnabled: Boolean, isRequired: Boolean, requiresConfig: Boolean, + isConfigured: Boolean = false, configFields: List = emptyList(), - onToggle: (Boolean) -> Unit + onToggle: (Boolean) -> Unit, + onConfigure: (() -> Unit)? = null ) { + val semantic = SemanticColors.forTheme(ColorTheme.DEFAULT, isDark = false) + + val adapterTag = name.lowercase().replace(" ", "_") Surface( shape = RoundedCornerShape(8.dp), color = if (isEnabled) SetupColors.SuccessLight else SetupColors.GrayLight, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testableClickable("adapter_toggle_$adapterTag") { + if (!isRequired) onToggle(!isEnabled) + } ) { Row( modifier = Modifier @@ -1856,16 +2115,32 @@ private fun AdapterToggleItem( } if (requiresConfig && isEnabled) { Spacer(modifier = Modifier.width(8.dp)) - Surface( - shape = RoundedCornerShape(4.dp), - color = SemanticColors.forTheme(ColorTheme.DEFAULT, isDark = false).surfaceWarning - ) { - Text( - text = "Needs Config", - color = SemanticColors.forTheme(ColorTheme.DEFAULT, isDark = false).onWarning, - fontSize = 10.sp, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) - ) + if (isConfigured) { + // Show configured badge (green) + Surface( + shape = RoundedCornerShape(4.dp), + color = semantic.surfaceSuccess + ) { + Text( + text = "Configured", + color = semantic.onSuccess, + fontSize = 10.sp, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } + } else { + // Show needs config badge (warning) + Surface( + shape = RoundedCornerShape(4.dp), + color = semantic.surfaceWarning + ) { + Text( + text = "Needs Config", + color = semantic.onWarning, + fontSize = 10.sp, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } } } } @@ -1875,10 +2150,26 @@ private fun AdapterToggleItem( fontSize = 12.sp, modifier = Modifier.padding(top = 2.dp) ) - if (requiresConfig && configFields.isNotEmpty() && isEnabled) { + + // Show configure button for configurable adapters + if (requiresConfig && isEnabled && onConfigure != null) { + TextButton( + onClick = onConfigure, + modifier = Modifier + .padding(top = 4.dp) + .testableClickable("btn_configure_${name.lowercase().replace(" ", "_")}") { onConfigure() } + ) { + Text( + text = if (isConfigured) "Reconfigure" else "Configure Now", + color = SetupColors.Primary, + fontSize = 12.sp, + fontWeight = FontWeight.Medium + ) + } + } else if (requiresConfig && configFields.isNotEmpty() && isEnabled && !isConfigured) { Text( text = "Required: ${configFields.joinToString(", ")}", - color = SemanticColors.forTheme(ColorTheme.DEFAULT, isDark = false).onWarning, + color = semantic.onWarning, fontSize = 11.sp, modifier = Modifier.padding(top = 4.dp) ) diff --git a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/InteractViewModel.kt b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/InteractViewModel.kt index 8381dabde..1f5f799cd 100644 --- a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/InteractViewModel.kt +++ b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/InteractViewModel.kt @@ -954,6 +954,7 @@ class InteractViewModel( val toolName = metadata?.get("tool_name")?.jsonPrimitiveContent() ?: "Unknown Tool" val toolAdapter = metadata?.get("tool_adapter")?.jsonPrimitiveContent() ?: "unknown" val toolParameters = parseToolParameters(metadata?.get("tool_parameters")) + val toolResult = metadata?.get("tool_result")?.jsonPrimitiveContent() ActionDetails( actionType = actionType, @@ -962,7 +963,8 @@ class InteractViewModel( description = description, toolName = toolName, toolAdapter = toolAdapter, - toolParameters = toolParameters + toolParameters = toolParameters, + toolResult = toolResult ) } ActionType.MEMORIZE, ActionType.RECALL, ActionType.FORGET -> { @@ -1011,8 +1013,9 @@ class InteractViewModel( val ponderTopic = metadata?.get("topic")?.jsonPrimitiveContent() ?: metadata?.get("ponder_topic")?.jsonPrimitiveContent() - // Parse ponder_questions from the double-encoded parameters JSON string - val ponderQuestions = parsePonderQuestions(metadata?.get("parameters")) + // Parse ponder_questions - check direct metadata first, then nested in parameters + val ponderQuestions = parsePonderQuestionsDirect(metadata?.get("ponder_questions")) + .ifEmpty { parsePonderQuestions(metadata?.get("parameters")) } ActionDetails( actionType = actionType, @@ -1109,6 +1112,56 @@ class InteractViewModel( return parameters } + /** + * Parse ponder questions directly from metadata (when merged from graph entries). + * Can be a JSON array, a JSON-encoded string array, or a double-encoded string. + */ + private fun parsePonderQuestionsDirect(questionsElement: kotlinx.serialization.json.JsonElement?): List { + if (questionsElement == null) return emptyList() + + try { + // Case 1: Already a JSON array + if (questionsElement is kotlinx.serialization.json.JsonArray) { + return questionsElement.mapNotNull { element -> + when (element) { + is kotlinx.serialization.json.JsonPrimitive -> element.content + else -> null + } + } + } + + // Case 2: JSON-encoded string - may be single or double encoded + var questionsStr = questionsElement.jsonPrimitiveContent() ?: return emptyList() + + // Handle double-encoding: if it's a JSON string containing a JSON array string + // e.g., "\"[\\\"Q1\\\", \\\"Q2\\\"]\"" - parse first to get the inner string + if (questionsStr.startsWith("\"") || questionsStr.startsWith("\\\"")) { + try { + val unescaped = kotlinx.serialization.json.Json.parseToJsonElement(questionsStr) + if (unescaped is kotlinx.serialization.json.JsonPrimitive) { + questionsStr = unescaped.content + } + } catch (_: Exception) { + // First unescape failed, continue with original string + } + } + + // Now parse the array + val questionsJson = kotlinx.serialization.json.Json.parseToJsonElement(questionsStr) + if (questionsJson is kotlinx.serialization.json.JsonArray) { + return questionsJson.mapNotNull { element -> + when (element) { + is kotlinx.serialization.json.JsonPrimitive -> element.content + else -> null + } + } + } + } catch (e: Exception) { + logInfo("parsePonderQuestionsDirect", "Failed to parse: ${e.message}") + } + return emptyList() + } + /** * Parse ponder questions from double-encoded parameters JSON. * The parameters field contains a JSON string like: diff --git a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/SetupState.kt b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/SetupState.kt index c7c6f546c..05f239fda 100644 --- a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/SetupState.kt +++ b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/SetupState.kt @@ -1,6 +1,10 @@ package ai.ciris.mobile.shared.viewmodels import ai.ciris.mobile.shared.models.CommunicationAdapter +import ai.ciris.mobile.shared.models.ConfigSessionData +import ai.ciris.mobile.shared.models.DiscoveredItemData +import ai.ciris.mobile.shared.models.LoadableAdaptersData +import ai.ciris.mobile.shared.models.SelectOptionData import ai.ciris.mobile.shared.models.SetupMode import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -160,6 +164,12 @@ data class SetupFormState( // No message content or PII is ever sent val accordMetricsConsent: Boolean = false, + // Public API Services (Navigation & Weather) + // Email included in User-Agent header for Nominatim (OSM) and weather.gov (NOAA) + // Required by their usage policies for contact if issues arise + val publicApiEmail: String = "", + val publicApiServicesEnabled: Boolean = false, + // V1.9.7: Template selection (Advanced Settings) val availableTemplates: List = emptyList(), val selectedTemplateId: String = "default", @@ -174,6 +184,26 @@ data class SetupFormState( // Loading state for adapter list val adaptersLoading: Boolean = false, + // Adapter wizard state (for adapters that require configuration) + // This mirrors AdaptersViewModel's wizard state for use during setup + val showAdapterWizard: Boolean = false, + val adapterWizardType: String? = null, // Adapter type being configured (e.g., "home_assistant") + @kotlinx.serialization.Transient + val loadableAdaptersData: LoadableAdaptersData? = null, + @kotlinx.serialization.Transient + val adapterWizardSession: ConfigSessionData? = null, + val adapterWizardError: String? = null, + val adapterWizardLoading: Boolean = false, + @kotlinx.serialization.Transient + val adapterDiscoveredItems: List = emptyList(), + val adapterDiscoveryExecuted: Boolean = false, + val adapterOAuthUrl: String? = null, + val adapterAwaitingOAuthCallback: Boolean = false, + @kotlinx.serialization.Transient + val adapterSelectOptions: List = emptyList(), + // Map of adapterId -> completed configuration for adapters configured during setup + val configuredAdapterData: Map> = emptyMap(), + // Validation state val isValidating: Boolean = false, val validationError: String? = null, diff --git a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/SetupViewModel.kt b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/SetupViewModel.kt index 5d0de9a39..f385c1243 100644 --- a/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/SetupViewModel.kt +++ b/mobile/shared/src/commonMain/kotlin/ai/ciris/mobile/shared/viewmodels/SetupViewModel.kt @@ -4,9 +4,14 @@ import ai.ciris.mobile.shared.config.CIRISConfig import ai.ciris.mobile.shared.models.* import ai.ciris.mobile.shared.platform.PlatformLogger import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch private const val TAG = "SetupViewModel" @@ -33,6 +38,9 @@ class SetupViewModel : ViewModel() { private val _state = MutableStateFlow(SetupFormState()) val state: StateFlow = _state.asStateFlow() + // OAuth poll job for adapter wizard + private var adapterOAuthPollJob: Job? = null + // ========== Google OAuth State ========== // Source: SetupViewModel.kt:68-80, SetupWizardActivity.kt:110-174 @@ -234,6 +242,26 @@ class SetupViewModel : ViewModel() { _state.value = _state.value.copy(accordMetricsConsent = consent) } + // ========== Public API Services (Navigation & Weather) ========== + + /** + * Set the email address for public API services (Navigation & Weather). + * This email is included in User-Agent headers as required by + * OpenStreetMap Nominatim and NOAA weather.gov usage policies. + */ + fun setPublicApiEmail(email: String) { + _state.value = _state.value.copy(publicApiEmail = email) + } + + /** + * Enable or disable public API services (Navigation & Weather). + * When enabled, navigation:geocode can resolve location names to coordinates + * for use with weather tools. + */ + fun setPublicApiServicesEnabled(enabled: Boolean) { + _state.value = _state.value.copy(publicApiServicesEnabled = enabled) + } + // ========== Template Selection (V1.9.7) ========== /** @@ -337,6 +365,438 @@ class SetupViewModel : ViewModel() { return _state.value.enabledAdapterIds.contains(adapterId) } + // ========== Adapter Wizard (for adapters requiring configuration) ========== + + /** + * Interface for adapter wizard API calls. + * SetupScreen provides the implementation using apiClient. + */ + interface AdapterWizardApi { + suspend fun getLoadableAdapters(): LoadableAdaptersData + suspend fun startAdapterConfiguration(adapterType: String): ConfigSessionData + suspend fun executeConfigurationStep(sessionId: String, stepData: Map): ConfigStepResultData + suspend fun getConfigurationSessionStatus(sessionId: String): ConfigSessionData + suspend fun completeAdapterConfiguration(sessionId: String): ConfigCompleteData + } + + // Stored API instance for wizard operations + private var wizardApi: AdapterWizardApi? = null + + /** + * Set the API instance for wizard operations. + * Call this before starting the wizard. + */ + fun setWizardApi(api: AdapterWizardApi) { + wizardApi = api + } + + /** + * Start the adapter wizard for a specific adapter type. + * Called when user enables an adapter that requires configuration. + */ + fun startAdapterWizard(adapterType: String) { + val api = wizardApi + if (api == null) { + PlatformLogger.e(TAG, "startAdapterWizard: No API instance set") + _state.value = _state.value.copy( + adapterWizardError = "Configuration not available" + ) + return + } + + PlatformLogger.i(TAG, "startAdapterWizard: Starting wizard for adapter type: $adapterType") + viewModelScope.launch { + _state.value = _state.value.copy( + showAdapterWizard = true, + adapterWizardType = adapterType, + adapterWizardLoading = true, + adapterWizardError = null, + adapterDiscoveredItems = emptyList(), + adapterDiscoveryExecuted = false, + adapterSelectOptions = emptyList() + ) + try { + val session = api.startAdapterConfiguration(adapterType) + _state.value = _state.value.copy( + adapterWizardSession = session, + adapterWizardLoading = false + ) + // Auto-execute discovery step if first step is discovery type + if (session.currentStep?.stepType == "discovery") { + PlatformLogger.i(TAG, "First step is discovery, auto-executing...") + executeAdapterDiscoveryStepInternal(session) + } + // Auto-fetch options for select steps + if (session.currentStep?.stepType == "select") { + PlatformLogger.i(TAG, "First step is select, auto-fetching options...") + fetchAdapterSelectOptionsInternal(session) + } + } catch (e: Exception) { + PlatformLogger.e(TAG, "startAdapterWizard: Failed - ${e.message}") + _state.value = _state.value.copy( + adapterWizardError = "Failed to start wizard: ${e.message}", + adapterWizardLoading = false + ) + } + } + } + + /** + * Execute the discovery step for an adapter wizard. + */ + fun executeAdapterDiscoveryStep() { + val session = _state.value.adapterWizardSession ?: return + val api = wizardApi ?: return + PlatformLogger.i(TAG, "executeAdapterDiscoveryStep: Executing discovery for session: ${session.sessionId}") + viewModelScope.launch { + _state.value = _state.value.copy( + adapterWizardLoading = true, + adapterWizardError = null + ) + try { + executeAdapterDiscoveryStepInternal(session) + } catch (e: Exception) { + PlatformLogger.e(TAG, "executeAdapterDiscoveryStep: Failed - ${e.message}") + _state.value = _state.value.copy( + adapterWizardError = "Discovery failed: ${e.message}", + adapterWizardLoading = false + ) + } + } + } + + private suspend fun executeAdapterDiscoveryStepInternal(session: ConfigSessionData) { + val api = wizardApi ?: return + val result = api.executeConfigurationStep(session.sessionId, emptyMap()) + _state.value = _state.value.copy( + adapterDiscoveryExecuted = true, + adapterDiscoveredItems = result.discoveredItems, + adapterWizardLoading = false + ) + if (result.nextStepIndex != null) { + _state.value = _state.value.copy( + adapterWizardSession = session.copy(currentStepIndex = result.nextStepIndex) + ) + } + } + + private suspend fun fetchAdapterSelectOptionsInternal(session: ConfigSessionData) { + val api = wizardApi ?: return + try { + val result = api.executeConfigurationStep(session.sessionId, emptyMap()) + if (result.selectOptions.isNotEmpty()) { + PlatformLogger.i(TAG, "Fetched ${result.selectOptions.size} select options") + _state.value = _state.value.copy(adapterSelectOptions = result.selectOptions) + } + } catch (e: Exception) { + PlatformLogger.e(TAG, "fetchAdapterSelectOptionsInternal: Failed - ${e.message}") + } + } + + /** + * Select a discovered item in the wizard. + */ + fun selectAdapterDiscoveredItem(item: DiscoveredItemData) { + val session = _state.value.adapterWizardSession ?: return + val api = wizardApi ?: return + PlatformLogger.i(TAG, "selectAdapterDiscoveredItem: Selected ${item.label}") + viewModelScope.launch { + _state.value = _state.value.copy(adapterWizardLoading = true) + try { + val stepData = mapOf( + "selected_url" to item.value, + "selected_id" to item.id + ) + val result = api.executeConfigurationStep(session.sessionId, stepData) + handleAdapterWizardStepResult(session, result) + } catch (e: Exception) { + PlatformLogger.e(TAG, "selectAdapterDiscoveredItem: Failed - ${e.message}") + _state.value = _state.value.copy( + adapterWizardError = "Failed to select item: ${e.message}", + adapterWizardLoading = false + ) + } + } + } + + /** + * Submit a manual URL in the discovery step. + */ + fun submitAdapterManualUrl(url: String) { + val session = _state.value.adapterWizardSession ?: return + val api = wizardApi ?: return + PlatformLogger.i(TAG, "submitAdapterManualUrl: Submitting URL: $url") + viewModelScope.launch { + _state.value = _state.value.copy(adapterWizardLoading = true) + try { + val stepData = mapOf("manual_url" to url) + val result = api.executeConfigurationStep(session.sessionId, stepData) + handleAdapterWizardStepResult(session, result) + } catch (e: Exception) { + PlatformLogger.e(TAG, "submitAdapterManualUrl: Failed - ${e.message}") + _state.value = _state.value.copy( + adapterWizardError = "Failed to submit URL: ${e.message}", + adapterWizardLoading = false + ) + } + } + } + + /** + * Submit the current wizard step with field values. + */ + fun submitAdapterWizardStep(stepData: Map) { + val session = _state.value.adapterWizardSession ?: return + val api = wizardApi ?: return + PlatformLogger.i(TAG, "submitAdapterWizardStep: Submitting step data: $stepData") + viewModelScope.launch { + _state.value = _state.value.copy( + adapterWizardLoading = true, + adapterWizardError = null + ) + try { + val result = api.executeConfigurationStep(session.sessionId, stepData) + handleAdapterWizardStepResult(session, result) + } catch (e: Exception) { + PlatformLogger.e(TAG, "submitAdapterWizardStep: Failed - ${e.message}") + _state.value = _state.value.copy( + adapterWizardError = "Failed to submit step: ${e.message}", + adapterWizardLoading = false + ) + } + } + } + + /** + * Initiate OAuth step in the adapter wizard. + */ + fun initiateAdapterOAuthStep() { + val session = _state.value.adapterWizardSession ?: return + val api = wizardApi ?: return + PlatformLogger.i(TAG, "initiateAdapterOAuthStep: Starting OAuth for session: ${session.sessionId}") + viewModelScope.launch { + _state.value = _state.value.copy( + adapterWizardLoading = true, + adapterWizardError = null + ) + try { + val stepData = mapOf("callback_base_url" to "http://127.0.0.1:8080") + val result = api.executeConfigurationStep(session.sessionId, stepData) + if (result.oauthUrl != null) { + PlatformLogger.i(TAG, "OAuth URL received: ${result.oauthUrl.take(80)}...") + _state.value = _state.value.copy( + adapterOAuthUrl = result.oauthUrl, + adapterAwaitingOAuthCallback = true, + adapterWizardLoading = false + ) + startAdapterOAuthPolling(session.sessionId) + } else { + PlatformLogger.e(TAG, "No OAuth URL in response") + _state.value = _state.value.copy( + adapterWizardError = "Failed to get OAuth URL", + adapterWizardLoading = false + ) + } + } catch (e: Exception) { + PlatformLogger.e(TAG, "initiateAdapterOAuthStep: Failed - ${e.message}") + _state.value = _state.value.copy( + adapterWizardError = "OAuth initiation failed: ${e.message}", + adapterWizardLoading = false + ) + } + } + } + + private fun startAdapterOAuthPolling(sessionId: String) { + val api = wizardApi ?: return + adapterOAuthPollJob?.cancel() + adapterOAuthPollJob = viewModelScope.launch { + PlatformLogger.i(TAG, "startAdapterOAuthPolling: Starting poll for session: $sessionId") + var attempts = 0 + val maxAttempts = 120 // 2 minutes + while (isActive && attempts < maxAttempts && _state.value.adapterAwaitingOAuthCallback) { + delay(1000) + attempts++ + try { + val updated = api.getConfigurationSessionStatus(sessionId) + val currentSession = _state.value.adapterWizardSession + if (currentSession != null && updated.currentStepIndex > currentSession.currentStepIndex) { + PlatformLogger.i(TAG, "OAuth callback received - step advanced") + _state.value = _state.value.copy( + adapterAwaitingOAuthCallback = false, + adapterOAuthUrl = null + ) + onAdapterOAuthStepAdvanced(updated) + return@launch + } + } catch (e: Exception) { + if (attempts % 10 == 0) { + PlatformLogger.e(TAG, "OAuth poll #$attempts failed: ${e.message}") + } + } + } + if (_state.value.adapterAwaitingOAuthCallback) { + PlatformLogger.e(TAG, "OAuth polling timed out") + _state.value = _state.value.copy( + adapterAwaitingOAuthCallback = false, + adapterWizardError = "OAuth authentication timed out. Please try again." + ) + } + } + } + + private suspend fun onAdapterOAuthStepAdvanced(updatedSession: ConfigSessionData) { + _state.value = _state.value.copy( + adapterWizardSession = updatedSession, + adapterDiscoveredItems = emptyList(), + adapterDiscoveryExecuted = false, + adapterSelectOptions = emptyList() + ) + + // Check if wizard is complete + if (updatedSession.currentStepIndex >= updatedSession.totalSteps) { + completeAdapterWizardInternal(updatedSession) + return + } + + // Auto-execute discovery or fetch select options for next step + if (updatedSession.currentStep?.stepType == "discovery") { + executeAdapterDiscoveryStepInternal(updatedSession) + } + if (updatedSession.currentStep?.stepType == "select") { + fetchAdapterSelectOptionsInternal(updatedSession) + } + } + + /** + * Check OAuth status on app resume. + */ + fun checkAdapterOAuthOnResume() { + if (!_state.value.adapterAwaitingOAuthCallback) return + val session = _state.value.adapterWizardSession ?: return + val api = wizardApi ?: return + PlatformLogger.i(TAG, "checkAdapterOAuthOnResume: Checking status...") + viewModelScope.launch { + try { + val updated = api.getConfigurationSessionStatus(session.sessionId) + if (updated.currentStepIndex > session.currentStepIndex) { + PlatformLogger.i(TAG, "OAuth completed while app was suspended") + _state.value = _state.value.copy( + adapterAwaitingOAuthCallback = false, + adapterOAuthUrl = null + ) + adapterOAuthPollJob?.cancel() + onAdapterOAuthStepAdvanced(updated) + } + } catch (e: Exception) { + PlatformLogger.e(TAG, "checkAdapterOAuthOnResume: Failed - ${e.message}") + } + } + } + + private suspend fun handleAdapterWizardStepResult(session: ConfigSessionData, result: ConfigStepResultData) { + val api = wizardApi ?: return + try { + val updatedSession = api.getConfigurationSessionStatus(session.sessionId) + PlatformLogger.i(TAG, "handleAdapterWizardStepResult: Step ${updatedSession.currentStepIndex}/${updatedSession.totalSteps}") + + // Check if wizard is complete + if (updatedSession.currentStepIndex >= updatedSession.totalSteps) { + PlatformLogger.i(TAG, "Wizard completed!") + completeAdapterWizardInternal(updatedSession) + return + } + + _state.value = _state.value.copy( + adapterWizardSession = updatedSession, + adapterDiscoveredItems = emptyList(), + adapterDiscoveryExecuted = false, + adapterSelectOptions = emptyList(), + adapterWizardLoading = false + ) + + // Auto-execute next step if needed + if (updatedSession.currentStep?.stepType == "discovery") { + executeAdapterDiscoveryStepInternal(updatedSession) + } + if (updatedSession.currentStep?.stepType == "select") { + fetchAdapterSelectOptionsInternal(updatedSession) + } + } catch (e: Exception) { + PlatformLogger.e(TAG, "handleAdapterWizardStepResult: Failed to fetch session status - ${e.message}") + _state.value = _state.value.copy(adapterWizardLoading = false) + if (result.nextStepIndex != null && result.nextStepIndex >= session.totalSteps) { + completeAdapterWizardInternal(session) + } + } + } + + private suspend fun completeAdapterWizardInternal(session: ConfigSessionData) { + val api = wizardApi ?: return + val adapterType = _state.value.adapterWizardType + try { + val completeResult = api.completeAdapterConfiguration(session.sessionId) + PlatformLogger.i(TAG, "completeAdapterWizardInternal: Completed - success=${completeResult.success}") + + // Store the collected config for this adapter + val collectedConfig = session.collectedConfig + val currentConfigured = _state.value.configuredAdapterData.toMutableMap() + if (adapterType != null) { + currentConfigured[adapterType] = collectedConfig + } + + // Enable the adapter since it's now configured + val currentEnabled = _state.value.enabledAdapterIds.toMutableSet() + if (adapterType != null) { + currentEnabled.add(adapterType) + } + + _state.value = _state.value.copy( + enabledAdapterIds = currentEnabled, + configuredAdapterData = currentConfigured + ) + closeAdapterWizard() + } catch (e: Exception) { + PlatformLogger.e(TAG, "completeAdapterWizardInternal: Failed - ${e.message}") + _state.value = _state.value.copy( + adapterWizardError = "Failed to apply configuration: ${e.message}", + adapterWizardLoading = false + ) + } + } + + /** + * Go back in the adapter wizard. + */ + fun adapterWizardBack() { + // For now, just close the session and clear state + _state.value = _state.value.copy( + adapterWizardSession = null, + adapterWizardError = null + ) + } + + /** + * Close the adapter wizard dialog. + */ + fun closeAdapterWizard() { + PlatformLogger.i(TAG, "closeAdapterWizard: Closing wizard") + adapterOAuthPollJob?.cancel() + _state.value = _state.value.copy( + showAdapterWizard = false, + adapterWizardType = null, + adapterWizardSession = null, + loadableAdaptersData = null, + adapterWizardError = null, + adapterWizardLoading = false, + adapterDiscoveredItems = emptyList(), + adapterDiscoveryExecuted = false, + adapterOAuthUrl = null, + adapterAwaitingOAuthCallback = false, + adapterSelectOptions = emptyList() + ) + } + /** * Reset to welcome step. */ @@ -602,14 +1062,17 @@ class SetupViewModel : ViewModel() { } } - // Build adapter config with accord metrics settings if consented - val adapterConfig = if (currentState.accordMetricsConsent) { - mapOf( - "CIRIS_ACCORD_METRICS_CONSENT" to "true", - "CIRIS_ACCORD_METRICS_TRACE_LEVEL" to "detailed" - ) - } else { - emptyMap() + // Build adapter config with consent settings + val adapterConfig = buildMap { + // Accord metrics settings + if (currentState.accordMetricsConsent) { + put("CIRIS_ACCORD_METRICS_CONSENT", "true") + put("CIRIS_ACCORD_METRICS_TRACE_LEVEL", "detailed") + } + // Public API services (Navigation & Weather) + if (currentState.publicApiServicesEnabled && currentState.publicApiEmail.isNotBlank()) { + put("PUBLIC_API_CONTACT_EMAIL", currentState.publicApiEmail) + } } // Node flow fields (if provisioned via Portal) diff --git a/tests/ciris_engine/logic/adapters/api/routes/test_audit_helpers.py b/tests/ciris_engine/logic/adapters/api/routes/test_audit_helpers.py new file mode 100644 index 000000000..bb3501480 --- /dev/null +++ b/tests/ciris_engine/logic/adapters/api/routes/test_audit_helpers.py @@ -0,0 +1,609 @@ +""" +Unit tests for audit API helper functions. + +Tests the extracted helper functions used to reduce cognitive complexity +in the audit multi-source merging logic. +""" + +import json +from datetime import datetime, timezone +from typing import Any, Dict +from unittest.mock import MagicMock + +import pytest + +from ciris_engine.logic.adapters.api.routes.audit import ( + AuditEntryResponse, + _MergedAuditEntry, + _add_new_graph_entry, + _add_new_sqlite_entry, + _entry_has_additional_metadata, + _extract_handler_metadata, + _extract_sqlite_entry_info, + _find_sqlite_entry_for_dedup_key, + _handle_duplicate_graph_entry, + _infer_outcome_from_event, + _merge_graph_metadata_into_entry, + _normalize_timestamp_str, + _parse_event_payload_metadata, + _track_sqlite_dedup_key, +) +from ciris_engine.schemas.api.audit import AuditContext +from ciris_engine.schemas.services.nodes import AuditEntry + + +class TestNormalizeTimestampStr: + """Tests for _normalize_timestamp_str function.""" + + def test_normalizes_space_to_t(self): + """Should replace space with T for ISO format.""" + result = _normalize_timestamp_str("2026-03-25 01:21:00.503015+00:00") + assert result == "2026-03-25T01:21:00.503015+00:00" + + def test_preserves_already_normalized(self): + """Should preserve already normalized timestamps.""" + result = _normalize_timestamp_str("2026-03-25T01:21:00.503015+00:00") + assert result == "2026-03-25T01:21:00.503015+00:00" + + def test_handles_empty_string(self): + """Should handle empty string.""" + result = _normalize_timestamp_str("") + assert result == "" + + +class TestEntryHasAdditionalMetadata: + """Tests for _entry_has_additional_metadata function.""" + + def test_returns_true_with_metadata(self): + """Should return True when entry has additional_data.""" + entry = MagicMock(spec=AuditEntry) + entry.context = MagicMock() + entry.context.additional_data = {"key": "value"} + assert _entry_has_additional_metadata(entry) is True + + def test_returns_false_without_context(self): + """Should return False when entry has no context.""" + entry = MagicMock() + del entry.context # Remove context attribute + assert _entry_has_additional_metadata(entry) is False + + def test_returns_false_without_additional_data_attr(self): + """Should return False when context has no additional_data.""" + entry = MagicMock(spec=AuditEntry) + entry.context = MagicMock() + del entry.context.additional_data # Remove additional_data attribute + assert _entry_has_additional_metadata(entry) is False + + def test_returns_false_with_empty_additional_data(self): + """Should return False when additional_data is empty.""" + entry = MagicMock(spec=AuditEntry) + entry.context = MagicMock() + entry.context.additional_data = {} + assert _entry_has_additional_metadata(entry) is False + + def test_returns_false_with_none_additional_data(self): + """Should return False when additional_data is None.""" + entry = MagicMock(spec=AuditEntry) + entry.context = MagicMock() + entry.context.additional_data = None + assert _entry_has_additional_metadata(entry) is False + + +class TestInferOutcomeFromEvent: + """Tests for _infer_outcome_from_event function.""" + + def test_returns_provided_outcome(self): + """Should return provided outcome when not None.""" + assert _infer_outcome_from_event("custom_outcome", "some_event") == "custom_outcome" + + def test_infers_failure_from_fail_event(self): + """Should infer failure when event_type contains 'fail'.""" + assert _infer_outcome_from_event(None, "action_failed") == "failure" + assert _infer_outcome_from_event(None, "TASK_FAIL") == "failure" + + def test_infers_failure_from_error_event(self): + """Should infer failure when event_type contains 'error'.""" + assert _infer_outcome_from_event(None, "error_occurred") == "failure" + assert _infer_outcome_from_event(None, "VALIDATION_ERROR") == "failure" + + def test_infers_success_for_normal_events(self): + """Should infer success for normal events.""" + assert _infer_outcome_from_event(None, "task_complete") == "success" + assert _infer_outcome_from_event(None, "SPEAK") == "success" + assert _infer_outcome_from_event(None, "MEMORIZE") == "success" + + def test_handles_empty_event_type(self): + """Should handle empty event_type.""" + assert _infer_outcome_from_event(None, "") == "success" + + +class TestExtractHandlerMetadata: + """Tests for _extract_handler_metadata function.""" + + def test_extracts_defer_params(self): + """Should extract DEFER handler parameters.""" + params = {"defer_reason": "need_human_input", "defer_until": "2026-03-26"} + result = _extract_handler_metadata(params) + assert result["defer_reason"] == "need_human_input" + assert result["defer_until"] == "2026-03-26" + + def test_extracts_tool_params_with_tool_name(self): + """Should extract TOOL handler parameters with tool_name key.""" + params = {"tool_name": "web_search", "parameters": {"query": "test"}} + result = _extract_handler_metadata(params) + assert result["tool_name"] == "web_search" + assert "tool_parameters" in result + + def test_extracts_tool_params_with_name_key(self): + """Should extract TOOL handler parameters with name key fallback.""" + params = {"name": "calculator"} + result = _extract_handler_metadata(params) + assert result["tool_name"] == "calculator" + + def test_extracts_tool_parameters_as_json(self): + """Should serialize dict parameters as JSON.""" + params = {"parameters": {"key": "value", "nested": {"a": 1}}} + result = _extract_handler_metadata(params) + parsed = json.loads(result["tool_parameters"]) + assert parsed == {"key": "value", "nested": {"a": 1}} + + def test_extracts_tool_parameters_as_string(self): + """Should convert non-dict parameters to string.""" + params = {"parameters": "simple_string"} + result = _extract_handler_metadata(params) + assert result["tool_parameters"] == "simple_string" + + def test_extracts_ponder_questions(self): + """Should extract PONDER handler questions.""" + params = {"ponder_questions": ["What is the meaning?", "How to proceed?"]} + result = _extract_handler_metadata(params) + assert result["ponder_questions"] == ["What is the meaning?", "How to proceed?"] + + def test_extracts_ponder_questions_fallback(self): + """Should extract questions key as fallback for ponder_questions.""" + params = {"questions": ["Question 1"]} + result = _extract_handler_metadata(params) + assert result["ponder_questions"] == ["Question 1"] + + def test_extracts_speak_content(self): + """Should extract SPEAK handler content.""" + params = {"content": "Hello, world!"} + result = _extract_handler_metadata(params) + assert result["content"] == "Hello, world!" + + def test_extracts_task_complete_reason(self): + """Should extract TASK_COMPLETE handler reason.""" + params = {"completion_reason": "Task finished successfully"} + result = _extract_handler_metadata(params) + assert result["completion_reason"] == "Task finished successfully" + + def test_extracts_reject_reason(self): + """Should extract REJECT handler reason.""" + params = {"reason": "Request violates policy"} + result = _extract_handler_metadata(params) + assert result["reject_reason"] == "Request violates policy" + + def test_handles_empty_params(self): + """Should return empty dict for empty params.""" + result = _extract_handler_metadata({}) + assert result == {} + + def test_extracts_multiple_handler_types(self): + """Should extract all present handler types.""" + params = { + "defer_reason": "waiting", + "tool_name": "search", + "content": "message", + } + result = _extract_handler_metadata(params) + assert result["defer_reason"] == "waiting" + assert result["tool_name"] == "search" + assert result["content"] == "message" + + +class TestParseEventPayloadMetadata: + """Tests for _parse_event_payload_metadata function.""" + + def test_returns_empty_for_none(self): + """Should return empty metadata and None description for None input.""" + metadata, description = _parse_event_payload_metadata(None) + assert metadata == {} + assert description is None + + def test_returns_empty_for_empty_string(self): + """Should return empty metadata and None description for empty string.""" + metadata, description = _parse_event_payload_metadata("") + assert metadata == {} + assert description is None + + def test_handles_invalid_json(self): + """Should return original string as description for invalid JSON.""" + metadata, description = _parse_event_payload_metadata("not valid json") + assert metadata == {} + assert description == "not valid json" + + def test_handles_non_dict_json(self): + """Should handle JSON that parses to non-dict.""" + metadata, description = _parse_event_payload_metadata('"just a string"') + assert metadata == {} + assert description == '"just a string"' + + def test_extracts_direct_payload_fields(self): + """Should extract direct payload fields.""" + payload = { + "thought_id": "thought-123", + "task_id": "task-456", + "handler_name": "SPEAK", + "action_type": "speak_action", + } + metadata, description = _parse_event_payload_metadata(json.dumps(payload)) + assert metadata["thought_id"] == "thought-123" + assert metadata["task_id"] == "task-456" + assert metadata["handler_name"] == "SPEAK" + assert metadata["action_type"] == "speak_action" + + def test_extracts_nested_parameters(self): + """Should extract nested parameters with handler metadata.""" + payload = { + "action_type": "tool_call", + "parameters": json.dumps({"tool_name": "calculator", "content": "2+2"}), + } + metadata, description = _parse_event_payload_metadata(json.dumps(payload)) + assert metadata["tool_name"] == "calculator" + assert metadata["content"] == "2+2" + assert metadata["action_type"] == "tool_call" + assert description == "tool_call" + + def test_uses_action_type_as_description(self): + """Should use action_type as description.""" + payload = {"action_type": "speak_action", "handler_name": "SPEAK"} + metadata, description = _parse_event_payload_metadata(json.dumps(payload)) + assert description == "speak_action" + + def test_uses_handler_name_as_fallback_description(self): + """Should use handler_name as fallback description.""" + payload = {"handler_name": "MEMORIZE"} + metadata, description = _parse_event_payload_metadata(json.dumps(payload)) + assert description == "MEMORIZE" + + def test_handles_invalid_nested_parameters(self): + """Should handle invalid JSON in nested parameters.""" + payload = {"parameters": "not valid json", "action_type": "test"} + metadata, description = _parse_event_payload_metadata(json.dumps(payload)) + assert description == "test" + # Should not crash, just skip parameter extraction + + +class TestExtractSqliteEntryInfo: + """Tests for _extract_sqlite_entry_info function.""" + + def test_extracts_all_fields(self): + """Should extract all required fields from SQLite entry.""" + sqlite_entry = { + "event_timestamp": "2026-03-25T12:00:00+00:00", + "originator_id": "user-123", + "event_type": "SPEAK", + "event_id": "evt-456", + } + result = _extract_sqlite_entry_info(sqlite_entry) + assert result["event_timestamp"] == "2026-03-25T12:00:00+00:00" + assert result["originator_id"] == "user-123" + assert result["event_type"] == "SPEAK" + assert result["entry_id"] == "evt-456" + assert result["dedup_key"] == "2026-03-25T12:00:00+00:00_SPEAK" + + def test_generates_entry_id_when_missing(self): + """Should generate entry_id when event_id is missing.""" + sqlite_entry = { + "event_timestamp": "2026-03-25T12:00:00+00:00", + "originator_id": "user-123", + "event_type": "SPEAK", + } + result = _extract_sqlite_entry_info(sqlite_entry) + assert result["entry_id"] == "audit_2026-03-25T12:00:00+00:00_user-123" + + def test_uses_defaults_for_missing_fields(self): + """Should use default values for missing fields.""" + sqlite_entry = {} + result = _extract_sqlite_entry_info(sqlite_entry) + assert result["event_timestamp"] == "" + assert result["originator_id"] == "unknown" + assert result["event_type"] == "unknown" + + +class TestTrackSqliteDedupKey: + """Tests for _track_sqlite_dedup_key function.""" + + def test_adds_to_seen_timestamps(self): + """Should add dedup_key to seen_timestamps set.""" + entry_info = {"dedup_key": "2026-03-25T12:00:00_SPEAK", "entry_id": "evt-123"} + seen_timestamps: set[str] = set() + dedup_to_entry: Dict[str, str] = {} + + _track_sqlite_dedup_key(entry_info, seen_timestamps, dedup_to_entry) + + assert "2026-03-25T12:00:00_SPEAK" in seen_timestamps + + def test_maps_dedup_key_to_entry_id(self): + """Should map dedup_key to entry_id.""" + entry_info = {"dedup_key": "2026-03-25T12:00:00_SPEAK", "entry_id": "evt-123"} + seen_timestamps: set[str] = set() + dedup_to_entry: Dict[str, str] = {} + + _track_sqlite_dedup_key(entry_info, seen_timestamps, dedup_to_entry) + + assert dedup_to_entry["2026-03-25T12:00:00_SPEAK"] == "evt-123" + + +class TestMergeGraphMetadataIntoEntry: + """Tests for _merge_graph_metadata_into_entry function.""" + + def test_creates_metadata_dict_if_none(self): + """Should create metadata dict if it's None.""" + entry_response = AuditEntryResponse( + id="test-id", + action="SPEAK", + actor="user", + timestamp=datetime.now(timezone.utc), + context=AuditContext(metadata=None), + ) + merged_entry = _MergedAuditEntry(entry=entry_response, sources=["sqlite"]) + + graph_ctx = MagicMock() + graph_ctx.additional_data = {"key": "value"} + + _merge_graph_metadata_into_entry(merged_entry, graph_ctx) + + assert merged_entry.entry.context.metadata == {"key": "value"} + + def test_merges_without_overwriting(self): + """Should merge metadata without overwriting existing keys.""" + entry_response = AuditEntryResponse( + id="test-id", + action="SPEAK", + actor="user", + timestamp=datetime.now(timezone.utc), + context=AuditContext(metadata={"existing": "data"}), + ) + merged_entry = _MergedAuditEntry(entry=entry_response, sources=["sqlite"]) + + graph_ctx = MagicMock() + graph_ctx.additional_data = {"existing": "new_value", "new_key": "new_data"} + + _merge_graph_metadata_into_entry(merged_entry, graph_ctx) + + assert merged_entry.entry.context.metadata["existing"] == "data" # Not overwritten + assert merged_entry.entry.context.metadata["new_key"] == "new_data" # Added + + def test_handles_no_additional_data(self): + """Should handle context without additional_data.""" + entry_response = AuditEntryResponse( + id="test-id", + action="SPEAK", + actor="user", + timestamp=datetime.now(timezone.utc), + context=AuditContext(metadata=None), + ) + merged_entry = _MergedAuditEntry(entry=entry_response, sources=["sqlite"]) + + graph_ctx = MagicMock() + graph_ctx.additional_data = None + + _merge_graph_metadata_into_entry(merged_entry, graph_ctx) + + assert merged_entry.entry.context.metadata == {} + + +class TestFindSqliteEntryForDedupKey: + """Tests for _find_sqlite_entry_for_dedup_key function.""" + + def test_finds_matching_entry(self): + """Should find entry with matching dedup key.""" + timestamp = datetime(2026, 3, 25, 12, 0, 0, tzinfo=timezone.utc) + entry_response = AuditEntryResponse( + id="test-id", + action="SPEAK", + actor="user", + timestamp=timestamp, + context=AuditContext(), + ) + merged_entry = _MergedAuditEntry(entry=entry_response, sources=["sqlite"]) + merged = {"test-id": merged_entry} + + result = _find_sqlite_entry_for_dedup_key(merged, "2026-03-25T12:00:00+00:00_SPEAK") + + assert result is merged_entry + + def test_returns_none_for_no_match(self): + """Should return None when no matching entry exists.""" + timestamp = datetime(2026, 3, 25, 12, 0, 0, tzinfo=timezone.utc) + entry_response = AuditEntryResponse( + id="test-id", + action="SPEAK", + actor="user", + timestamp=timestamp, + context=AuditContext(), + ) + merged_entry = _MergedAuditEntry(entry=entry_response, sources=["sqlite"]) + merged = {"test-id": merged_entry} + + result = _find_sqlite_entry_for_dedup_key(merged, "2026-03-25T13:00:00+00:00_TOOL") + + assert result is None + + def test_returns_none_for_empty_merged(self): + """Should return None for empty merged dict.""" + result = _find_sqlite_entry_for_dedup_key({}, "any_key") + assert result is None + + +class TestHandleDuplicateGraphEntry: + """Tests for _handle_duplicate_graph_entry function.""" + + def test_does_nothing_without_metadata(self): + """Should do nothing when has_metadata is False.""" + merged: Dict[str, _MergedAuditEntry] = {} + entry = MagicMock(spec=AuditEntry) + logger = MagicMock() + + _handle_duplicate_graph_entry(merged, entry, "dedup_key", False, logger) + + logger.warning.assert_not_called() + + def test_merges_metadata_when_match_found(self): + """Should merge metadata when matching SQLite entry exists.""" + timestamp = datetime(2026, 3, 25, 12, 0, 0, tzinfo=timezone.utc) + entry_response = AuditEntryResponse( + id="test-id", + action="SPEAK", + actor="user", + timestamp=timestamp, + context=AuditContext(metadata=None), + ) + merged_entry = _MergedAuditEntry(entry=entry_response, sources=["sqlite"]) + merged = {"test-id": merged_entry} + + entry = MagicMock(spec=AuditEntry) + entry.context = MagicMock() + entry.context.additional_data = {"graph_key": "graph_value"} + + logger = MagicMock() + + _handle_duplicate_graph_entry(merged, entry, "2026-03-25T12:00:00+00:00_SPEAK", True, logger) + + assert "graph" in merged_entry.sources + assert merged_entry.entry.context.metadata["graph_key"] == "graph_value" + + def test_logs_warning_when_no_match_found(self): + """Should log warning when no matching SQLite entry found.""" + merged: Dict[str, _MergedAuditEntry] = {} + entry = MagicMock(spec=AuditEntry) + entry.context = MagicMock() + entry.context.additional_data = {"key": "value"} + + logger = MagicMock() + + _handle_duplicate_graph_entry(merged, entry, "nonexistent_key", True, logger) + + logger.warning.assert_called_once() + + +class TestAddNewGraphEntry: + """Tests for _add_new_graph_entry function.""" + + def test_adds_new_entry_to_merged(self): + """Should add new entry when entry_id not in merged.""" + merged: Dict[str, _MergedAuditEntry] = {} + seen_timestamps: set[str] = set() + + entry = MagicMock(spec=AuditEntry) + entry.action = "SPEAK" + entry.actor = "user" + entry.timestamp = datetime(2026, 3, 25, 12, 0, 0, tzinfo=timezone.utc) + entry.signature = None + entry.hash_chain = None + entry.context = MagicMock() + entry.context.model_dump = MagicMock(return_value={}) + + logger = MagicMock() + + _add_new_graph_entry(merged, entry, "new-entry-id", "dedup_key", seen_timestamps, logger) + + assert "new-entry-id" in merged + assert "graph" in merged["new-entry-id"].sources + assert "dedup_key" in seen_timestamps + + def test_appends_source_for_existing_entry(self): + """Should append 'graph' source when entry_id already exists.""" + timestamp = datetime(2026, 3, 25, 12, 0, 0, tzinfo=timezone.utc) + entry_response = AuditEntryResponse( + id="existing-id", + action="SPEAK", + actor="user", + timestamp=timestamp, + context=AuditContext(), + ) + existing_entry = _MergedAuditEntry(entry=entry_response, sources=["sqlite"]) + merged = {"existing-id": existing_entry} + seen_timestamps: set[str] = set() + + entry = MagicMock(spec=AuditEntry) + logger = MagicMock() + + _add_new_graph_entry(merged, entry, "existing-id", "dedup_key", seen_timestamps, logger) + + assert "sqlite" in merged["existing-id"].sources + assert "graph" in merged["existing-id"].sources + + +class TestAddNewSqliteEntry: + """Tests for _add_new_sqlite_entry function.""" + + def test_adds_new_entry_with_payload(self): + """Should add new SQLite entry with parsed payload.""" + merged: Dict[str, _MergedAuditEntry] = {} + sqlite_entry = { + "event_timestamp": "2026-03-25T12:00:00+00:00", + "originator_id": "user-123", + "event_type": "TOOL", + "event_id": "evt-456", + "event_payload": json.dumps({ + "action_type": "tool_call", + "parameters": json.dumps({"tool_name": "calculator"}), + }), + "signature": "sig-123", + "previous_hash": "hash-123", + } + entry_info = { + "event_timestamp": "2026-03-25T12:00:00+00:00", + "originator_id": "user-123", + "event_type": "TOOL", + "entry_id": "evt-456", + } + + _add_new_sqlite_entry(merged, sqlite_entry, entry_info) + + assert "evt-456" in merged + assert merged["evt-456"].entry.action == "TOOL" + assert merged["evt-456"].entry.actor == "user-123" + assert "sqlite" in merged["evt-456"].sources + assert merged["evt-456"].entry.context.metadata["tool_name"] == "calculator" + + def test_adds_new_entry_without_payload(self): + """Should add new SQLite entry without payload.""" + merged: Dict[str, _MergedAuditEntry] = {} + sqlite_entry = { + "event_timestamp": "2026-03-25T12:00:00+00:00", + "originator_id": "user-123", + "event_type": "SPEAK", + } + entry_info = { + "event_timestamp": "2026-03-25T12:00:00+00:00", + "originator_id": "user-123", + "event_type": "SPEAK", + "entry_id": "audit_2026-03-25T12:00:00+00:00_user-123", + } + + _add_new_sqlite_entry(merged, sqlite_entry, entry_info) + + assert "audit_2026-03-25T12:00:00+00:00_user-123" in merged + assert merged["audit_2026-03-25T12:00:00+00:00_user-123"].entry.action == "SPEAK" + + def test_infers_outcome_from_event_type(self): + """Should infer outcome from event type.""" + merged: Dict[str, _MergedAuditEntry] = {} + sqlite_entry = { + "event_timestamp": "2026-03-25T12:00:00+00:00", + "originator_id": "user-123", + "event_type": "action_failed", + } + entry_info = { + "event_timestamp": "2026-03-25T12:00:00+00:00", + "originator_id": "user-123", + "event_type": "action_failed", + "entry_id": "evt-fail", + } + + _add_new_sqlite_entry(merged, sqlite_entry, entry_info) + + assert merged["evt-fail"].entry.context.outcome == "failure" diff --git a/tests/ciris_engine/logic/adapters/api/routes/test_audit_multi_source.py b/tests/ciris_engine/logic/adapters/api/routes/test_audit_multi_source.py index 17d7e9511..29e7a6c6a 100644 --- a/tests/ciris_engine/logic/adapters/api/routes/test_audit_multi_source.py +++ b/tests/ciris_engine/logic/adapters/api/routes/test_audit_multi_source.py @@ -281,25 +281,29 @@ class TestMergeAuditSources: @pytest.mark.asyncio async def test_merge_all_sources(self, mock_graph_entries, mock_sqlite_db, mock_jsonl_file): - """Test merging audit entries - SQLite is authoritative when present.""" + """Test merging audit entries - all unique entries from all sources are preserved.""" # Get data from fixtures sqlite_entries = await _query_sqlite_audit(mock_sqlite_db) jsonl_entries = await _query_jsonl_audit(mock_jsonl_file) - # Merge all sources - SQLite is authoritative so graph/jsonl are skipped + # Merge all sources - entries are deduplicated by timestamp+action merged = await _merge_audit_sources(mock_graph_entries, sqlite_entries, jsonl_entries) - # Should have only SQLite entries (SQLite is authoritative) - assert len(merged) == 3 # Only 3 sqlite entries + # Should have entries from all sources (unique by timestamp+action) + # 3 sqlite + 2 jsonl + 2 graph = 7-8 (depending on dedup) + assert len(merged) >= 3 # At least sqlite entries - # All entries should be from sqlite - storage_sources = [entry.storage_sources for entry in merged] - assert all(sources == ["sqlite"] for sources in storage_sources) + # Check we have entries from multiple sources + all_sources = set() + for entry in merged: + all_sources.update(entry.storage_sources) + # SQLite entries should always be present + assert "sqlite" in all_sources @pytest.mark.asyncio async def test_merge_with_duplicates(self, mock_graph_entries): - """Test merging handles duplicate entries - SQLite is authoritative.""" - # Create duplicate entries across sources + """Test merging handles duplicate entries - entries show all sources they appear in.""" + # Create duplicate entries across sources (same timestamp+action = same event) sqlite_entries = [ { "event_id": "graph_001", # Same ID as graph entry @@ -326,12 +330,13 @@ async def test_merge_with_duplicates(self, mock_graph_entries): merged = await _merge_audit_sources(mock_graph_entries[:1], sqlite_entries, jsonl_entries) - # SQLite is authoritative - only SQLite entry should be present + # Should be deduplicated to 1 entry (same timestamp+action) assert len(merged) == 1 - # Entry should only have sqlite as storage source (authoritative source) + # Entry should show all sources it appears in (sorted alphabetically) graph_001_entry = next(entry for entry in merged if entry.id == "graph_001") - assert graph_001_entry.storage_sources == ["sqlite"] + assert "sqlite" in graph_001_entry.storage_sources # SQLite is authoritative + # May also include graph and/or jsonl if they were merged @pytest.mark.asyncio async def test_merge_empty_sources(self): @@ -681,11 +686,11 @@ async def test_defer_reason_extracted_from_nested_parameters(self): @pytest.mark.asyncio async def test_sqlite_authoritative_deduplication(self): - """Test that when SQLite has entries, graph and JSONL sources are skipped.""" - # Create mock graph entry + """Test that entries with same timestamp+action are deduplicated, with SQLite as authoritative.""" + # Create mock graph entry with SAME action as SQLite for deduplication mock_graph = MagicMock() mock_graph.id = "graph_dup_001" - mock_graph.action = "SPEAK" + mock_graph.action = "HANDLER_ACTION_SPEAK" # Same action as SQLite mock_graph.actor = "user_graph" mock_graph.timestamp = datetime(2025, 9, 1, 10, 0, 0, tzinfo=timezone.utc) mock_graph.signature = "graph_sig_001" @@ -712,18 +717,19 @@ async def test_sqlite_authoritative_deduplication(self): { "id": "jsonl_dup_001", "timestamp": "2025-09-01T10:00:00+00:00", - "action": "SPEAK", + "action": "HANDLER_ACTION_SPEAK", # Same action as SQLite "actor": "user_jsonl", } ] merged = await _merge_audit_sources(graph_entries, sqlite_entries, jsonl_entries) - # Should only have 1 entry (SQLite is authoritative when present) + # Should be deduplicated to 1 entry (same timestamp+action) assert len(merged) == 1 - # Should be from SQLite - verify by checking storage_sources and signature - assert merged[0].storage_sources == ["sqlite"] + # SQLite is authoritative - should have its signature assert merged[0].signature == "sig_sqlite_001" + # SQLite should be in storage_sources + assert "sqlite" in merged[0].storage_sources @pytest.mark.asyncio async def test_graph_and_jsonl_used_when_sqlite_empty(self): @@ -761,6 +767,43 @@ async def test_graph_and_jsonl_used_when_sqlite_empty(self): assert "graph_only_001" in entry_ids assert "jsonl_only_001" in entry_ids + @pytest.mark.asyncio + async def test_jsonl_source_added_when_graph_entry_exists(self): + """Test that JSONL source is added to graph entry when they have same timestamp+action.""" + # Create mock graph entry + mock_graph = MagicMock() + mock_graph.id = "graph_dup_002" + mock_graph.action = "SPEAK" + mock_graph.actor = "user_graph" + mock_graph.timestamp = datetime(2025, 9, 1, 10, 0, 0, tzinfo=timezone.utc) + mock_graph.signature = "graph_sig_002" + mock_graph.hash_chain = "graph_hash_002" + mock_graph.context = MagicMock() + mock_graph.context.model_dump.return_value = {} + + graph_entries = [mock_graph] + + # JSONL entry with SAME timestamp+action (should be deduplicated, adding jsonl to sources) + jsonl_entries = [ + { + "id": "jsonl_dup_002", + "timestamp": "2025-09-01T10:00:00+00:00", # Same timestamp + "action": "SPEAK", # Same action + "actor": "user_jsonl", + } + ] + + # No SQLite entries - graph is first source + sqlite_entries: List[dict] = [] + + merged = await _merge_audit_sources(graph_entries, sqlite_entries, jsonl_entries) + + # Should be deduplicated to 1 entry + assert len(merged) == 1 + # Should have both graph and jsonl in storage_sources + assert "graph" in merged[0].storage_sources + assert "jsonl" in merged[0].storage_sources + @pytest.mark.asyncio async def test_malformed_payload_json_handled_gracefully(self): """Test that malformed event_payload JSON doesn't crash metadata extraction."""