squashed commit

This commit is contained in:
hoangvv
2026-09-18 13:55:25 +07:00
commit 039c98d4d0
45882 changed files with 24235585 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
/.venv
/.pytest_cache
# C extensions
*.so
# Distribution / packaging
.Python
env/
bin/
build/
develop-eggs/
dist/
eggs/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
*.eggs
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Pycharm
.idea
# Eclipse
.settings
# Visual Studio cache/options directory
.vs/
.vscode
# OSX Files
.DS_Store
# Django stuff:
*.log
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Rope
.ropeproject
# Sphinx documentation
docs/_build/
# Backup files
*~
*.swp
# OCA rules
!static/lib/
.ruff_cache
.claudesync
llm_thread/gptree_output.txt
**/.gptree_config
**/gptree_output.txt
+187
View File
@@ -0,0 +1,187 @@
# Changelog
All notable changes to the Odoo LLM Integration project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- **Tool Event System**: Real-time tool execution tracking with streaming events (2025-01-12)
- Added `tool_called` event when tool execution begins (`llm_tool/models/mail_message.py:120-128`)
- Added `tool_succeeded` event when tool completes successfully (`llm_tool/models/mail_message.py:147-157`)
- Added `tool_failed` event when tool execution fails (`llm_tool/models/mail_message.py:166-176`)
- Events include comprehensive tool data: tool_call_id, tool_name, arguments, status, result/error
- Events are yielded through existing generator chain for automatic propagation to Fleek platform
### Changed
- **Enhanced Tool Execution Flow**: `mail.message.execute_tool_call()` now emits real-time events (2025-01-12)
- Tool execution status is now broadcasted in real-time for UI updates
- Maintains backward compatibility with existing message-based status tracking
- Events automatically flow through `yield from` chain to Fleek broadcasting system
### Technical Details
- Tool events are generated at key execution points in `execute_tool_call()` method
- Event structure follows consistent pattern with `type` and `tool_data` fields
- Integration with Fleek platform enables WebSocket broadcasting to clients
- Supports real-time tracking of image generation and other AI tool operations
## [16.0-pr] - 2025-01-04 - Major Architecture Consolidation
### Added
- **PostgreSQL Advisory Locking**: Prevents race conditions in concurrent generation scenarios
- **Unified Generation API**: New `generate()` method provides consistent interface for text, image, and other content types
- **Enhanced Message System**: Added `body_json` field to `mail.message` for structured data storage
- **Indexed Role Field**: New `llm_role` field for 10x faster message queries
- **Auto-Detection System**: Automatic prompt argument detection and schema synchronization
- **Schema Source Transparency**: Clear indication of schema sources in UI (Prompt vs Model vs None)
- **Loading State Management**: Proper async handling and loading indicators in forms
- **Comprehensive Test Coverage**: Tests for prompt arguments, thread schemas, and race condition fixes
### Changed
- **Module Consolidation**: Merged `llm_resource` functionality into `llm_knowledge` module
- **Prompt Integration**: Moved `llm_prompt` functionality into `llm_assistant` module
- **Message Subtypes**: Moved from separate module into base `llm` module
- **Tool Message Format**: All tool data now stored in `body_json` instead of separate fields
- **Assistant Management**: Enhanced with integrated prompt templates and testing capabilities
- **Generation Forms**: Improved with automatic schema detection and better error handling
### Removed
- **llm_resource module**: Consolidated into `llm_knowledge`
- **llm_prompt module**: Integrated into `llm_assistant`
- **llm_mail_message_subtypes module**: Moved to base `llm` module
- **Deprecated API methods**: Replaced with unified generation interface
### Fixed
- **Race Conditions**: Fixed async loading issues in media form components
- **Schema Computation**: Eliminated inconsistencies between template and arguments
- **Form Loading**: Prevented empty forms and incorrect field rendering
- **Context Management**: Improved handling of context changes and reloads
- **Tool Execution**: Better error handling and structured data storage
- **Performance Issues**: Optimized database queries with indexed fields
### Security
- **Enhanced Tool Consent**: Improved security framework for tool execution
- **Role-Based Access**: Strengthened permission-based tool access control
### Migration Notes
- **Automatic Migration**: All existing installations will be automatically migrated
- **Data Preservation**: No loss of existing messages and tool execution history
- **Backward Compatibility**: Maintains support for existing workflows during transition
- **Module Updates**: Dependencies automatically updated to reflect consolidations
## [Previous Releases]
### Module Version History
#### Core Modules
- **llm**: 16.0.1.3.0 (2025-01-04) - Message subtypes integration and role optimization
- **llm_assistant**: 16.0.1.4.0 (2025-01-04) - Integrated prompt templates and enhanced testing
- **llm_thread**: 16.0.1.3.0 (2025-01-04) - Role field optimization and PostgreSQL locking
- **llm_tool**: 16.0.3.0.0 (2025-01-04) - Body_json refactoring and enhanced execution
- **llm_generate**: 16.0.2.0.0 (2025-01-04) - Unified generation API and clean integration
- **llm_store**: 16.0.1.0.0 (2025-01-02) - Vector store abstraction framework
#### Provider Modules
- **llm_openai**: 16.0.1.1.3 (2025-01-04) - Enhanced tool support and API improvements
- **llm_anthropic**: 16.0.1.1.0 (2025-03-06) - Anthropic provider enhancements
- **llm_ollama**: 16.0.1.1.0 (2025-03-06) - Chat method parameter updates
- **llm_mistral**: 16.0.1.0.0 (2025-01-02) - Mistral AI integration
- **llm_litellm**: 16.0.1.1.0 (2025-03-06) - LiteLLM integration updates
- **llm_replicate**: 16.0.1.1.0 (2025-03-06) - Replicate provider improvements
- **llm_fal_ai**: 16.0.2.0.0 (2025-01-04) - Unified generate endpoint and schema storage
#### Knowledge & Vector Store Modules
- **llm_knowledge**: 16.0.1.1.0 (2025-01-04) - Consolidated resource management and RAG
- **llm_chroma**: 16.0.1.0.0 (2025-01-02) - ChromaDB vector store integration
- **llm_pgvector**: 16.0.1.0.0 (2025-01-02) - PostgreSQL vector extension
- **llm_qdrant**: 16.0.1.0.0 (2025-01-02) - Qdrant vector database integration
#### Specialized Modules
- **llm_mcp**: 16.0.1.0.0 (2025-01-02) - Model Context Protocol support
- **llm_training**: 16.0.1.0.0 (2025-01-02) - Fine-tuning capabilities
- **llm_tool_knowledge**: 16.0.1.0.0 (2025-01-02) - Knowledge base tool integration
### Historical Changes (2025)
#### January 2025
- **16.0.1.2.0** (llm_thread) - LLM base module message subtypes integration
- **16.0.1.0.1** (llm_tool) - Minor fixes and improvements
#### March 2025
- **16.0.1.1.0** (llm_thread) - Tool integration in chat interface
- **16.0.1.1.0** (multiple providers) - Chat method parameter updates
#### April 2025
- **16.0.1.1.1** (llm_thread) - Method name consistency updates
- **16.0.1.0.1** (llm_tool) - Additional fixes and improvements
## Performance Improvements
### Database Optimization
- **10x Query Performance**: Indexed `llm_role` field eliminates expensive subtype lookups
- **Reduced Complexity**: Consolidated modules reduce maintenance overhead
- **Optimized Frontend**: Direct field access instead of computed role checking
### User Experience
- **Smoother Loading**: Proper async handling prevents UI flashing
- **Real-time Updates**: Enhanced streaming generation with live feedback
- **Better Error Handling**: Comprehensive error messages and fallback handling
## Migration Guide
### From llm_resource to llm_knowledge
1. **Automatic Process**: Migration script handles module transition
2. **Data Preservation**: All resources, collections, and embeddings preserved
3. **API Compatibility**: All existing methods continue to work
4. **Dependency Updates**: Module dependencies automatically updated
### From Separate Prompt Module
1. **Seamless Integration**: Prompts now managed within assistants
2. **Enhanced Features**: Auto-detection and testing capabilities added
3. **Template Compatibility**: All existing templates continue to work
4. **Improved UI**: Integrated prompt management in assistant interface
### Tool System Migration
1. **Structured Data**: Tool results now in `body_json` format
2. **Enhanced Execution**: Better error handling and result storage
3. **MCP Compatibility**: Improved Model Context Protocol integration
4. **Provider Support**: Unified tool calling across all providers
## Compatibility Matrix
| Module Version | Odoo Version | Python Version | Dependencies |
| -------------- | ------------ | -------------- | ------------ |
| 16.0.x.x.x | 16.0+ | 3.8+ | mail, web |
## Support & Resources
- **Issues**: [GitHub Issues](https://github.com/apexive/odoo-llm/issues)
- **Documentation**: [GitHub Repository](https://github.com/apexive/odoo-llm)
- **Discussions**: [GitHub Discussions](https://github.com/apexive/odoo-llm/discussions)
---
_For more detailed technical information, see [OVERVIEW.md](OVERVIEW.md) for architecture details._
+358
View File
@@ -0,0 +1,358 @@
# Odoo LLM Integration Modules - Project Context
## Project Overview
This is a comprehensive suite of Odoo modules for integrating Large Language Models (LLMs) with Odoo ERP. The modules provide AI-powered features, knowledge management, and various LLM provider integrations.
## Current Status
- **Current Version**: Odoo 16.0
- **Target Version**: Odoo 18.0
- **Migration Status**: In Progress
- **Main Branch**: 16.0
- **Migration Branch**: 18.0-migration
## Module Architecture
### Core Modules
1. **llm** - Base module providing core LLM functionality, models, and providers
2. **llm_thread** - Thread management for LLM conversations
3. **llm_tool** - Tool management and consent configuration for LLM operations
4. **llm_assistant** - Assistant functionality with prompts, categories, and tags
### Provider Modules
- **llm_anthropic** - Anthropic Claude integration
- **llm_openai** - OpenAI GPT integration
- **llm_mistral** - Mistral AI integration
- **llm_ollama** - Ollama local LLM integration
- **llm_litellm** - LiteLLM proxy integration
- **llm_replicate** - Replicate API integration
- **llm_fal_ai** - Fal.ai integration
### Knowledge Management
- **llm_knowledge** - Core knowledge base with chunking and RAG
- **llm_knowledge_automation** - Automated knowledge collection
- **llm_knowledge_llama** - Llama-specific knowledge features
- **llm_knowledge_mistral** - Mistral-specific knowledge features
- **llm_tool_knowledge** - Tool-knowledge integration
### Vector Storage
- **llm_pgvector** - PostgreSQL vector storage
- **llm_chroma** - Chroma vector database integration
- **llm_qdrant** - Qdrant vector database integration
### Generation & Processing
- **llm_generate** - Content generation features
- **llm_generate_job** - Job queue for generation tasks
- **llm_training** - Training dataset management
- **llm_comfyui** - ComfyUI integration
- **llm_comfy_icu** - ComfyICU integration
### Additional Features
- **llm_document_page** - Document page integration
- **llm_mcp** - Model Context Protocol server
- **llm_store** - LLM marketplace/store functionality
- **web_json_editor** - JSON editor widget
## Migration to Odoo 18.0 - Key Changes
### Critical Breaking Changes
1. **tree → list**: All `<tree>` tags must be renamed to `<list>`
2. **attrs → direct attributes**: Convert domain syntax to Python expressions
3. **states → invisible**: Button states attribute replaced with invisible
4. **name_get() → \_compute_display_name()**: Display name computation changed
5. **message_format() removed**: Use Store system with `_to_store()` method instead
6. **Registry import**: Use `from odoo.modules.registry import Registry` not `from odoo import registry`
### Module-Specific Migration Requirements
#### High Priority (Core + Heavy UI)
- **llm**: Update manifest, migrate views (4 view files)
- **llm_thread**: Migrate tree views in thread views
- **llm_tool**: Migrate consent config and tool views
- **llm_assistant**: Multiple view files with tree tags
- **llm_knowledge**: Complex module with multiple views and wizards
#### Medium Priority (Feature Modules)
- **llm_mcp**: Has attrs attributes that need conversion
- **llm_training**: Dataset and job views need migration
- **llm_generate_job**: Queue and job views
- **llm_pgvector**: Embedding views
- **llm_store**: Store views
- **llm_document_page**: Wizard attrs attributes
- **llm_litellm**: Provider views with attrs
#### Low Priority (Manifest Only)
Provider modules with minimal UI:
- llm_anthropic, llm_openai, llm_mistral, llm_ollama
- llm_replicate, llm_fal_ai, llm_comfy_icu, llm_comfyui
- llm_generate, llm_chroma, llm_qdrant
- llm_knowledge_llama, llm_knowledge_mistral, llm_tool_knowledge
## Testing Strategy
1. Run individual module tests after each migration
2. Test inter-module dependencies
3. Validate all view rendering
4. Check all workflows and actions
5. Verify API compatibility
## Code Quality Standards
- Python 3.11+ compatibility
- Ruff for linting and formatting
- Pre-commit hooks configured
- Type hints where applicable
## Development Commands
### Testing
```bash
# Run all tests
./run_tests.sh
# Test specific module
odoo-bin --test-enable --stop-after-init --test-tags=llm -d test_db -u llm
```
### Code Quality
```bash
# Format and lint
ruff format . && ruff check . --fix --unsafe-fixes
# Pre-commit
pre-commit run --all-files
```
## Migration Progress Tracking
### ✅ Completed (18.0 Compatible)
#### Core Modules - COMPLETED ✅
1. **llm** - Base module providing core LLM functionality, models, and providers
- ✅ Migrated to Odoo 18.0
- ✅ Updated manifests and dependencies
- ✅ Core LLM provider and model management
2. **llm_thread** - Thread management for LLM conversations
- ✅ Migrated to Odoo 18.0 mail system architecture
- ✅ Implemented proper `_init_messaging()` and `_thread_to_store()` methods
- ✅ Fixed message handling (tool messages, empty message filtering, squashing)
- ✅ Fixed HTML escaping issues in streaming messages
- ✅ Updated thread header components with proper fetchData() patterns
- ✅ Integrated with standard mail.store service patterns
3. **llm_tool** - Tool management and consent configuration for LLM operations
- ✅ Migrated to Odoo 18.0
- ✅ Updated manifests and tool configuration views
- ✅ Tool consent and management functionality
4. **llm_assistant** - Assistant functionality with prompts and tools
- ✅ Migrated assistant dropdown UI with full functionality
- ✅ Implemented assistant selection and clearing
- ✅ Fixed UI reactivity issues with proper context binding
- ✅ Extended `_thread_to_store()` to handle assistant_id states
- ✅ Clean separation from llm_thread module following DRY principles
#### Text/Chat Provider Modules - COMPLETED ✅
1. **llm_openai** - OpenAI GPT integration
- ✅ Migrated to Odoo 18.0
- ✅ Updated manifests and dependencies
2. **llm_anthropic** - Anthropic Claude integration
- ✅ Migrated to Odoo 18.0
- ✅ Updated manifests and dependencies
3. **llm_mistral** - Mistral AI integration
- ✅ Migrated to Odoo 18.0
- ✅ Updated manifests and dependencies
4. **llm_ollama** - Ollama local LLM integration
- ✅ Migrated to Odoo 18.0
- ✅ Updated manifests and dependencies
5. **llm_litellm** - LiteLLM proxy integration
- ✅ Migrated to Odoo 18.0
- ✅ Updated manifests and dependencies
### 🚧 In Progress
#### UI/UX Improvements
- 🔄 Make LLM components responsive/mobile friendly
- 🔄 Fix auto scrolling for new messages in thread
- 🔄 Investigate `_to_store` pattern in mail module for future use
### ⏳ Remaining Migration Tasks
#### High Priority (Image Generation Providers)
- **llm_replicate** - Replicate API integration (image generation)
- **llm_fal_ai** - Fal.ai integration (image generation)
- **llm_comfyui** - ComfyUI integration (image workflows)
- **llm_comfy_icu** - ComfyICU integration
#### Medium Priority (Knowledge & Advanced Features)
- **llm_knowledge** - Knowledge base with chunking and RAG
- **llm_knowledge_automation** - Automated knowledge collection
- **llm_mcp** - Model Context Protocol server
- **llm_generate** - Content generation features
- **llm_generate_job** - Job queue for generation tasks
- **llm_training** - Training dataset management
#### Low Priority (Vector Storage & Extensions)
- **llm_pgvector**, **llm_chroma**, **llm_qdrant** - Vector database integrations
- **llm_document_page** - Document page integration
- **llm_store** - LLM marketplace functionality
- **web_json_editor** - JSON editor widget
## Future Architecture Improvements
### \_to_store Pattern Implementation
**Priority**: Medium
**Investigation needed**: Study how Odoo's mail module implements `_to_store()` methods for different models.
**Potential Implementation**:
- **llm.provider** - Standardize provider data serialization for frontend
- **llm.model** - Consistent model data structure in mail.store
- **llm.tool** - Tool data formatting for UI components
- **llm.assistant** - Enhanced assistant data structure (already partially implemented)
**Benefits**:
- Consistent data format across all LLM models
- Better integration with Odoo 18.0 mail.store patterns
- Simplified frontend data access and reactivity
- Reduced custom serialization logic
**Research Tasks**:
1. Analyze `mail.thread._to_store()` and related methods
2. Study how different mail models extend the pattern
3. Design unified approach for LLM model serialization
4. Create base mixin for LLM models to inherit
## Known Issues
- Some modules may have additional hidden dependencies
- Vector storage modules might need special attention for data migration
- Job queue modules need careful testing for async operations
## Odoo 18.0 Mail System Architecture (IMPORTANT)
### Mail Store System
- **USE** `mail.store` service for all message/thread operations
- **REUSE** existing mail components, don't create separate messaging models
- **PATCH** components conditionally using `@web/core/utils/patch`
- The new system uses Record-based reactive architecture
### Thread and Message Management
```javascript
// Correct Thread.get() format in Odoo 18.0
mailStore.Thread.get({ model: "llm.thread", id: threadId });
// Message insertion pattern
mailStore.insert({ "mail.message": [messageData] }, { html: true });
// IMPORTANT: Also add to thread.messages collection for UI updates
if (!thread.messages.some((m) => m.id === message.id)) {
thread.messages.push(message);
}
```
### Message Serialization
```python
# Use Store system for message formatting
from odoo.addons.mail.tools.discuss import Store
def to_store_format(self, message):
store = Store()
message._to_store(store)
result = store.get_result()
return result['mail.message'][0]
```
### LLM-Specific Implementation
#### Service Setup
```javascript
export const llmStoreService = {
dependencies: ["orm", "bus_service", "mail.store", "notification"],
start(env, { orm, bus_service, "mail.store": mailStore, notification }) {
// mailStore is the standard Odoo mail.store service
},
};
```
#### Safe Component Patching
```javascript
patch(Composer.prototype, {
setup() {
super.setup();
try {
this.llmStore = useService("llm.store");
} catch (error) {
this.llmStore = null; // Graceful fallback
}
},
});
```
#### Message Processing Rules
- **User messages**: Plain text, no processing through `_process_llm_body()`
- **Assistant messages**: Process through `_process_llm_body()` for markdown→HTML
- **Tool messages**: Use `body_json` field, no HTML processing
#### Streaming Architecture
1. User message → `message_post()` → standard bus events
2. AI response → EventSource streaming → custom handling in llm.store
3. Messages inserted via `mailStore.insert()`
4. Manually add to `thread.messages` collection for reactivity
### Message History Flow for LLM
1. User message posted with `llm_role="user"` → saved to DB
2. `generate_messages()` called → `get_llm_messages()` retrieves all messages
3. Full history including new user message passed to LLM
### Common Pitfalls to Avoid
- Don't use `message_format()` - it's removed in 18.0
- Don't use `existingMessage.update()` for streaming - use `mailStore.insert()`
- Don't forget to add messages to `thread.messages` collection
- Don't process user messages as markdown/HTML
- Don't use wrong Thread.get() format (array instead of object)
## References
- [MIGRATION_16_TO_18.md](./MIGRATION_16_TO_18.md) - Detailed migration guide
- [LLM_THREAD_18_MIGRATION_GUIDE.md](./LLM_THREAD_18_MIGRATION_GUIDE.md) - LLM thread specific migration
- Odoo 18.0 official documentation
- Module interdependency graph (to be created)
+501
View File
@@ -0,0 +1,501 @@
# Architecture Overview - Odoo LLM Integration
This document provides a comprehensive technical overview of the Odoo LLM Integration architecture, covering the consolidated module structure, core concepts, and development patterns.
## Architecture Overview
The Odoo LLM Integration provides a modular framework for integrating Large Language Models into Odoo. The architecture follows a consolidated design with clear separation of concerns, enabling seamless interaction with various AI providers while building sophisticated AI-powered applications.
## Core Module Architecture
The system is built around five core modules that form the foundation for all LLM operations:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ llm (Base) │ │ llm_assistant │ │ llm_generate │
│ Foundation │◄───┤ Intelligence │◄───┤ Generation │
│ │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
▲ ▲ ▲
│ │ │
┌─────────────────┐ ┌─────────────────┐ │
│ llm_tool │ │ llm_store │ │
│ Actions │ │ Storage │ │
│ │ │ │ │
└─────────────────┘ └─────────────────┘ │
▲ ▲ │
│ │ │
└───────────────────────┴───────────────────────┘
```
### Core Module Responsibilities
1. **`llm`** - Foundation infrastructure and provider abstraction
2. **`llm_assistant`** - Intelligence layer with prompt management
3. **`llm_generate`** - Unified content generation API
4. **`llm_tool`** - Function calling and Odoo integration
5. **`llm_store`** - Vector storage and similarity search
## Enhanced Mail Message System
### Message Extensions
The base `llm` module extends Odoo's core messaging system with AI-specific capabilities:
```python
# Extended mail.message fields
llm_role = fields.Selection([
('user', 'User'),
('assistant', 'Assistant'),
('tool', 'Tool'),
('system', 'System')
], compute='_compute_llm_role', store=True, index=True)
body_json = fields.Json() # Structured data for tool messages
```
### Message Subtypes
Integrated message subtypes for AI interactions:
- **`llm.mt_user`**: User messages in AI conversations
- **`llm.mt_assistant`**: AI-generated responses
- **`llm.mt_tool`**: Tool execution results and data
- **`llm.mt_system`**: System prompts and configuration messages
### Performance Optimization
The **`llm_role` field** provides **10x faster** message queries by:
- Eliminating expensive subtype lookups with indexed field access
- Enabling efficient conversation history processing
- Simplifying frontend filtering and display logic
- Optimizing database operations for large datasets
## Core Models & Concepts
### 1. LLM.Thread (`llm_thread/models/llm_thread.py`)
**Purpose**: Central conversation management and data bridge
```python
class LLMThread(models.Model):
_name = "llm.thread"
_description = "LLM Chat Thread"
_inherit = ["mail.thread"]
```
**Core Function**: Serves as the primary link between Odoo business data and AI conversations:
- **Storage Backend**: Persistent storage for all LLM conversations
- **Data Bridge**: Links any Odoo record to AI conversation threads
- **Conversation Context**: Maintains state, configuration, and history
- **Multi-Threading**: Multiple concurrent conversations per business record
**Key Relationships**:
```
Odoo Record (sale.order, project.task, etc.)
↓ (1:many relationship)
LLM Thread(s) - Multiple AI conversations per record
↓ (inherits from mail.thread)
Mail Messages - Conversation history with AI enhancements
```
**PostgreSQL Advisory Locking**:
- Prevents concurrent generation conflicts
- Ensures message consistency during streaming
- Database-level coordination for multi-user scenarios
### 2. LLM.Assistant (`llm_assistant/models/llm_assistant.py`)
**Purpose**: Intelligent configuration orchestrator
```python
class LLMAssistant(models.Model):
_name = "llm.assistant"
_description = "LLM Assistant"
_inherit = ["mail.thread"]
```
**Core Function**: Provides the intelligence layer that configures HOW to connect Odoo data to AI models:
**Configuration Domains**:
- **Model Selection**: AI provider and model specification
- **Instruction Templates**: System prompts and conversation templates
- **Data Mapping**: Odoo record transformation to LLM inputs
- **Tool Orchestration**: Available tools and usage patterns
- **Context Management**: History trimming and optimization
- **Generation Parameters**: Content generation configuration
**Consolidated Prompt Management**:
The assistant module now includes integrated prompt template functionality (previously `llm_prompt`):
```python
# Integrated prompt fields
prompt_id = fields.Many2one('llm.prompt')
default_values = fields.Json() # Default template arguments
```
**Assistant Types**:
1. **Chat Assistants**: Conversational AI with specific personas
2. **Generation Assistants**: Content creation workflows
3. **Analysis Assistants**: Data analysis and insights
4. **Training Assistants**: Model fine-tuning workflows
### 3. LLM.Provider (`llm/models/llm_provider.py`)
**Purpose**: AI service provider abstraction
```python
class LLMProvider(models.Model):
_name = "llm.provider"
_inherit = ["mail.thread"]
_description = "LLM Provider"
```
**Provider Integration Pattern**:
```python
def _dispatch(self, method, *args, **kwargs):
"""Dynamic method dispatch to service implementations"""
service_method = f"{self.service}_{method}"
return getattr(self, service_method)(*args, **kwargs)
```
**Supported Providers**:
- **OpenAI** (`llm_openai`): GPT models, DALL-E, embeddings
- **Anthropic** (`llm_anthropic`): Claude models with tool calling
- **Ollama** (`llm_ollama`): Local model deployment
- **Mistral** (`llm_mistral`): Mistral AI models
- **LiteLLM** (`llm_litellm`): Multi-provider proxy
- **Replicate** (`llm_replicate`): Model marketplace access
- **FAL.ai** (`llm_fal_ai`): Fast inference API
### 4. LLM.Tool (`llm_tool/models/llm_tool.py`)
**Purpose**: Function calling and Odoo integration
```python
class LLMTool(models.Model):
_name = "llm.tool"
_description = "LLM Tool"
_inherit = ["mail.thread"]
```
**Enhanced Tool System**:
- **Structured Data Storage**: Tool results in `body_json` format
- **MCP Integration**: Model Context Protocol compatibility
- **Security Framework**: User consent and permission system
- **Error Handling**: Comprehensive error propagation and logging
**Tool Message Format**:
```python
# New structured format
thread.message_post(
body_json={
"tool_call_id": "call_123",
"function": "search_records",
"arguments": {"model": "sale.order", "domain": [...]},
"result": {"records": [...], "count": 5}
},
llm_role="tool"
)
```
### 5. LLM.Store (`llm_store/models/llm_store.py`)
**Purpose**: Vector storage abstraction
```python
class LLMStore(models.Model):
_name = "llm.store"
_inherit = ["mail.thread"]
_description = "LLM Vector Store"
```
**Vector Store Implementations**:
- **ChromaDB** (`llm_chroma`): HTTP client integration
- **pgvector** (`llm_pgvector`): PostgreSQL extension
- **Qdrant** (`llm_qdrant`): Qdrant vector database
**RAG Integration**:
```python
# Vector similarity search
results = store.search_vectors(
collection='knowledge_base',
query_vector=embedding,
limit=5,
filter_metadata={"domain": "sales"}
)
```
## Unified Generation System
### Generation API
The `llm_generate` module provides a unified interface for all content generation:
```python
# Unified generation method
response = thread.generate_response(
user_input="Create a sales report",
generation_type="text", # or "image", "audio", etc.
use_queue=True # Optional background processing
)
```
### Dynamic Form Generation
Automatic form generation based on model schemas:
```python
def get_input_schema(self):
"""Generate form schema for model inputs"""
# Priority order:
# 1. Assistant's prompt schema
# 2. Thread's direct prompt schema
# 3. Model's default schema
```
### Race Condition Fixes
Comprehensive fixes for async loading issues:
- **Loading State Management**: Proper async handling in UI components
- **Schema Synchronization**: Automatic prompt argument detection
- **Form Stability**: Prevention of empty forms during loading
- **Context Handling**: Improved state management during updates
## Knowledge Management System
### Consolidated llm_knowledge Module
The `llm_knowledge` module consolidates functionality from the former `llm_resource` module:
**Consolidated Features**:
- **Resource Management**: Document processing and storage (`llm.resource`)
- **RAG Capabilities**: Retrieval-Augmented Generation
- **Vector Integration**: Embedding and similarity search
- **Processing Pipeline**: Retrieve → Parse → Chunk → Embed → Index
**Processing States**:
```
draft → retrieved → parsed → chunked → ready
```
**API Compatibility**: All existing methods preserved:
```python
# Complete processing pipeline
resource.process_resource() # Full pipeline
resource.retrieve() # Get content from source
resource.parse() # Convert to markdown
resource.chunk() # Split into segments
resource.embed() # Generate embeddings
```
## Message System Architecture
### Enhanced Message Creation
```python
# Role-based message posting
def message_post(self, body=None, llm_role=None, body_json=None, **kwargs):
"""Enhanced message posting with AI-specific fields"""
# Automatic role computation from subtype
if not llm_role and message_type:
llm_role = self._compute_role_from_subtype(message_type)
# Structured data handling
if body_json:
kwargs['body_json'] = body_json
return super().message_post(body=body, **kwargs)
```
### Streaming Message Updates
Real-time message creation during AI generation:
```python
def message_post_from_stream(self, stream, llm_role, **kwargs):
"""Create and update message from streaming response"""
# Create placeholder message
message = self.message_post(body="", llm_role=llm_role, **kwargs)
# Update content as stream progresses
for chunk in stream:
message.body += chunk
# Real-time UI updates via bus notifications
```
## Provider Integration Patterns
### Service Registration
```python
@api.model
def _get_available_services(self):
return super()._get_available_services() + [
('openai', 'OpenAI'),
('anthropic', 'Anthropic'),
('ollama', 'Ollama'),
# Additional providers...
]
```
### Method Implementation Pattern
```python
def openai_chat(self, messages, model=None, stream=False, **kwargs):
"""OpenAI-specific chat implementation"""
def anthropic_chat(self, messages, model=None, stream=False, **kwargs):
"""Anthropic-specific chat implementation"""
```
## Model Context Protocol (MCP) Integration
### MCP Server Configuration
```python
class LLMMCPServer(models.Model):
_name = "llm.mcp.server"
_description = "MCP Server Configuration"
name = fields.Char(required=True)
command = fields.Char(required=True) # Server start command
args = fields.Text() # Command arguments
env_vars = fields.Json() # Environment variables
```
### Tool Exposure via MCP
```python
def get_tool_definition(self):
"""Returns MCP-compatible tool definition"""
return {
"name": self.name,
"description": self.description,
"inputSchema": json.loads(self.input_schema or '{}')
}
```
## Frontend Architecture
### JavaScript Component Structure
**Core Components**:
- **LLMChatContainer**: Main chat interface controller
- **LLMChatComposer**: Message input and generation forms
- **LLMChatThreadHeader**: Provider/model/assistant selection
- **LLMMediaForm**: Dynamic form generation with schema handling
### Real-time Features
- **Streaming Generation**: Live message updates during AI response
- **Tool Execution**: Visual feedback for function calls
- **Context Switching**: Dynamic provider/model/assistant changes
- **Form Generation**: Automatic UI generation from model schemas
## Security & Access Control
### Role-Based Security
**User Groups**:
- **LLM User** (`llm.group_llm_user`): Basic access to AI features
- **LLM Manager** (`llm.group_llm_manager`): Full administrative access
### Tool Security Framework
```python
# Tool consent system
requires_user_consent = fields.Boolean(default=False)
destructive_hint = fields.Boolean(default=False)
read_only_hint = fields.Boolean(default=True)
```
### Record Rules
- Company-based access control for providers
- User-specific thread access restrictions
- Administrative override capabilities
## Performance Optimizations
### Database Improvements
1. **Indexed Role Field**: 10x faster message queries
2. **Reduced Subtype Lookups**: Direct field access instead of joins
3. **PostgreSQL Locking**: Concurrent operation safety
4. **Optimized Vector Queries**: Efficient similarity search
### Frontend Optimizations
1. **Loading State Management**: Prevents UI flashing during async operations
2. **Schema Caching**: Reduced API calls for form generation
3. **Streaming Updates**: Real-time UI updates without full refreshes
4. **Component Reuse**: Efficient component lifecycle management
## Development Patterns
### Adding New Providers
1. **Create Provider Module**: Inherit from `llm.provider`
2. **Implement Service Methods**: Follow naming convention `{service}_{method}`
3. **Register Service**: Add to `_get_available_services()`
4. **Add Configuration**: Provider-specific settings and UI
### Creating Custom Tools
1. **Inherit Tool Model**: Extend `llm.tool`
2. **Implement Execute Method**: `{implementation}_execute()`
3. **Define Schema**: Method signature or JSON schema
4. **Register Implementation**: Add to available implementations
### Extending Message Handling
1. **Override Message Post**: Custom `message_post()` in thread model
2. **Handle Custom Roles**: Process custom `llm_role` values
3. **Process Structured Data**: Handle `body_json` content
4. **Implement Email Patterns**: Custom `email_from` logic
## Migration & Compatibility
### Module Consolidations
**Completed Consolidations**:
- `llm_resource``llm_knowledge` (resource management + RAG)
- `llm_prompt``llm_assistant` (prompt templates + assistants)
- `llm_mail_message_subtypes``llm` (message subtypes)
**Migration Scripts**: Automatic migration preserves all data:
- Message subtype conversion
- Tool data format migration
- Module dependency updates
- Configuration preservation
### Backward Compatibility
- **API Compatibility**: All existing methods continue to work
- **Data Preservation**: Zero data loss during consolidations
- **Configuration Migration**: Settings automatically transferred
- **Progressive Enhancement**: New features don't break existing workflows
This architecture provides a robust, scalable foundation for building sophisticated AI-powered applications within Odoo, with clear patterns for extension and a strong emphasis on performance, security, and maintainability.
+265
View File
@@ -0,0 +1,265 @@
# Odoo LLM Integration
![Banner](llm/static/description/banner.jpeg)
This repository provides a comprehensive framework for integrating Large Language Models (LLMs) into Odoo. It allows seamless interaction with various AI providers including OpenAI, Anthropic, Ollama, and Replicate, enabling chat completions, text embeddings, and more within your Odoo environment.
## 🚀 Latest Updates (Version 16.0-pr)
### **Major Architecture Improvements**
- **Consolidated Architecture**: Merged `llm_resource` into `llm_knowledge` and `llm_prompt` into `llm_assistant` for streamlined management
- **Performance Optimization**: Added indexed `llm_role` field for 10x faster message queries and improved database performance
- **Unified Generation API**: New `generate()` method provides consistent content generation across all model types (text, images, etc.)
- **Enhanced Tool System**: Simplified tool execution with structured `body_json` storage and better error handling
- **PostgreSQL Advisory Locking**: Prevents concurrent generation issues with proper database-level locks
### **Developer Experience Enhancements**
- **Cleaner APIs**: Simplified method signatures with `llm_role` parameter instead of complex subtype handling
- **Better Debugging**: Enhanced logging, error messages, and comprehensive test coverage throughout the system
- **Reduced Dependencies**: Eliminated separate modules by consolidating related functionality
## 🚀 Features
- **Multiple LLM Provider Support**: Connect to OpenAI, Anthropic, Ollama, Mistral, Replicate, LiteLLM, and FAL.ai.
- **Unified API**: Consistent interface for all LLM operations regardless of the provider.
- **Modern Chat UI**: Responsive interface with real-time streaming, tool execution display, and assistant switching.
- **Thread Management**: Organize and manage AI conversations with context and related record linking.
- **Model Management**: Configure and utilize different models for chat, embeddings, and content generation.
- **Knowledge Base (RAG)**: Store, index, and retrieve documents for Retrieval-Augmented Generation.
- **Vector Store Integrations**: Supports ChromaDB, pgvector, and Qdrant for efficient similarity searches.
- **Advanced Tool Framework**: Allows LLMs to interact with Odoo data, execute actions, and use custom tools.
- **AI Assistants with Prompts**: Build specialized AI assistants with custom instructions, prompt templates, and tool access.
- **Content Generation**: Generate images, text, and other content types using specialized models.
- **Security**: Role-based access control, secure API key management, and permission-based tool access.
## 📦 Core Modules
The architecture centers around five core modules that provide the foundation for all LLM operations:
| Module | Version | Purpose |
|--------|---------|---------|
| **`llm`** | 16.0.1.3.0 | **Foundation** - Base infrastructure, providers, models, and enhanced messaging system |
| **`llm_assistant`** | 16.0.1.4.0 | **Intelligence** - AI assistants with integrated prompt templates and testing |
| **`llm_generate`** | 16.0.2.0.0 | **Generation** - Unified content generation API for text, images, and more |
| **`llm_tool`** | 16.0.3.0.0 | **Actions** - Tool framework for LLM-Odoo interactions and function calling |
| **`llm_store`** | 16.0.1.0.0 | **Storage** - Vector store abstraction for embeddings and similarity search |
## 📦 All Available Modules
| Module | Version | Description |
|--------|---------|-------------|
| **Core Infrastructure** | | |
| `llm` | 16.0.1.3.0 | Base module with providers, models, and enhanced messaging |
| `llm_assistant` | 16.0.1.4.0 | AI assistants with integrated prompt templates |
| `llm_generate` | 16.0.2.0.0 | Unified content generation with dynamic forms |
| `llm_tool` | 16.0.3.0.0 | Enhanced tool framework with structured data storage |
| `llm_store` | 16.0.1.0.0 | Vector store abstraction layer |
| **Chat & Threading** | | |
| `llm_thread` | 16.0.1.3.0 | Chat threads with PostgreSQL locking and optimized performance |
| **AI Providers** | | |
| `llm_openai` | 16.0.1.1.3 | OpenAI (GPT) provider integration with enhanced tool support |
| `llm_anthropic` | 16.0.1.1.0 | Anthropic (Claude) provider integration |
| `llm_ollama` | 16.0.1.1.0 | Ollama provider for local model deployment |
| `llm_mistral` | 16.0.1.0.0 | Mistral AI provider integration |
| `llm_litellm` | 16.0.1.1.0 | LiteLLM proxy for centralized model management |
| `llm_replicate` | 16.0.1.1.0 | Replicate.com provider integration |
| `llm_fal_ai` | 16.0.2.0.0 | FAL.ai provider with unified generate endpoint |
| **Knowledge & RAG** | | |
| `llm_knowledge` | 16.0.1.1.0 | **Consolidated** - RAG functionality with document management |
| `llm_knowledge_automation` | 16.0.1.0.0 | Automation rules for knowledge processing |
| `llm_tool_knowledge` | 16.0.1.0.0 | Tool for LLMs to query the knowledge base |
| **Vector Stores** | | |
| `llm_chroma` | 16.0.1.0.0 | ChromaDB vector store integration |
| `llm_pgvector` | 16.0.1.0.0 | pgvector (PostgreSQL) vector store integration |
| `llm_qdrant` | 16.0.1.0.0 | Qdrant vector store integration |
| **Specialized Features** | | |
| `llm_mcp` | 16.0.1.0.0 | Model Context Protocol support |
| `llm_training` | 16.0.1.0.0 | Fine-tuning and model training capabilities |
| `llm_generate_job` | 16.0.1.0.0 | Job queue management for content generation |
| `llm_document_page` | 16.0.1.0.0 | Integration with document pages and knowledge articles |
## 🛠️ Installation
Install these modules by cloning the repository and making them available in your Odoo addons path:
1. **Clone the repository:**
```bash
git clone https://github.com/apexive/odoo-llm
```
2. **Install dependencies:**
```bash
pip install -r requirements.txt
```
3. **Make modules available to Odoo:**
```bash
# Option A: Clone directly into addons directory
cd /path/to/your/odoo/addons/
git clone https://github.com/apexive/odoo-llm
# Option B: Copy modules to extra-addons
cp -r /path/to/odoo-llm/* /path/to/your/odoo/extra-addons/
```
4. **Restart Odoo and install modules** through the Apps menu
## 🚀 Quick Start Guide
Thanks to Odoo's dependency management, you only need to install the end modules to get started:
### 1. **Complete AI Assistant Setup** (Recommended)
```
Install: llm_assistant + llm_openai (or your preferred provider)
```
**What you get:**
- ✅ Full chat interface with AI assistants
- ✅ Prompt template management and testing
- ✅ Tool framework for Odoo interactions
- ✅ Content generation capabilities
- ✅ Optimized message handling (10x faster)
### 2. **Knowledge Base (RAG) Setup**
```
Install: llm_knowledge + llm_pgvector (or llm_chroma/llm_qdrant)
```
**What you get:**
- ✅ Document embedding and retrieval
- ✅ Vector similarity search
- ✅ RAG-enhanced conversations
- ✅ Automated knowledge processing
### 3. **Advanced Content Generation**
```
Install: llm_generate + llm_fal_ai (for images)
```
**What you get:**
- ✅ Image generation from text prompts
- ✅ Dynamic form generation based on schemas
- ✅ Streaming generation responses
- ✅ Multi-format content support
### 4. **Local AI Deployment**
```
Install: llm_ollama + llm_assistant
```
**What you get:**
- ✅ Privacy-focused local AI models
- ✅ No external API dependencies
- ✅ Full feature compatibility
- ✅ Custom model support
## ⚙️ Configuration
After installation:
1. **Set up AI Provider:**
- Navigate to **LLM → Configuration → Providers**
- Create a new provider with your API credentials
- Use "Fetch Models" to automatically import available models
2. **Create AI Assistants:**
- Go to **LLM → Configuration → Assistants**
- Configure assistants with specific roles and instructions
- Assign prompt templates and available tools
3. **Configure Access Rights:**
- Grant appropriate permissions to users
- Set up tool consent requirements
- Configure security policies
4. **Set up Knowledge Base (optional):**
- Configure vector store connections
- Create knowledge collections
- Import and process documents
## 🔄 LLM Tools: Building AI-Driven ERP
This integration enables revolutionary AI-powered business processes:
### **Why This Matters**
- **AI-driven automation** of repetitive tasks with sophisticated tool execution
- **Smart querying & decision-making** with direct access to Odoo data
- **Flexible ecosystem** for custom AI assistants with role-specific configurations
- **Real-time streaming** interactions with enterprise-grade reliability
### **Recent Performance Improvements**
- **10x Performance Boost**: New `llm_role` field eliminates expensive database lookups
- **Simplified Architecture**: Module consolidation reduces complexity and maintenance
- **Enhanced Tool System**: Better error handling and structured data storage
- **PostgreSQL Locking**: Prevents race conditions in concurrent scenarios
- **Unified Generation API**: Consistent interface across all content types
### **Enterprise-Ready Features**
- **PostgreSQL Advisory Locking**: Prevents concurrent generation conflicts
- **Role-Based Security**: Granular access control for AI features
- **Tool Consent System**: User approval for sensitive operations
- **Audit Trail**: Complete tracking of AI interactions and tool usage
- **Migration Support**: Automatic upgrades preserve existing data
## 🤝 Contributing
We're committed to building an open AI layer for Odoo that benefits everyone. Areas where we welcome contributions:
- **Testing & CI/CD**: Unit tests for the consolidated architecture
- **Security Enhancements**: Access control and audit improvements
- **Provider Integrations**: Support for additional AI services
- **Localization**: Translations and regional customizations
- **Documentation**: Examples, tutorials, and use case guides
- **Performance**: Optimization and scalability improvements
### **How to Contribute**
1. **Issues**: Report bugs or suggest features via GitHub Issues
2. **Discussions**: Join conversations about priorities and approaches
3. **Pull Requests**: Submit code contributions following our guidelines
### **Development Guidelines**
- Follow existing code style and structure
- Write comprehensive tests for new functionality
- Update documentation for changes
- Test with the consolidated architecture
- Include migration scripts for breaking changes
## 🔮 Roadmap
- [x] **Enhanced RAG** capabilities ✅ *Production ready*
- [x] **Function calling support***Advanced tool framework*
- [x] **Prompt template management***Integrated in assistants*
- [x] **Performance optimization***10x improvement achieved*
- [x] **Content generation***Unified API implemented*
- [x] **Module consolidation***Architecture simplified*
- [ ] **Multi-modal content** 🚧 *Image + text generation*
- [ ] **Advanced workflow automation** 🔄 *Business process AI*
- [ ] **Integration with CRM/HR/Manufacturing** 🔄 *Domain-specific assistants*
- [ ] **Model fine-tuning workflows** 🔄 *Custom model training*
## 📈 Performance & Migration
The latest version includes significant architectural improvements:
- **Backward Compatible**: All existing installations automatically migrate
- **Performance Gains**: Up to 10x faster message queries with optimized database schema
- **Reduced Complexity**: Consolidated modules eliminate maintenance overhead
- **Enhanced Reliability**: PostgreSQL advisory locking prevents concurrent issues
- **Data Preservation**: Zero data loss during module consolidations
For detailed migration information, see [CHANGELOG.md](CHANGELOG.md).
## 📜 License
This project is licensed under LGPL-3 - see the [LICENSE](LICENSE) file for details.
## 🌐 About
Developed by [Apexive](https://apexive.com) - We're passionate about bringing advanced AI capabilities to the Odoo ecosystem.
**Support & Resources:**
- **Documentation**: [GitHub Repository](https://github.com/apexive/odoo-llm)
- **Community Support**: [GitHub Discussions](https://github.com/apexive/odoo-llm/discussions)
- **Bug Reports**: [GitHub Issues](https://github.com/apexive/odoo-llm/issues)
- **Architecture Details**: [OVERVIEW.md](OVERVIEW.md)
- **Change History**: [CHANGELOG.md](CHANGELOG.md)
---
*For questions, support, or collaboration opportunities, please open an issue or discussion in this repository.*
+1
View File
@@ -0,0 +1 @@
# Changelog
+443
View File
@@ -0,0 +1,443 @@
# LLM Integration Base for Odoo
The foundational module for integrating Large Language Models into Odoo. This base module provides the core infrastructure, provider abstraction, and enhanced messaging system that enables all other LLM modules in the ecosystem.
**Module Type:** 📦 Infrastructure (Core Foundation)
## Architecture
```mermaid
graph TD
subgraph External AI Clients
CD[Claude Desktop<br>Cursor · Windsurf]
CC[Claude Code<br>Codex CLI]
end
subgraph Odoo AI Chat
LA[llm_assistant]
LT[llm_thread]
end
CD -->|MCP Protocol| MCP
CC -->|MCP Protocol| MCP
LA --> LLM
LT --> LLM
MCP[llm_mcp_server<br>MCP Server for Odoo] --> LLM
LLM[⭐ llm — This Module ⭐<br>Provider Abstraction · Model Management<br>Enhanced mail.message · Security Framework]
LLM --> TOOL[llm_tool<br>Tool Framework + Generic CRUD Tools]
LLM --> PROV[AI Providers<br>llm_openai · llm_ollama<br>llm_mistral · ...]
LLM --> INFRA[Infrastructure<br>llm_store · llm_generate]
TOOL --> PACKS[Domain-Specific Tool Packs<br>llm_tool_account · 18 accounting tools<br>llm_tool_mis_builder · 44 MIS reporting tools<br>llm_tool_knowledge · RAG search tools<br>llm_tool_ocr_mistral · OCR via Mistral vision]
style LLM fill:#f9f8fc,stroke:#71639e,stroke-width:3px,color:#71639e
style MCP fill:#fff,stroke:#71639e,stroke-width:2px,color:#71639e
style TOOL fill:#fff,stroke:#71639e,stroke-width:2px,color:#71639e
style PACKS fill:#f9f8fc,stroke:#71639e,stroke-width:2px,color:#71639e
style PROV fill:#fff,stroke:#dee2e6,stroke-width:2px
style INFRA fill:#fff,stroke:#dee2e6,stroke-width:2px
```
## Installation
### What to Install
This module is **auto-installed** as a dependency. You typically don't install it directly. **Choose a setup below based on your use case.**
### Common Setups
| I want to... | Install |
| -------------------------------------------- | ------------------------------------------------------- |
| **Use Claude/Cursor/Codex with Odoo (MCP)** | `llm_mcp_server` (+ tool packs below) |
| Chat with AI inside Odoo | `llm_assistant` + `llm_openai` |
| Use local AI (privacy) | `llm_assistant` + `llm_ollama` |
| Add RAG/knowledge base | Above + `llm_knowledge` + `llm_pgvector` |
| AI-powered accounting via MCP or chat | `llm_tool_account` (+ `llm_mcp_server` for external AI) |
| AI-powered financial reporting (MIS Builder) | `llm_tool_mis_builder` (+ `llm_mcp_server`) |
### MCP Server — Connect External AI Clients
The **`llm_mcp_server`** module exposes all your Odoo tools to external AI clients via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/). This lets you use **Claude Desktop**, **Claude Code**, **Cursor**, **Windsurf**, **VS Code**, and **Codex CLI** to interact directly with your Odoo data.
```bash
odoo-bin -d your_db -i llm_mcp_server
```
After installing, each user generates their own API key from **My Profile → Account Security → New MCP Key**. The wizard provides ready-to-paste configurations for each client. See the [llm_mcp_server README](../llm_mcp_server/README.md) for full setup instructions.
### Domain-Specific Tool Packs
Install tool packs to give AI assistants (both in-Odoo and external MCP clients) specialized capabilities:
| Module | Tools | Description |
| -------------------------- | ----- | -------------------------------------------------------------------------- |
| **`llm_tool_account`** | 18 | Trial balance, journal entries, reconciliation, payments, tax reports, P&L |
| **`llm_tool_mis_builder`** | 44 | KPI management, report computation, drill-down, variance analysis, trends |
| **`llm_tool_knowledge`** | — | RAG search, semantic retrieval, source citations from your knowledge base |
| **`llm_tool_ocr_mistral`** | 1 | Extract text from images and PDFs using Mistral AI vision models |
| **`llm_tool_demo`** | 6 | Example tools showing `@llm_tool` decorator patterns for developers |
The base `llm_tool` module also includes 6 **generic CRUD tools** out of the box: `odoo_record_retriever`, `odoo_record_creator`, `odoo_record_updater`, `odoo_record_unlinker`, `odoo_model_method_executor`, and `odoo_model_inspector` — enabling AI to read, create, update, and delete records in any Odoo model.
### This Module Provides
- Provider abstraction framework
- Model and publisher management
- Enhanced `mail.message` with `llm_role` field
- Security groups and access control
- Base configuration menus
### Modules That Depend on This
| Category | Modules |
| ------------------ | ----------------------------------------------------------------------------------------------------- |
| **MCP Server** | `llm_mcp_server` |
| **Interfaces** | `llm_assistant`, `llm_thread` |
| **Tool Framework** | `llm_tool``llm_tool_account`, `llm_tool_mis_builder`, `llm_tool_knowledge`, `llm_tool_ocr_mistral` |
| **Providers** | `llm_openai`, `llm_ollama`, `llm_mistral`, `llm_replicate`, `llm_fal_ai` |
| **Infrastructure** | `llm_store`, `llm_generate` |
## Overview
The LLM Integration Base serves as the foundation for building AI-powered features across Odoo applications. It extends Odoo's core messaging system with AI-specific capabilities and provides a unified framework for connecting with various AI providers.
### Core Capabilities
- **Enhanced Messaging System** - AI-optimized message handling with 10x performance improvement
- **Provider Abstraction** - Unified interface for multiple AI services (OpenAI, Anthropic, Ollama, etc.)
- **Model Management** - Centralized catalog of AI models with capabilities and metadata
- **Publisher Tracking** - Management of AI model publishers and organizations
- **Security Framework** - Role-based access control and API key management
## Key Features
### Enhanced Mail Message System
The module extends Odoo's `mail.message` model with LLM-specific fields:
```python
# Performance-optimized role field (10x faster queries)
llm_role = fields.Selection([
('user', 'User'),
('assistant', 'Assistant'),
('tool', 'Tool'),
('system', 'System')
], compute='_compute_llm_role', store=True, index=True)
# Structured data for tool messages
body_json = fields.Json()
```
### AI Message Subtypes
Integrated message subtypes for AI interactions:
- **`llm.mt_user`**: User messages in AI conversations
- **`llm.mt_assistant`**: AI-generated responses
- **`llm.mt_tool`**: Tool execution results and data
- **`llm.mt_system`**: System prompts and configuration messages
### Provider Framework
Unified provider abstraction supporting multiple AI services:
```python
# Dynamic method dispatch to service implementations
provider._dispatch('chat', messages=messages, model=model)
provider._dispatch('embedding', text=text)
provider._dispatch('generate', prompt=prompt, type='image')
```
**Supported Providers:**
- **OpenAI** - GPT models, DALL-E, embeddings
- **Anthropic** - Claude models with tool calling
- **Ollama** - Local model deployment
- **Mistral** - Mistral AI models
- **LiteLLM** - Multi-provider proxy
- **Replicate** - Model marketplace
- **FAL.ai** - Fast inference API
### Model Management
Comprehensive model catalog with automatic discovery:
- **Model Registry**: Centralized tracking of available AI models
- **Capability Mapping**: Model features (chat, embedding, multimodal, etc.)
- **Publisher Management**: Organization tracking and official status
- **Auto-Discovery**: "Fetch Models" functionality for automatic import
- **Default Selection**: Configurable default models per use case
## Performance Improvements
### 10x Faster Message Queries
The new `llm_role` field provides dramatic performance improvements:
- **Before**: Complex subtype joins and computed fields
- **After**: Direct indexed field access
- **Result**: 10x faster conversation history queries
### Optimized Database Operations
- **Indexed Role Field**: Fast filtering and sorting of AI messages
- **Reduced Complexity**: Elimination of expensive role lookups
- **Efficient Pagination**: Optimized conversation history loading
- **Scalable Architecture**: Performance maintained with large datasets
## Getting Started
### Installation
1. Install the module in your Odoo instance
2. Verify dependencies are satisfied (`mail`, `web`)
3. Install provider modules for your preferred AI services
### Basic Configuration
1. **Set up AI Provider:**
```
Navigate to: LLM → Configuration → Providers
Create new provider with API credentials
Click "Fetch Models" to import available models
```
2. **Configure Models:**
```
Go to: LLM → Configuration → Models
Set default models for chat, embedding, etc.
Configure model parameters and capabilities
```
3. **Security Setup:**
```
Assign users to LLM User or LLM Manager groups
Configure API key access permissions
Set up tool consent requirements
```
## Technical Specifications
### Module Information
- **Name**: LLM Integration Base
- **Version**: 18.0.1.4.0
- **Category**: Technical
- **License**: LGPL-3
- **Dependencies**: `mail`, `web`
- **Author**: Apexive Solutions LLC
### Key Models
#### `llm.provider`
Manages connections to AI service providers:
- API authentication and configuration
- Model discovery and import
- Service-specific implementations
- Usage tracking and monitoring
#### `llm.model`
Represents individual AI models:
- Model capabilities and parameters
- Publisher information and status
- Default model configuration
- Performance and cost metadata
#### `llm.publisher`
Tracks AI model publishers:
- Organization information
- Official status verification
- Model portfolio management
- Publisher-specific settings
#### `mail.message` (Extended)
Enhanced with LLM-specific fields:
- `llm_role`: Performance-optimized role tracking
- `body_json`: Structured data for tool messages
- Computed role from message subtypes
- AI-specific email handling
### Multimodal Attachments
The module supports sending file attachments to LLM providers via enhanced mail.message:
#### Supported File Types
| Category | Mimetypes |
| ------------- | ------------------------------------------------------------------- |
| **Images** | JPEG, PNG, GIF, WebP |
| **Documents** | PDF |
| **Text** | Plain text, Markdown, CSV, HTML, CSS, JavaScript, XML, Python, JSON |
#### API Methods
```python
# Get formatted attachments from a message
images = message._get_image_attachments() # Returns list of base64 images
pdfs = message._get_pdf_attachments() # Returns list of base64 PDFs
texts = message._get_text_attachments() # Returns decoded text content
# Prepare all attachments for multimodal LLM call
attachments = message._prepare_multimodal_attachments(is_multimodal=True)
# Returns: {"images": [...], "pdfs": [...], "texts": [...], "has_attachments": bool}
```
Non-multimodal models automatically skip images/PDFs while still processing text files.
### Database Schema
```sql
-- Performance optimization: indexed role field
ALTER TABLE mail_message ADD COLUMN llm_role VARCHAR;
CREATE INDEX idx_mail_message_llm_role ON mail_message(llm_role);
-- Structured data storage for tools
ALTER TABLE mail_message ADD COLUMN body_json JSONB;
```
## API Reference
### Provider Methods
```python
# Chat completion
response = provider.chat(
messages=[{"role": "user", "content": "Hello"}],
model="gpt-4",
stream=False
)
# Text embedding
embedding = provider.embedding(
text="Sample text to embed",
model="text-embedding-ada-002"
)
# Content generation
content = provider.generate(
prompt="A beautiful landscape",
type="image",
model="dall-e-3"
)
```
### Message Posting
```python
# AI-optimized message posting
thread.message_post(
body="AI response content",
llm_role="assistant",
author_id=False
)
# Tool result with structured data
thread.message_post(
body_json={
"tool_call_id": "call_123",
"function": "search_records",
"result": {"count": 5, "records": [...]}
},
llm_role="tool"
)
```
## Integration Patterns
### Extending with New Providers
1. **Create Provider Module:**
```python
class LLMProvider(models.Model):
_inherit = "llm.provider"
@api.model
def _get_available_services(self):
return super()._get_available_services() + [
('my_service', 'My AI Service')
]
```
2. **Implement Service Methods:**
```python
def my_service_chat(self, messages, model=None, **kwargs):
"""Service-specific chat implementation"""
# Implementation details
def my_service_embedding(self, text, model=None, **kwargs):
"""Service-specific embedding implementation"""
# Implementation details
```
### Custom Message Handling
```python
class CustomThread(models.Model):
_inherit = "llm.thread"
def message_post(self, **kwargs):
# Custom pre-processing
if kwargs.get('llm_role') == 'custom':
# Handle custom role logic
pass
return super().message_post(**kwargs)
```
## Related Modules
Build complete AI solutions by combining with specialized modules:
### External AI Integration
- **`llm_mcp_server`**: MCP server exposing all Odoo tools to Claude Desktop, Claude Code, Cursor, Codex CLI, and any MCP-compatible client
### Tool Packs
- **`llm_tool`**: Tool framework with generic CRUD tools (retrieve, create, update, delete, inspect, execute)
- **`llm_tool_account`**: 18 AI-powered accounting tools — trial balance, journal entries, reconciliation, payments, tax reports, P&L, period close
- **`llm_tool_mis_builder`**: 44 tools for MIS Builder financial reporting — KPIs, periods, report computation, drill-down, variance analysis
- **`llm_tool_knowledge`**: RAG tools for semantic search, knowledge retrieval, and source citations
- **`llm_tool_ocr_mistral`**: OCR tool using Mistral vision models for invoices, receipts, scanned documents
### Chat & Assistants
- **`llm_assistant`**: AI assistants with custom prompts and personalities
- **`llm_thread`**: Chat interfaces and conversation management
### Infrastructure
- **`llm_generate`**: Unified content generation API
- **`llm_knowledge`**: RAG and knowledge base functionality
- **`llm_store`**: Vector storage and similarity search
## Support & Resources
- **Documentation**: [GitHub Repository](https://github.com/apexive/odoo-llm)
- **Architecture Guide**: [OVERVIEW.md](../OVERVIEW.md)
- **Community Support**: [GitHub Discussions](https://github.com/apexive/odoo-llm/discussions)
- **Bug Reports**: [GitHub Issues](https://github.com/apexive/odoo-llm/issues)
## License
This module is licensed under [LGPL-3](https://www.gnu.org/licenses/lgpl-3.0.html).
---
_© 2025 Apexive Solutions LLC. All rights reserved._
+2
View File
@@ -0,0 +1,2 @@
from . import models
from . import wizards
+32
View File
@@ -0,0 +1,32 @@
{
"name": "LLM Integration Base",
"summary": """
Integration with various LLM providers like Ollama, OpenAI, Replicate and Anthropic""",
"description": """
Provides integration with LLM (Large Language Model) providers for:
- Chat completions
- Text embeddings
- Model management
""",
"author": "Apexive Solutions LLC",
"website": "https://github.com/apexive/odoo-llm",
"category": "Technical",
"version": "18.0.1.7.2",
"depends": ["mail", "web"],
"data": [
"security/llm_security.xml",
"security/ir.model.access.csv",
"wizards/fetch_models_views.xml",
"views/llm_provider_views.xml",
"views/llm_model_views.xml",
"views/llm_publisher_views.xml",
"views/llm_menu_views.xml",
"data/mail_message_subtype.xml",
],
"license": "LGPL-3",
"installable": True,
"images": [
"static/description/banner.jpeg",
],
}
+59
View File
@@ -0,0 +1,59 @@
18.0.1.7.0 (2026-01-17)
~~~~~~~~~~~~~~~~~~~~~~~
* [ADD] Multimodal file support for images, PDFs, and text files
* [ADD] New is_error field for excluding error messages from LLM context
* [ADD] New _get_attachments_by_mimetype() base method for DRY attachment handling
* [ADD] New _get_unsupported_attachments() for file compatibility validation
* [ADD] AUDIO/VIDEO/OFFICE_MIMETYPES constants for file type detection
* [IMP] Refactored _get_image/pdf/text/audio_attachments() to use base method
18.0.1.6.0 (2026-01-07)
~~~~~~~~~~~~~~~~~~~~~~~
* [IMP] LLM Manager role now auto-implied for admin users (base.group_system)
* [IMP] Removed manual user assignment in favor of group implication
18.0.1.5.0 (2025-11-28)
~~~~~~~~~~~~~~~~~~~~~~~
* [ADD] Added _extract_content_text() helper for extracting text from message content (handles both string and OpenAI list formats)
* [ADD] Added _dispatch("normalize_prepend_messages") call in chat() for provider-specific message normalization
* [IMP] Improved dispatch pattern consistency for prepend_messages handling
18.0.1.4.1 (2025-11-17)
~~~~~~~~~~~~~~~~~~~~~~~
* [FIX] Fixed wizard_id not being set on llm.fetch.models.line records
* [IMP] Refactored model fetching: moved logic from wizard default_get() to provider action_fetch_models()
* [IMP] Moved _determine_model_use() from wizard to provider for better extensibility
* [REM] Removed wizard write() override workaround
* [ADD] Comprehensive docstrings with extension pattern examples
* [ADD] Documented standard capability names and priority order
18.0.1.4.0 (2025-10-23)
~~~~~~~~~~~~~~~~~~~~~~~
* [MIGRATION] Migrated to Odoo 18.0
* [IMP] Updated views and manifest for compatibility
16.0.1.3.0
~~~~~~~~~~
* [BREAKING] Moved message subtypes to base module
* [ADD] Added required `llm_role` field computation with automatic migration
* [IMP] Enhanced provider dispatch mechanism
* [MIGRATION] Automatic computation of `llm_role` for existing messages
* [MIGRATION] Database migration creates indexes for performance
16.0.1.1.0 (2025-03-06)
~~~~~~~~~~~~~~~~~~~~~~~
* [ADD] Tool support framework in base LLM models
* [IMP] Enhanced provider interface to support tool execution
* [IMP] Updated model handling for function calling capabilities
16.0.1.0.0 (2025-01-02)
~~~~~~~~~~~~~~~~~~~~~~~
* [INIT] Initial release
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="mt_tool" model="mail.message.subtype">
<field name="name">LLM Tool</field>
<field name="internal" eval="False" />
<field name="default" eval="False" />
<field name="sequence">100</field>
<field name="res_model">mail.thread</field>
</record>
<record id="mt_user" model="mail.message.subtype">
<field name="name">LLM User</field>
<field name="internal" eval="False" />
<field name="default" eval="False" />
<field name="sequence">110</field>
<field name="res_model">mail.thread</field>
</record>
<record id="mt_assistant" model="mail.message.subtype">
<field name="name">LLM Assistant</field>
<field name="internal" eval="False" />
<field name="default" eval="False" />
<field name="sequence">120</field>
<field name="res_model">mail.thread</field>
</record>
<record id="mt_system" model="mail.message.subtype">
<field name="name">LLM System</field>
<field name="internal" eval="False" />
<field name="default" eval="False" />
<field name="sequence">120</field>
<field name="res_model">mail.thread</field>
</record>
</odoo>
+171
View File
@@ -0,0 +1,171 @@
==============================
LLM Integration Base for Odoo
==============================
The foundational module for integrating Large Language Models into Odoo.
**Module Type:** 📦 Infrastructure (Core Foundation)
Architecture
============
::
┌─────────────────────────────────────────────────────────┐
│ Layer 2: Interfaces │
│ ┌─────────────┐ ┌───────────┐ ┌──────────────────────┐ │
│ │llm_assistant│ │llm_thread │ │ llm_mcp_server │ │
│ └──────┬──────┘ └─────┬─────┘ └──────────┬───────────┘ │
└─────────┼──────────────┼──────────────────┼─────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ★ llm (This Module) ★ │
│ Core Odoo-LLM Base │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ • Provider Abstraction • Model Management • Enhanced mail.message │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
▲ ▲
┌───────────────────────┼──────────────┼───────────────────────┐
│ │ │ │
┌─────┴─────┐ ┌──────┴─────┐ ┌─────┴──────┐ ┌──────┴─────┐
│llm_openai │ │ llm_ollama │ │llm_mistral │ │llm_replicate│
└───────────┘ └────────────┘ └────────────┘ └────────────┘
Providers extend this module
Installation
============
What to Install
---------------
This module is **auto-installed** as a dependency. You typically don't install it directly.
**For AI chat features, install:**
.. code-block:: bash
odoo-bin -d your_db -i llm_assistant,llm_openai
This Module Provides
--------------------
- Provider abstraction framework
- Model and publisher management
- Enhanced ``mail.message`` with ``llm_role`` field
- Security groups and access control
- Base configuration menus
Modules That Depend on This
---------------------------
+-----------------+----------------------------------------------------------------+
| Category | Modules |
+=================+================================================================+
| **Interfaces** | ``llm_assistant``, ``llm_thread``, ``llm_mcp_server`` |
+-----------------+----------------------------------------------------------------+
| **Providers** | ``llm_openai``, ``llm_ollama``, ``llm_mistral``, etc. |
+-----------------+----------------------------------------------------------------+
| **Infrastructure** | ``llm_tool``, ``llm_store``, ``llm_generate`` |
+-----------------+----------------------------------------------------------------+
Common Setups
-------------
+---------------------------+----------------------------------------------+
| I want to... | Install |
+===========================+==============================================+
| Chat with AI in Odoo | ``llm_assistant`` + ``llm_openai`` |
+---------------------------+----------------------------------------------+
| Use local AI (privacy) | ``llm_assistant`` + ``llm_ollama`` |
+---------------------------+----------------------------------------------+
| Add RAG/knowledge base | Above + ``llm_knowledge`` + ``llm_pgvector`` |
+---------------------------+----------------------------------------------+
Overview
========
The LLM Integration Base serves as the foundation for building AI-powered features across Odoo applications. It extends Odoo's core messaging system with AI-specific capabilities.
Core Capabilities
-----------------
- **Enhanced Messaging System** - AI-optimized message handling with 10x performance improvement
- **Provider Abstraction** - Unified interface for multiple AI services
- **Model Management** - Centralized catalog of AI models with capabilities
- **Security Framework** - Role-based access control and API key management
Key Features
============
Enhanced Mail Message System
----------------------------
.. code-block:: python
# Performance-optimized role field (10x faster queries)
llm_role = fields.Selection([
('user', 'User'),
('assistant', 'Assistant'),
('tool', 'Tool'),
('system', 'System')
], store=True, index=True)
# Structured data for tool messages
body_json = fields.Json()
Provider Framework
------------------
.. code-block:: python
# Dynamic method dispatch to service implementations
provider._dispatch('chat', messages=messages, model=model)
provider._dispatch('embedding', text=text)
provider._dispatch('generate', prompt=prompt, type='image')
Technical Specifications
========================
Module Information
------------------
- **Name**: LLM Integration Base
- **Version**: 18.0.1.4.0
- **Category**: Technical
- **License**: LGPL-3
- **Dependencies**: ``mail``, ``web``
- **Author**: Apexive Solutions LLC
Key Models
----------
- **``llm.provider``**: AI service provider configuration
- **``llm.model``**: AI model registry
- **``llm.publisher``**: Model publisher tracking
- **``mail.message``** (extended): Enhanced with ``llm_role`` and ``body_json``
Related Modules
===============
- **``llm_assistant``** - AI assistants with prompt management
- **``llm_thread``** - Chat interfaces and conversation management
- **``llm_tool``** - Function calling and Odoo integration
- **``llm_openai``** - OpenAI provider implementation
- **``llm_ollama``** - Local model deployment
Resources
=========
- `GitHub Repository <https://github.com/apexive/odoo-llm>`_
- `Architecture Overview <../OVERVIEW.md>`_
License
=======
This module is licensed under `LGPL-3 <https://www.gnu.org/licenses/lgpl-3.0.html>`_.
----
*© 2025 Apexive Solutions LLC. All rights reserved.*
+636
View File
@@ -0,0 +1,636 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * llm
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-27 00:08+0000\n"
"PO-Revision-Date: 2025-02-27 00:08+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: llm
#. odoo-python
#: code:addons/llm/wizards/fetch_models_wizard.py:0
#, python-format
msgid "%d models have been imported/updated."
msgstr "%d modelos foram importados/atualizados."
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.view_fetch_models_wizard
msgid ""
"<i class=\"fa fa-info-circle\"/>\n"
" Select the models you want to import or update. New models will be created, and existing ones will be updated if modified."
msgstr ""
"<i class=\"fa fa-info-circle\"/>\n"
" Selecione os modelos que deseja importar ou atualizar. Novos modelos serão criados e os existentes serão atualizados se modificados."
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__message_needaction
#: model:ir.model.fields,field_description:llm.field_llm_provider__message_needaction
#: model:ir.model.fields,field_description:llm.field_llm_publisher__message_needaction
msgid "Action Needed"
msgstr "Ação Necessária"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__active
#: model:ir.model.fields,field_description:llm.field_llm_provider__active
msgid "Active"
msgstr "Ativo"
#. module: llm
#: model_terms:ir.actions.act_window,help:llm.llm_publisher_action
msgid "Add publishers like OpenAI, Anthropic, Meta, Google, etc."
msgstr "Adicione editores como OpenAI, Anthropic, Meta, Google, etc."
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_provider__api_base
msgid "Api Base"
msgstr ""
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_provider__api_key
msgid "Api Key"
msgstr ""
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_model_view_search
msgid "Archived"
msgstr "Arquivado"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__message_attachment_count
#: model:ir.model.fields,field_description:llm.field_llm_provider__message_attachment_count
#: model:ir.model.fields,field_description:llm.field_llm_publisher__message_attachment_count
msgid "Attachment Count"
msgstr "Contagem de Anexos"
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.view_fetch_models_wizard
msgid "Cancel"
msgstr "Cancelar"
#. module: llm
#: model:ir.model.fields.selection,name:llm.selection__llm_fetch_models_line__model_use__chat
#: model:ir.model.fields.selection,name:llm.selection__llm_model__model_use__chat
msgid "Chat"
msgstr ""
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_provider__company_id
msgid "Company"
msgstr "Empresa"
#. module: llm
#: model:ir.model.fields.selection,name:llm.selection__llm_fetch_models_line__model_use__completion
#: model:ir.model.fields.selection,name:llm.selection__llm_model__model_use__completion
msgid "Completion"
msgstr "Conclusão"
#. module: llm
#: model:ir.ui.menu,name:llm.menu_llm_config
msgid "Configuration"
msgstr "Configuração"
#. module: llm
#: model_terms:ir.actions.act_window,help:llm.llm_provider_action
msgid ""
"Configure providers like OpenAI, Anthropic, or Ollama to enable AI features."
msgstr "Configure provedores como OpenAI, Anthropic ou Ollama para habilitar recursos de IA."
#. module: llm
#: model_terms:ir.actions.act_window,help:llm.llm_provider_action
msgid "Create your first LLM provider!"
msgstr "Crie seu primeiro provedor de LLM!"
#. module: llm
#: model_terms:ir.actions.act_window,help:llm.llm_publisher_action
msgid "Create your first LLM publisher!"
msgstr "Crie seu primeiro editor de LLM!"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__create_uid
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_wizard__create_uid
#: model:ir.model.fields,field_description:llm.field_llm_model__create_uid
#: model:ir.model.fields,field_description:llm.field_llm_provider__create_uid
#: model:ir.model.fields,field_description:llm.field_llm_publisher__create_uid
msgid "Created by"
msgstr "Criado por"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__create_date
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_wizard__create_date
#: model:ir.model.fields,field_description:llm.field_llm_model__create_date
#: model:ir.model.fields,field_description:llm.field_llm_provider__create_date
#: model:ir.model.fields,field_description:llm.field_llm_publisher__create_date
msgid "Created on"
msgstr "Criado em"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__default
msgid "Default"
msgstr "Padrão"
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_model_view_search
msgid "Default Models"
msgstr "Modelos Padrão"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_publisher__description
#: model_terms:ir.ui.view,arch_db:llm.llm_publisher_view_form
msgid "Description"
msgstr "Descrição"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__details
#: model:ir.model.fields,field_description:llm.field_llm_model__details
msgid "Details"
msgstr "Detalhes"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__display_name
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_wizard__display_name
#: model:ir.model.fields,field_description:llm.field_llm_model__display_name
#: model:ir.model.fields,field_description:llm.field_llm_provider__display_name
#: model:ir.model.fields,field_description:llm.field_llm_publisher__display_name
msgid "Display Name"
msgstr "Nome Exibido"
#. module: llm
#: model:ir.model.constraint,message:llm.constraint_llm_fetch_models_line_unique_model_per_wizard
msgid "Each model can only be listed once per import."
msgstr "Cada modelo pode ser listado apenas uma vez por importação."
#. module: llm
#: model:ir.model.fields.selection,name:llm.selection__llm_fetch_models_line__model_use__embedding
#: model:ir.model.fields.selection,name:llm.selection__llm_model__model_use__embedding
msgid "Embedding"
msgstr ""
#. module: llm
#: model:ir.model.fields.selection,name:llm.selection__llm_fetch_models_line__status__existing
msgid "Existing"
msgstr "Existente"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__existing_model_id
msgid "Existing Model"
msgstr "Modelo Existente"
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_provider_view_form
msgid "Fetch Models"
msgstr "Buscar Modelos"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__message_follower_ids
#: model:ir.model.fields,field_description:llm.field_llm_provider__message_follower_ids
#: model:ir.model.fields,field_description:llm.field_llm_publisher__message_follower_ids
msgid "Followers"
msgstr "Seguidores"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__message_partner_ids
#: model:ir.model.fields,field_description:llm.field_llm_provider__message_partner_ids
#: model:ir.model.fields,field_description:llm.field_llm_publisher__message_partner_ids
msgid "Followers (Partners)"
msgstr "Seguidores (Parceiros)"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_publisher__frontier
msgid "Frontier"
msgstr ""
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_publisher_view_search
msgid "Frontier AI"
msgstr ""
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_publisher_view_search
msgid "Frontier Status"
msgstr ""
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_model_view_search
#: model_terms:ir.ui.view,arch_db:llm.llm_publisher_view_search
msgid "Group By"
msgstr "Agrupar por"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__has_message
#: model:ir.model.fields,field_description:llm.field_llm_provider__has_message
#: model:ir.model.fields,field_description:llm.field_llm_publisher__has_message
msgid "Has Message"
msgstr "Possui Mensagem"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__id
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_wizard__id
#: model:ir.model.fields,field_description:llm.field_llm_model__id
#: model:ir.model.fields,field_description:llm.field_llm_provider__id
#: model:ir.model.fields,field_description:llm.field_llm_publisher__id
msgid "ID"
msgstr ""
#. module: llm
#: model:ir.model.fields,help:llm.field_llm_model__message_needaction
#: model:ir.model.fields,help:llm.field_llm_provider__message_needaction
#: model:ir.model.fields,help:llm.field_llm_publisher__message_needaction
msgid "If checked, new messages require your attention."
msgstr "Se marcado, novas mensagens exigem sua atenção."
#. module: llm
#: model:ir.model.fields,help:llm.field_llm_model__message_has_error
#: model:ir.model.fields,help:llm.field_llm_provider__message_has_error
#: model:ir.model.fields,help:llm.field_llm_publisher__message_has_error
msgid "If checked, some messages have a delivery error."
msgstr "Se marcado, algumas mensagens apresentam erro de entrega."
#. module: llm
#: model:ir.model,name:llm.model_llm_fetch_models_wizard
msgid "Import LLM Models"
msgstr "Importar Modelos de LLM"
#. module: llm
#: model:ir.actions.act_window,name:llm.action_fetch_models_wizard
msgid "Import Models"
msgstr "Importar Modelos"
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.view_fetch_models_wizard
msgid "Import Selected Models"
msgstr "Importar Modelos Selecionados"
#. module: llm
#: model:ir.model.fields,help:llm.field_llm_publisher__official
msgid "Indicates if this is an official model publisher"
msgstr "Indica se este é um editor de modelos oficial"
#. module: llm
#: model:ir.model.fields,help:llm.field_llm_publisher__frontier
msgid "Indicates if this publisher is working on frontier AI models"
msgstr ""
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__message_is_follower
#: model:ir.model.fields,field_description:llm.field_llm_provider__message_is_follower
#: model:ir.model.fields,field_description:llm.field_llm_publisher__message_is_follower
msgid "Is Follower"
msgstr "É Seguidor"
#. module: llm
#: model:ir.ui.menu,name:llm.menu_llm_root
msgid "LLM"
msgstr ""
#. module: llm
#: model:res.groups,name:llm.group_llm_manager
msgid "LLM Manager"
msgstr "Gerenciador de LLM"
#. module: llm
#: model:ir.model,name:llm.model_llm_model
msgid "LLM Model"
msgstr "Modelo de LLM"
#. module: llm
#: model:ir.model,name:llm.model_llm_fetch_models_line
msgid "LLM Model Import Line"
msgstr "Linha de Importação de Modelo de LLM"
#. module: llm
#: model:ir.actions.act_window,name:llm.llm_model_action
msgid "LLM Models"
msgstr "Modelos de LLM"
#. module: llm
#: model:ir.model,name:llm.model_llm_provider
msgid "LLM Provider"
msgstr "Provedor de LLM"
#. module: llm
#: model:ir.actions.act_window,name:llm.llm_provider_action
msgid "LLM Providers"
msgstr "Provedores de LLM"
#. module: llm
#: model:ir.model,name:llm.model_llm_publisher
msgid "LLM Publisher"
msgstr ""
#. module: llm
#: model:ir.actions.act_window,name:llm.llm_publisher_action
msgid "LLM Publishers"
msgstr "Editor de LLM"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line____last_update
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_wizard____last_update
#: model:ir.model.fields,field_description:llm.field_llm_model____last_update
#: model:ir.model.fields,field_description:llm.field_llm_provider____last_update
#: model:ir.model.fields,field_description:llm.field_llm_publisher____last_update
msgid "Last Modified on"
msgstr "Última Modificação em"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__write_uid
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_wizard__write_uid
#: model:ir.model.fields,field_description:llm.field_llm_model__write_uid
#: model:ir.model.fields,field_description:llm.field_llm_provider__write_uid
#: model:ir.model.fields,field_description:llm.field_llm_publisher__write_uid
msgid "Last Updated by"
msgstr "Última Atualização por"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__write_date
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_wizard__write_date
#: model:ir.model.fields,field_description:llm.field_llm_model__write_date
#: model:ir.model.fields,field_description:llm.field_llm_provider__write_date
#: model:ir.model.fields,field_description:llm.field_llm_publisher__write_date
msgid "Last Updated on"
msgstr "Última Atualização em"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_publisher__logo
msgid "Logo"
msgstr ""
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__message_main_attachment_id
#: model:ir.model.fields,field_description:llm.field_llm_provider__message_main_attachment_id
#: model:ir.model.fields,field_description:llm.field_llm_publisher__message_main_attachment_id
msgid "Main Attachment"
msgstr "Anexo Principal"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__message_has_error
#: model:ir.model.fields,field_description:llm.field_llm_provider__message_has_error
#: model:ir.model.fields,field_description:llm.field_llm_publisher__message_has_error
msgid "Message Delivery error"
msgstr "Erro de Entrega da Mensagem"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__message_ids
#: model:ir.model.fields,field_description:llm.field_llm_provider__message_ids
#: model:ir.model.fields,field_description:llm.field_llm_publisher__message_ids
msgid "Messages"
msgstr "Mensagens"
#. module: llm
#. odoo-python
#: code:addons/llm/models/llm_provider.py:0
#, python-format
msgid "Method %s not implemented for service %s"
msgstr ""
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_publisher__model_count
msgid "Model Count"
msgstr "Contagem de Modelos"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__model_info
msgid "Model Info"
msgstr "Informações do Modelo"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__name
#: model_terms:ir.ui.view,arch_db:llm.llm_model_view_form
msgid "Model Name"
msgstr "Nome do Modelo"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__model_use
#: model:ir.model.fields,field_description:llm.field_llm_model__model_use
msgid "Model Use"
msgstr "Uso do Modelo"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_wizard__line_ids
#: model:ir.model.fields,field_description:llm.field_llm_provider__model_ids
#: model:ir.model.fields,field_description:llm.field_llm_publisher__model_ids
#: model:ir.ui.menu,name:llm.menu_llm_model
#: model_terms:ir.ui.view,arch_db:llm.llm_provider_view_form
#: model_terms:ir.ui.view,arch_db:llm.llm_publisher_view_form
msgid "Models"
msgstr "Modelos"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_wizard__model_count
msgid "Models Found"
msgstr "Modelos Encontrados"
#. module: llm
#: model_terms:ir.actions.act_window,help:llm.llm_model_action
msgid "Models will be automatically created when you configure a provider."
msgstr "Os modelos serão criados automaticamente quando você configurar um provedor."
#. module: llm
#: model:ir.model.fields.selection,name:llm.selection__llm_fetch_models_line__status__modified
msgid "Modified"
msgstr "Modificado"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_wizard__modified_count
msgid "Modified Models"
msgstr "Modelos Modificado"
#. module: llm
#: model:ir.model.fields.selection,name:llm.selection__llm_fetch_models_line__model_use__multimodal
#: model:ir.model.fields.selection,name:llm.selection__llm_model__model_use__multimodal
msgid "Multimodal"
msgstr ""
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__name
#: model:ir.model.fields,field_description:llm.field_llm_provider__name
#: model:ir.model.fields,field_description:llm.field_llm_publisher__name
msgid "Name"
msgstr "Nome"
#. module: llm
#: model:ir.model.fields.selection,name:llm.selection__llm_fetch_models_line__status__new
msgid "New"
msgstr "Novo"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_wizard__new_count
msgid "New Models"
msgstr "Novos Modelos"
#. module: llm
#: model_terms:ir.actions.act_window,help:llm.llm_model_action
msgid "No models found!"
msgstr "Nenhum modelo encontrado!"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__message_needaction_counter
#: model:ir.model.fields,field_description:llm.field_llm_provider__message_needaction_counter
#: model:ir.model.fields,field_description:llm.field_llm_publisher__message_needaction_counter
msgid "Number of Actions"
msgstr "Número de Ações"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__message_has_error_counter
#: model:ir.model.fields,field_description:llm.field_llm_provider__message_has_error_counter
#: model:ir.model.fields,field_description:llm.field_llm_publisher__message_has_error_counter
msgid "Number of errors"
msgstr "Número de erros"
#. module: llm
#: model:ir.model.fields,help:llm.field_llm_model__message_needaction_counter
#: model:ir.model.fields,help:llm.field_llm_provider__message_needaction_counter
#: model:ir.model.fields,help:llm.field_llm_publisher__message_needaction_counter
msgid "Number of messages requiring action"
msgstr "Número de mensagens que exigem ação"
#. module: llm
#: model:ir.model.fields,help:llm.field_llm_model__message_has_error_counter
#: model:ir.model.fields,help:llm.field_llm_provider__message_has_error_counter
#: model:ir.model.fields,help:llm.field_llm_publisher__message_has_error_counter
msgid "Number of messages with delivery error"
msgstr "Número de mensagens com erro de entrega"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_publisher__official
msgid "Official"
msgstr "Oficial"
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_publisher_view_search
msgid "Official Publishers"
msgstr "Editores Oficiais"
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_publisher_view_search
msgid "Official Status"
msgstr "Status Oficial"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__parameters
msgid "Parameters"
msgstr "Parâmetros"
#. module: llm
#. odoo-python
#: code:addons/llm/wizards/fetch_models_wizard.py:0
#, python-format
msgid "Please select at least one model to import."
msgstr "Por favor, selecione pelo menos um modelo para importar."
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_wizard__provider_id
#: model:ir.model.fields,field_description:llm.field_llm_model__provider_id
#: model_terms:ir.ui.view,arch_db:llm.llm_model_view_search
msgid "Provider"
msgstr "Provedor"
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_provider_view_form
msgid "Provider Name"
msgstr "Nome do Provedor"
#. module: llm
#. odoo-python
#: code:addons/llm/wizards/fetch_models_wizard.py:0
#, python-format
msgid "Provider not found."
msgstr "Provedor não encontrado."
#. module: llm
#. odoo-python
#: code:addons/llm/models/llm_provider.py:0
#, python-format
msgid "Provider service not configured"
msgstr "Serviço do provedor não configurado"
#. module: llm
#: model:ir.ui.menu,name:llm.menu_llm_provider
msgid "Providers"
msgstr "Provedores"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__publisher_id
msgid "Publisher"
msgstr "Editor"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_publisher__meta
msgid "Publisher Metadata"
msgstr "Metadados do Editor"
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_publisher_view_form
msgid "Publisher Name"
msgstr "Nome do Editor"
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_publisher_view_form
msgid "Publisher description..."
msgstr "Descrição do Editor..."
#. module: llm
#: model:ir.model.fields,help:llm.field_llm_publisher__logo
msgid "Publisher logo"
msgstr "Logo do Editor"
#. module: llm
#: model:ir.ui.menu,name:llm.menu_llm_publisher
msgid "Publishers"
msgstr "Editores"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__selected
msgid "Selected"
msgstr "Selecionado"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_provider__service
msgid "Service"
msgstr "Serviço"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__status
msgid "Status"
msgstr "Status"
#. module: llm
#. odoo-python
#: code:addons/llm/wizards/fetch_models_wizard.py:0
#, python-format
msgid "Success"
msgstr "Sucesso"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_model__template
msgid "Template"
msgstr ""
#. module: llm
#: model:ir.model.fields,help:llm.field_llm_model__publisher_id
msgid "The organization or entity that published this model"
msgstr "A organização ou entidade que publicou este modelo"
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_model_view_search
msgid "Usage"
msgstr "Uso"
#. module: llm
#: model:ir.model.fields,field_description:llm.field_llm_fetch_models_line__wizard_id
msgid "Wizard"
msgstr "Assistente"
#. module: llm
#: model_terms:ir.ui.view,arch_db:llm.llm_provider_view_form
msgid "http://localhost:11434"
msgstr ""
@@ -0,0 +1,65 @@
import logging
from odoo import SUPERUSER_ID, api
_logger = logging.getLogger(__name__)
def migrate(cr, version):
"""
Simple and efficient LLM migration:
Migrate messages from old subtypes to new subtypes using direct batch updates.
The llm_role field will auto-compute based on the new subtype_id.
"""
env = api.Environment(cr, SUPERUSER_ID, {})
_logger.info("Starting LLM migration...")
# Migrate messages from old subtypes to new subtypes
migrations = [
("llm_mail_message_subtypes.mt_llm_user", "llm.mt_user"),
("llm_mail_message_subtypes.mt_llm_assistant", "llm.mt_assistant"),
("llm_mail_message_subtypes.mt_llm_tool_result", "llm.mt_tool"),
]
total_migrated = 0
for old_xmlid, new_xmlid in migrations:
try:
# Get old and new subtype references
old_subtype = env.ref(old_xmlid, raise_if_not_found=False)
new_subtype = env.ref(new_xmlid, raise_if_not_found=False)
if not old_subtype:
_logger.info(f"Old subtype {old_xmlid} not found, skipping")
continue
if not new_subtype:
_logger.warning(f"New subtype {new_xmlid} not found, skipping")
continue
# Find and migrate messages
messages = env["mail.message"].search([("subtype_id", "=", old_subtype.id)])
if messages:
_logger.info(
f"Migrating {len(messages)} messages from {old_xmlid} to {new_xmlid}"
)
messages.write({"subtype_id": new_subtype.id})
total_migrated += len(messages)
_logger.info(f"✓ Successfully migrated {len(messages)} messages")
else:
_logger.info(f"No messages found for {old_xmlid}")
except Exception as e:
_logger.error(f"Error migrating {old_xmlid}: {str(e)}")
continue
_logger.info(f"LLM migration completed. Total messages migrated: {total_migrated}")
# Clear cache to ensure role computation works properly
try:
env["mail.message"].get_llm_roles.clear_cache(env["mail.message"])
_logger.info("Cleared LLM role cache")
except Exception as e:
_logger.warning(f"Could not clear cache: {str(e)}")
+4
View File
@@ -0,0 +1,4 @@
from . import llm_model
from . import llm_provider
from . import llm_publisher
from . import mail_message
+88
View File
@@ -0,0 +1,88 @@
from odoo import api, fields, models
class LLMModel(models.Model):
_name = "llm.model"
_description = "LLM Model"
_inherit = ["mail.thread"]
name = fields.Char(required=True)
provider_id = fields.Many2one("llm.provider", required=True, ondelete="cascade")
publisher_id = fields.Many2one(
"llm.publisher",
string="Publisher",
ondelete="restrict",
tracking=True,
help="The organization or entity that published this model",
)
model_use = fields.Selection(
selection="_get_available_model_usages",
required=True,
default="chat",
)
default = fields.Boolean(default=False)
active = fields.Boolean(default=True)
# Model details
details = fields.Json()
model_info = fields.Json()
parameters = fields.Text()
template = fields.Text()
@api.model
def _get_available_model_usages(self):
return [
("embedding", "Embedding"),
("completion", "Completion"),
("chat", "Chat"),
("multimodal", "Multimodal"),
("generation", "Generic binary generation"),
("image_generation", "Image Generation"),
]
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
for record in records:
if record.default:
# Ensure only one default per provider/use combo
self.search(
[
("provider_id", "=", record.provider_id.id),
("model_use", "=", record.model_use),
("default", "=", True),
("id", "!=", record.id),
]
).write({"default": False})
return records
def chat(self, messages, stream=False, **kwargs):
"""Send chat messages using this model"""
return self.provider_id.chat(messages, model=self, stream=stream, **kwargs)
def embedding(self, texts):
"""Generate embeddings using this model"""
return self.provider_id.embedding(texts, model=self)
def generate(self, input_data, stream=False, **kwargs):
"""Generate content using this model
Args:
input_data: Input data for generation (could be text, prompt, or structured data)
stream: Whether to stream the response
**kwargs: Additional provider-specific parameters
Returns:
Generated content from the provider
"""
return self.provider_id.generate(
input_data, model=self, stream=stream, **kwargs
)
def action_open_fetch_this_model_wizard(self):
self.ensure_one()
# Call the provider's action_fetch_models with context for specific model
return self.provider_id.with_context(
default_model_to_fetch=self.name
).action_fetch_models()
+411
View File
@@ -0,0 +1,411 @@
from datetime import datetime
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
class LLMProvider(models.Model):
_name = "llm.provider"
_inherit = ["mail.thread"]
_description = "LLM Provider"
name = fields.Char(required=True)
service = fields.Selection(
selection=lambda self: self._selection_service(),
required=True,
)
active = fields.Boolean(default=True)
company_id = fields.Many2one(
"res.company",
string="Company",
required=True,
default=lambda self: self.env.company,
)
api_key = fields.Char()
api_base = fields.Char()
model_ids = fields.One2many("llm.model", "provider_id", string="Models")
@api.constrains("name")
def _check_unique_name(self):
other_providers = self.search([("id", "not in", self.ids)])
existing_names_lower = [p.name.lower() for p in other_providers if p.name]
for record in self:
if record.name and record.name.lower() in existing_names_lower:
raise ValidationError(
_("The provider name must be unique (case-insensitive)."),
)
return True
@property
def client(self):
"""Get client instance using dispatch pattern"""
return self._dispatch("get_client")
def _dispatch(self, method, *args, record=None, **kwargs):
"""Dispatch method call to appropriate service implementation on self or a given record."""
if not self.service:
raise UserError(_("Provider service not configured"))
service_method = f"{self.service}_{method}"
record = record if record else self
record_name = record._name
if not hasattr(record, service_method):
raise NotImplementedError(
_("Method '%s' not implemented for service '%s' on target '%s'")
% (method, self.service, record_name),
)
return getattr(record, service_method)(*args, **kwargs)
@api.model
def _selection_service(self):
"""Get all available services from provider implementations"""
services = []
for provider in self._get_available_services():
services.append(provider)
return services
@api.model
def _get_available_services(self):
"""Hook method for registering provider services"""
return []
def chat(
self,
messages,
model=None,
stream=False,
tools=None,
prepend_messages=None,
**kwargs,
):
"""Send chat messages using this provider.
Args:
messages: mail.message recordset (Odoo records) to send
model: Optional specific model to use
stream: Whether to stream the response
tools: llm.tool recordset of available tools
prepend_messages: List of pre-formatted message dicts to prepend (e.g., system prompts)
**kwargs: Additional provider-specific parameters
Returns:
Generator yielding response chunks if streaming, else complete response
"""
# Hook: allow extensions to modify prepend_messages (e.g., add tool consent)
prepend_messages = self._prepare_prepend_messages(prepend_messages, tools)
# Normalize prepend_messages for the specific provider format
prepend_messages = self._dispatch(
"normalize_prepend_messages",
prepend_messages,
)
return self._dispatch(
"chat",
messages,
model=model,
stream=stream,
tools=tools,
prepend_messages=prepend_messages,
**kwargs,
)
def _prepare_prepend_messages(self, prepend_messages, tools):
"""Hook for extensions to modify prepend messages before sending to provider.
Override in extension modules (e.g., llm_tool for consent injection).
Args:
prepend_messages: List of pre-formatted message dicts (e.g., system prompts)
tools: llm.tool recordset of available tools
Returns:
List of message dicts to prepend to the conversation
"""
return prepend_messages or []
def _extract_content_text(self, content):
"""Extract plain text from message content.
Handles both string and OpenAI list formats:
- String: "hello""hello"
- List: [{"type": "text", "text": "hello"}] → "hello"
Args:
content: Message content (string or list format)
Returns:
str: Plain text content
"""
if isinstance(content, str):
return content
if isinstance(content, list):
return "\n".join(
item.get("text", "")
for item in content
if isinstance(item, dict) and item.get("type") == "text"
)
return ""
def embedding(self, texts, model=None):
"""Generate embeddings using this provider"""
return self._dispatch("embedding", texts, model=model)
def generate(self, input_data, model=None, stream=False, **kwargs):
"""Generate content using this provider
Args:
input_data: Input data for generation (could be text, prompt, or structured data)
model: Optional specific model to use
stream: Whether to stream the response
**kwargs: Additional provider-specific parameters
Returns:
tuple: (output_dict, urls_list) where:
- output_dict: Dictionary containing provider-specific output data
- urls_list: List of dictionaries with URL metadata
"""
return self._dispatch(
"generate",
input_data,
model=model,
stream=stream,
**kwargs,
)
def list_models(self, model_id=None):
"""List available models from the provider"""
return self._dispatch("models", model_id=model_id)
def action_fetch_models(self):
"""Fetch models from provider and open import wizard"""
self.ensure_one()
# Create wizard first so it has an ID
wizard = self.env["llm.fetch.models.wizard"].create(
{
"provider_id": self.id,
},
)
# Get existing models for comparison
existing_models = {
model.name: model
for model in self.env["llm.model"].search([("provider_id", "=", self.id)])
}
# Fetch models from provider
model_to_fetch = self._context.get("default_model_to_fetch")
if model_to_fetch:
models_data = self.list_models(model_id=model_to_fetch)
else:
models_data = self.list_models()
# Track models to prevent duplicates
wizard_models = set()
lines_to_create = []
for model_data in models_data:
details = model_data.get("details", {})
name = model_data.get("name") or details.get("id")
if not name:
continue
# Skip duplicates
if name in wizard_models:
continue
wizard_models.add(name)
# Determine model use and capabilities
capabilities = details.get("capabilities", ["chat"])
model_use = self._determine_model_use(name, capabilities)
# Check against existing models
existing = existing_models.get(name)
status = "new"
if existing:
status = "modified" if existing.details != details else "existing"
lines_to_create.append(
{
"wizard_id": wizard.id,
"name": name,
"model_use": model_use,
"status": status,
"details": details,
"existing_model_id": existing.id if existing else False,
"selected": status in ["new", "modified"],
},
)
# Create all lines
if lines_to_create:
self.env["llm.fetch.models.line"].create(lines_to_create)
# Return action to open the wizard
return {
"type": "ir.actions.act_window",
"res_model": "llm.fetch.models.wizard",
"res_id": wizard.id,
"view_mode": "form",
"target": "new",
"name": _("Import Models"),
}
def _determine_model_use(self, name, capabilities):
"""
Determine the primary model use based on capabilities.
This method classifies models into Odoo's model_use categories based on their
capabilities. The classification follows a priority order from most specialized
to most general.
EXTENSION POINT: Override this method in your provider class to add custom
model types or modify classification logic.
Args:
name (str): Model name/ID from the provider
capabilities (list): List of capability strings (usually from API response)
Returns:
str: One of the model_use values from _get_available_model_usages()
Default options: "chat", "embedding", "multimodal", "completion", etc.
Priority Order:
1. embedding - Specialized embedding models
2. multimodal - Models with vision/image understanding
3. chat - General conversational models (default)
Standard Capability Names:
- "chat": Text-based conversations
- "embedding"/"text-embedding": Vector embeddings
- "multimodal"/"vision": Image/vision understanding
- "completion": Text completion
- "function_calling": Tool/function support
Provider-specific: "ocr", "image_generation", etc.
Example Override:
```python
class MyProvider(models.Model):
_inherit = "llm.provider"
def _determine_model_use(self, name, capabilities):
# Add custom model type
if "ocr" in capabilities:
return "ocr"
# Fall back to parent logic for standard types
return super()._determine_model_use(name, capabilities)
```
See Also:
- llm_mistral.models.mistral_provider for a working example
- _<provider>_parse_model() for setting capabilities
"""
# Priority 1: Embedding models (specialized, distinct use case)
if (
any(cap in capabilities for cap in ["embedding", "text-embedding"])
or "embedding" in name.lower()
):
return "embedding"
# Priority 2: Multimodal models (advanced capability)
if any(cap in capabilities for cap in ["multimodal", "vision"]):
return "multimodal"
# Priority 3: Chat models (default for most LLMs)
return "chat"
def get_model(self, model=None, model_use="chat"):
"""Get a model to use for the given purpose
Args:
model: Optional specific model to use
model_use: Type of model to get if no specific model provided
Returns:
llm.model record to use
"""
if model:
return model
# Get models from provider
models = self.model_ids
# Filter for default model of requested type
default_models = models.filtered(
lambda m: m.default and m.model_use == model_use,
)
if not default_models:
# Fallback to any model of requested type
default_models = models.filtered(lambda m: m.model_use == model_use)
if not default_models:
raise ValueError(f"No {model_use} model found for provider {self.name}")
return default_models[0]
@staticmethod
def serialize_datetime(obj):
"""Helper function to serialize datetime objects to ISO format strings."""
if isinstance(obj, datetime):
return obj.isoformat()
return obj
@staticmethod
def serialize_model_data(data: dict) -> dict:
"""
Recursively process dictionary to serialize datetime objects
and handle any other non-serializable types.
Args:
data (dict): Dictionary potentially containing datetime objects
Returns:
dict: Processed dictionary with datetime objects converted to ISO strings
"""
return {
key: LLMProvider.serialize_datetime(value)
if isinstance(value, datetime)
else LLMProvider.serialize_model_data(value)
if isinstance(value, dict)
else [
LLMProvider.serialize_model_data(item)
if isinstance(item, dict)
else LLMProvider.serialize_datetime(item)
for item in value
]
if isinstance(value, list)
else value
for key, value in data.items()
}
def format_tools(self, tools):
"""Format tools for the specific provider"""
return self._dispatch("format_tools", tools)
def format_messages(self, messages, system_prompt=None, model=None):
"""Format messages for this provider
Args:
messages: List of messages to format for specific provider, could be mail.message record set or similar data format
system_prompt: Optional system prompt to include at the beginning of the messages
model: llm.model record (to determine if multimodal)
Returns:
List of formatted messages in provider-specific format
"""
return self._dispatch(
"format_messages",
messages,
system_prompt=system_prompt,
model=model,
)
def _get_provider_tool_params(self, tools, kwargs):
"""Hook for provider-specific tool parameters."""
return {}
@@ -0,0 +1,33 @@
from odoo import api, fields, models
class LLMPublisher(models.Model):
_name = "llm.publisher"
_description = "LLM Publisher"
_inherit = ["mail.thread"]
name = fields.Char(required=True, tracking=True)
logo = fields.Image(
max_width=1024, max_height=1024, verify_resolution=True, help="Publisher logo"
)
description = fields.Text(tracking=True)
meta = fields.Json(string="Publisher Metadata")
official = fields.Boolean(
default=False,
tracking=True,
help="Indicates if this is an official model publisher",
)
frontier = fields.Boolean(
default=False,
tracking=True,
help="Indicates if this publisher is working on frontier AI models",
)
# Relationships
model_ids = fields.One2many("llm.model", "publisher_id", string="Models")
model_count = fields.Integer(compute="_compute_model_count", store=True)
@api.depends("model_ids")
def _compute_model_count(self):
for record in self:
record.model_count = len(record.model_ids)
+441
View File
@@ -0,0 +1,441 @@
import base64
import logging
from odoo import _, api, fields, models, tools
_logger = logging.getLogger(__name__)
IMAGE_MIMETYPES = (
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
)
# Magic bytes for image type detection
IMAGE_MAGIC_BYTES = {
b"\x89PNG\r\n\x1a\n": "image/png",
b"\xff\xd8\xff": "image/jpeg",
b"GIF87a": "image/gif",
b"GIF89a": "image/gif",
b"RIFF": "image/webp", # RIFF....WEBP
}
PDF_MIMETYPES = ("application/pdf",)
# Audio mimetypes - only supported by OpenAI gpt-4o-audio-preview models
AUDIO_MIMETYPES = (
"audio/wav",
"audio/x-wav",
"audio/mpeg",
"audio/mp3",
"audio/ogg",
"audio/flac",
"audio/webm",
"audio/mp4",
"audio/m4a",
"audio/x-m4a",
)
# Video mimetypes - NOT supported by any LLM provider
VIDEO_MIMETYPES = (
"video/mp4",
"video/webm",
"video/quicktime",
"video/x-msvideo",
"video/x-matroska",
"video/ogg",
)
# Office document mimetypes - NOT supported via chat API
OFFICE_MIMETYPES = (
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
)
TEXT_MIMETYPES = (
"text/plain",
"text/markdown",
"text/csv",
"text/html",
"text/css",
"text/javascript",
"text/xml",
"text/x-python",
"application/json",
"application/xml",
"application/javascript",
"application/x-python-code",
)
SUPPORTED_IMAGE_MIMETYPES = IMAGE_MIMETYPES
def _detect_image_mimetype(raw_bytes):
"""Detect the real image mimetype from magic bytes.
Args:
raw_bytes: The raw image bytes
Returns:
The detected mimetype or None if not recognized
"""
for magic, mimetype in IMAGE_MAGIC_BYTES.items():
if raw_bytes.startswith(magic):
# Special check for WebP: must have WEBP after RIFF header
if magic == b"RIFF" and len(raw_bytes) >= 12:
if raw_bytes[8:12] != b"WEBP":
continue
return mimetype
return None
def _detect_audio_format(raw_bytes):
"""Detect audio format from magic bytes for OpenAI API.
Args:
raw_bytes: The raw audio bytes
Returns:
The format string for OpenAI API (wav, mp3, flac, ogg) or None
"""
# WAV: RIFF....WAVE
if raw_bytes[:4] == b"RIFF" and len(raw_bytes) >= 12:
if raw_bytes[8:12] == b"WAVE":
return "wav"
# MP3: ID3 tag or sync bytes
if raw_bytes[:3] == b"ID3" or raw_bytes[:2] == b"\xff\xfb":
return "mp3"
# FLAC
if raw_bytes[:4] == b"fLaC":
return "flac"
# OGG
if raw_bytes[:4] == b"OggS":
return "ogg"
# M4A/MP4 audio
if raw_bytes[4:8] == b"ftyp":
return "mp4"
return None
class MailMessage(models.Model):
_inherit = "mail.message"
LLM_XMLIDS = (
"llm.mt_tool",
"llm.mt_user",
"llm.mt_assistant",
"llm.mt_system",
)
llm_role = fields.Char(
string="LLM Role",
compute="_compute_llm_role",
store=True,
index=True, # Add index for better query performance
help="The LLM role for this message (user, assistant, tool, system)",
)
is_error = fields.Boolean(
string="Is Error Message",
default=False,
index=True,
help="Error messages are shown to users but excluded from LLM context",
)
body_json = fields.Json(
string="JSON Body",
help="JSON data for tool messages and other structured content",
)
@api.depends("subtype_id")
def _compute_llm_role(self):
"""Compute the LLM role for messages based on their subtype."""
id_to_role, _ = self.get_llm_roles()
for message in self:
if message.subtype_id and message.subtype_id.id in id_to_role:
message.llm_role = id_to_role[message.subtype_id.id]
else:
message.llm_role = False
@tools.ormcache()
def get_llm_roles(self):
"""Get cached mapping of LLM subtype IDs to clean role names and vice versa.
Returns:
tuple: (id_to_role_dict, role_to_id_dict) where:
- id_to_role_dict: {subtype_id: 'user', subtype_id: 'assistant', ...}
- role_to_id_dict: {'user': subtype_id, 'assistant': subtype_id, ...}
"""
id_to_role = {}
role_to_id = {}
for xmlid in self.LLM_XMLIDS:
subtype_id = self.env["ir.model.data"]._xmlid_to_res_id(
xmlid,
raise_if_not_found=False,
)
if subtype_id:
# Extract clean role name (e.g., 'user' from 'llm.mt_user')
role = xmlid.split(".")[-1][3:] # Remove 'mt_' prefix
id_to_role[subtype_id] = role
role_to_id[role] = subtype_id
return id_to_role, role_to_id
def get_llm_role(self):
"""Get the LLM role for this message (ensure_one).
DEPRECATED: Use the llm_role computed field instead.
Returns:
str or False: The role name ('user', 'assistant', 'tool', 'system') or False if not an LLM message
"""
self.ensure_one()
return self.llm_role
def is_llm_message(self):
"""Check if messages are LLM messages using the stored field."""
return {message: bool(message.llm_role) for message in self}
def is_llm_user_message(self):
"""Check if messages are LLM user messages using the stored field."""
return {message: message.llm_role == "user" for message in self}
def is_llm_assistant_message(self):
"""Check if messages are LLM assistant messages using the stored field."""
return {message: message.llm_role == "assistant" for message in self}
def is_llm_tool_message(self):
"""Check if messages are LLM tool messages using the stored field."""
return {message: message.llm_role == "tool" for message in self}
def is_llm_system_message(self):
"""Check if messages are LLM system messages using the stored field."""
return {message: message.llm_role == "system" for message in self}
def _check_llm_role(self, role):
"""Check if messages match a specific LLM role using the stored field.
Args:
role (str): The role name ('user', 'assistant', 'tool', 'system')
"""
return {message: message.llm_role == role for message in self}
def to_store_format(self):
"""Convert message to store format compatible with Odoo 18.0. Used by frontend js components"""
self.ensure_one()
from odoo.addons.mail.tools.discuss import Store
store = Store()
self._to_store(store)
result = store.get_result()
return result["mail.message"][0]
def _get_attachments_by_mimetype(self, mimetypes):
"""Get attachments filtered by mimetype.
Base method for DRY attachment extraction. Returns raw attachment records
filtered by the given mimetypes and having data.
Args:
mimetypes: Tuple of mimetype strings to filter by
Returns:
Filtered ir.attachment recordset
"""
self.ensure_one()
return self.attachment_ids.filtered(
lambda att: att.mimetype and att.mimetype in mimetypes and att.datas,
)
def _get_image_attachments(self):
"""Get image attachments with validated mimetype from magic bytes.
Returns list of dicts with mimetype (validated), data (base64), and name.
The mimetype is detected from the actual image content, not from Odoo's
stored mimetype, to ensure compatibility with strict API validators
like Anthropic Claude.
"""
images = []
for att in self._get_attachments_by_mimetype(SUPPORTED_IMAGE_MIMETYPES):
try:
raw_bytes = base64.b64decode(att.datas)
real_mimetype = _detect_image_mimetype(raw_bytes)
if real_mimetype:
if real_mimetype != att.mimetype:
_logger.debug(
"Image %s: correcting mimetype from %s to %s",
att.name,
att.mimetype,
real_mimetype,
)
images.append(
{
"mimetype": real_mimetype,
"data": att.datas.decode("utf-8"),
"name": att.name or "image",
},
)
else:
_logger.warning(
"Could not detect image type for %s, using stored mimetype %s",
att.name,
att.mimetype,
)
images.append(
{
"mimetype": att.mimetype,
"data": att.datas.decode("utf-8"),
"name": att.name or "image",
},
)
except (ValueError, TypeError) as e:
_logger.warning(
"Failed to process image attachment %s: %s",
att.name,
e,
)
return images
def _get_pdf_attachments(self):
"""Get PDF attachments as base64 data."""
return [
{
"mimetype": att.mimetype,
"data": att.datas.decode("utf-8"),
"name": att.name or "document.pdf",
}
for att in self._get_attachments_by_mimetype(PDF_MIMETYPES)
]
def _get_text_attachments(self):
"""Get text attachments with decoded content."""
texts = []
for att in self._get_attachments_by_mimetype(TEXT_MIMETYPES):
try:
raw_data = base64.b64decode(att.datas)
content = raw_data.decode("utf-8")
texts.append(
{
"mimetype": att.mimetype,
"content": content,
"name": att.name or "file.txt",
},
)
except (UnicodeDecodeError, ValueError) as e:
_logger.warning("Failed to decode text attachment %s: %s", att.name, e)
return texts
def _get_audio_attachments(self):
"""Get audio attachments with detected format for OpenAI API.
Returns list of dicts with format (wav, mp3, etc.), data (base64), and name.
Only for use with OpenAI gpt-4o-audio-preview models.
"""
audios = []
for att in self._get_attachments_by_mimetype(AUDIO_MIMETYPES):
try:
raw_bytes = base64.b64decode(att.datas)
audio_format = _detect_audio_format(raw_bytes)
if audio_format:
audios.append(
{
"format": audio_format,
"data": att.datas.decode("utf-8"),
"name": att.name or "audio",
},
)
else:
_logger.warning("Could not detect audio format for %s", att.name)
except (ValueError, TypeError) as e:
_logger.warning(
"Failed to process audio attachment %s: %s",
att.name,
e,
)
return audios
def _get_unsupported_attachments(
self,
provider_service,
is_multimodal=False,
):
"""Get list of attachments not supported by the current provider/model.
This method supports both single messages and recordsets, which is essential
for validating the ENTIRE conversation context before sending to the LLM.
Why check the full context?
---------------------------
When a user switches LLM models mid-conversation, previously valid attachments
may become incompatible. For example:
- User sends image to multimodal model → works
- User switches to text-only model → images in context unsupported
Supported file types:
- Images (JPEG, PNG, GIF, WebP) - requires multimodal model
- PDFs - requires multimodal model
- Text files (plain text, markdown, code) - always supported
Unsupported (user is notified, file skipped):
- Audio files - not yet implemented
- Video files - not supported by any LLM
- Office documents (Word, Excel, PPT) - must convert to PDF first
The notification message has is_error=True so it's excluded from
future LLM context, allowing the conversation to continue normally.
Args:
provider_service: The provider service name (e.g., 'anthropic', 'openai')
is_multimodal: Whether the model supports images/PDFs
Returns:
List of dicts with name, mimetype, and reason for each unsupported attachment
"""
unsupported = []
for message in self:
for att in message.attachment_ids:
if not att.mimetype or not att.datas:
continue
mimetype = att.mimetype
reason = None
# Video - never supported by any LLM
if mimetype in VIDEO_MIMETYPES:
reason = _("Video files are not supported")
# Office documents - must convert to PDF
elif mimetype in OFFICE_MIMETYPES:
reason = _("Office documents must be converted to PDF first")
# Audio - not yet implemented
elif mimetype in AUDIO_MIMETYPES:
reason = _("Audio files are not yet supported")
# Images/PDFs - only with multimodal models
elif mimetype in IMAGE_MIMETYPES and not is_multimodal:
reason = _("This model does not support images")
elif mimetype in PDF_MIMETYPES and not is_multimodal:
reason = _("This model does not support PDFs")
if reason:
unsupported.append(
{
"name": att.name,
"mimetype": mimetype,
"reason": reason,
},
)
return unsupported
+3
View File
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
@@ -0,0 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_llm_provider_user,llm.provider.user,model_llm_provider,base.group_user,1,0,0,0
access_llm_provider_manager,llm.provider.manager,model_llm_provider,group_llm_manager,1,1,1,1
access_llm_model_user,llm.model.user,model_llm_model,base.group_user,1,0,0,0
access_llm_model_manager,llm.model.manager,model_llm_model,group_llm_manager,1,1,1,1
access_llm_publisher_user,llm.publisher.user,model_llm_publisher,base.group_user,1,0,0,0
access_llm_publisher_manager,llm.publisher.manager,model_llm_publisher,group_llm_manager,1,1,1,1
access_llm_fetch_models_wizard_manager,llm.fetch.models.wizard.manager,model_llm_fetch_models_wizard,group_llm_manager,1,1,1,1
access_llm_fetch_models_line_manager,llm.fetch.models.line.manager,model_llm_fetch_models_line,group_llm_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_llm_provider_user llm.provider.user model_llm_provider base.group_user 1 0 0 0
3 access_llm_provider_manager llm.provider.manager model_llm_provider group_llm_manager 1 1 1 1
4 access_llm_model_user llm.model.user model_llm_model base.group_user 1 0 0 0
5 access_llm_model_manager llm.model.manager model_llm_model group_llm_manager 1 1 1 1
6 access_llm_publisher_user llm.publisher.user model_llm_publisher base.group_user 1 0 0 0
7 access_llm_publisher_manager llm.publisher.manager model_llm_publisher group_llm_manager 1 1 1 1
8 access_llm_fetch_models_wizard_manager llm.fetch.models.wizard.manager model_llm_fetch_models_wizard group_llm_manager 1 1 1 1
9 access_llm_fetch_models_line_manager llm.fetch.models.line.manager model_llm_fetch_models_line group_llm_manager 1 1 1 1
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Create a new Application Category for LLM -->
<record id="module_category_llm" model="ir.module.category">
<field name="name">LLM</field>
<field name="description">Manage access to LLM features</field>
<field name="sequence">25</field>
</record>
<!-- Update the LLM Manager group to use the new category -->
<record id="group_llm_manager" model="res.groups">
<field name="name">LLM Manager</field>
<field name="category_id" ref="module_category_llm" />
<field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
</record>
<!-- Make Settings/Admin group imply LLM Manager -->
<record id="base.group_system" model="res.groups">
<field name="implied_ids" eval="[(4, ref('group_llm_manager'))]" />
</record>
<!-- Record Rules -->
<!-- LLM Provider Rules -->
<record id="llm_provider_rule_all" model="ir.rule">
<field name="name">LLM Providers: read-only for all users</field>
<field name="model_id" ref="model_llm_provider" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_read" eval="True" />
<field name="perm_write" eval="False" />
<field name="perm_create" eval="False" />
<field name="perm_unlink" eval="False" />
<field name="groups" eval="[(4, ref('base.group_user'))]" />
</record>
<record id="llm_provider_rule_manager" model="ir.rule">
<field name="name">LLM Providers: full access for managers</field>
<field name="model_id" ref="model_llm_provider" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_read" eval="True" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="True" />
<field name="groups" eval="[(4, ref('group_llm_manager'))]" />
</record>
<!-- LLM Model Rules -->
<record id="llm_model_rule_all" model="ir.rule">
<field name="name">LLM Models: read-only for all users</field>
<field name="model_id" ref="model_llm_model" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_read" eval="True" />
<field name="perm_write" eval="False" />
<field name="perm_create" eval="False" />
<field name="perm_unlink" eval="False" />
<field name="groups" eval="[(4, ref('base.group_user'))]" />
</record>
<record id="llm_model_rule_manager" model="ir.rule">
<field name="name">LLM Models: full access for managers</field>
<field name="model_id" ref="model_llm_model" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="perm_read" eval="True" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="True" />
<field name="groups" eval="[(4, ref('group_llm_manager'))]" />
</record>
</odoo>
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 70 70">
<defs>
<path id="icon-a" d="M4,0 C36.4160122,0 58.0836068,0 65,0 C69,0 70,1 70,5 C70,43.0488877 70,62.4235458 70,65 C70,69 69,70 65,70 C61,70 9,70 4,70 C1,70 0,69 0,65 C0,62.4676575 0,41.8005206 0,5 C0,1 1,0 4,0 Z"/>
<linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#9370DB"/>
<stop offset="100%" stop-color="#663399"/>
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd">
<mask id="icon-b" fill="#fff">
<use xlink:href="#icon-a"/>
</mask>
<g mask="url(#icon-b)">
<rect width="70" height="70" fill="url(#icon-c)"/>
<path fill="#FFF" fill-opacity=".383" d="M4,1.8 L65,1.8 C67.6666667,1.8 69.3333333,1.13333333 70,-0.2 C70,2.46666667 70,3.46666667 70,2.8 L0,2.8 C0,3.46666667 0,2.46666667 0,-0.2 C0.666666667,1.13333333 2,1.8 4,1.8 Z" transform="matrix(1 0 0 -1 0 2.8)"/>
<path fill="#000" fill-opacity=".3" d="M39.8461538,54 L4,54 C2,54 0,53.8544474 0,49.9245283 L0,25.9563572 L19.6596494,1.0710105 L47,0 L49.9211999,35.2849068 L39.8461538,54 Z" opacity=".324" transform="translate(5 15)"/>
<path fill="#000" fill-opacity=".383" d="M4,4 L65,4 C67.6666667,4 69.3333333,3 70,1 C70,3.66666667 70,5 70,5 L0,5 C0,5 0,3.66666667 0,1 C0.666666667,3 2,4 4,4 Z" transform="translate(0 65)"/>
<path fill="#000" fill-rule="evenodd" clip-rule="evenodd" opacity=".3" transform="translate(10 5) scale(0.3)" d="M129.49 114.51C129.121 116.961 128.187 119.293 126.762 121.322C125.337 123.351 123.461 125.021 121.28 126.2C120.676 126.535 120.043 126.816 119.39 127.04C120.22 138.04 102.74 142.04 93.32 139.42L96.82 151.66L87.82 151.98L72.07 129.43C66.76 130.93 60.49 131.65 56.44 125.15C56.0721 124.553 55.7382 123.935 55.44 123.3C54.4098 123.51 53.3614 123.617 52.31 123.62C49.31 123.62 44.31 122.72 41.77 120.96C39.7563 119.625 38.1588 117.75 37.16 115.55C31.75 116.29 27.16 115.02 24.16 111.88C20.36 107.97 19.28 101.51 21.26 94.58C23.87 85.33 31.81 74.91 47.59 71C48.9589 69.2982 50.5972 67.8322 52.44 66.66C62.35 60.31 78.44 59.76 90.65 65.79C95.3836 64.9082 100.27 65.376 104.75 67.14C113.53 70.43 119.91 77.31 121.11 84.3C123.487 85.5317 125.433 87.4568 126.69 89.82C129.32 94.76 129.69 99.71 127.92 103.71C129.587 107.049 130.138 110.835 129.49 114.51ZM123.01 109.31C121.612 110.048 120.056 110.434 118.475 110.434C116.894 110.434 115.338 110.048 113.94 109.31L114.67 104.46C117.75 104.76 120.26 103.8 121.57 101.83C123.04 99.64 122.81 96.39 120.95 92.9C118.87 88.99 114.38 88.37 111.89 88.34H111.73C105.49 88.34 99.13 91.89 96.56 96.52L92.82 94.73C93.5553 92.3449 94.8046 90.15 96.48 88.3C95.0376 87.0754 93.9474 85.4887 93.3217 83.703C92.696 81.9173 92.5574 79.9971 92.92 78.14L96.61 77.8C96.7789 79.302 97.4 80.7172 98.3911 81.8583C99.3822 82.9994 100.697 83.8125 102.16 84.19C105.238 82.8161 108.58 82.1335 111.95 82.19C112.43 82.19 112.89 82.24 113.36 82.27C110.969 78.0312 107.18 74.7545 102.64 73C91.56 68.7 84.09 75.37 82.38 77.67C78.26 83.19 80.9 88.41 82.91 91.8L79.61 94.8C76.736 92.314 74.8075 88.9127 74.15 85.17C69.92 86.44 64.24 86.17 61.06 80.74L64.06 78.68C67.43 81.2 72.78 80.98 75.32 77.87C75.9252 76.4949 76.6905 75.1959 77.6 74C79.044 72.093 80.7864 70.4316 82.76 69.08C74.47 66.82 62.76 67.19 55.68 71.73C53.7668 72.841 52.192 74.4517 51.1244 76.3895C50.0569 78.3274 49.5368 80.5192 49.62 82.73C49.62 86.3 52.42 91.94 56.19 92.82L54 97.07C51.5946 96.5129 49.4109 95.2487 47.73 93.44L44.48 97.58L41.23 96L44.41 87.68C43.8904 86.064 43.624 84.3774 43.62 82.68C43.628 81.3361 43.7687 79.9963 44.04 78.68C34.04 82.81 29.1 89.68 27.29 95.96C25.9 100.79 26.44 105.15 28.72 107.49C30.53 109.35 33.3 109.79 35.91 109.62L42.91 104.17L45.21 106.11L43.13 112.93C44.22 116.4 47.79 118.19 54.3 116.93C54.6375 114.169 55.7272 111.554 57.45 109.37C58.7133 107.552 60.3846 106.056 62.33 105L65.75 95.79L69.17 95.64L68.8 103.19C74.55 102.6 80.98 103.77 86.97 102.87L88.07 106.87C79.29 110.93 70.3 104.31 62.15 113.04C59.22 116.18 60.34 118.91 62.15 121.66C64.76 125.59 69.66 123.23 74.67 121.66C82.26 119.34 87.77 117.66 98.16 118.51C95.68 113.8 95.92 108.11 99.24 101.85L104.13 103.78C100.7 111.69 103.91 116.27 106.13 118.29C109.56 121.41 114.72 122.35 118.13 120.47C119.436 119.749 120.559 118.737 121.412 117.513C122.265 116.289 122.825 114.885 123.05 113.41C123.275 112.051 123.258 110.663 123 109.31H123.01Z"/>
<path fill="#FFF" fill-rule="evenodd" clip-rule="evenodd" transform="translate(10 3) scale(0.3)" d="M129.49 114.51C129.121 116.961 128.187 119.293 126.762 121.322C125.337 123.351 123.461 125.021 121.28 126.2C120.676 126.535 120.043 126.816 119.39 127.04C120.22 138.04 102.74 142.04 93.32 139.42L96.82 151.66L87.82 151.98L72.07 129.43C66.76 130.93 60.49 131.65 56.44 125.15C56.0721 124.553 55.7382 123.935 55.44 123.3C54.4098 123.51 53.3614 123.617 52.31 123.62C49.31 123.62 44.31 122.72 41.77 120.96C39.7563 119.625 38.1588 117.75 37.16 115.55C31.75 116.29 27.16 115.02 24.16 111.88C20.36 107.97 19.28 101.51 21.26 94.58C23.87 85.33 31.81 74.91 47.59 71C48.9589 69.2982 50.5972 67.8322 52.44 66.66C62.35 60.31 78.44 59.76 90.65 65.79C95.3836 64.9082 100.27 65.376 104.75 67.14C113.53 70.43 119.91 77.31 121.11 84.3C123.487 85.5317 125.433 87.4568 126.69 89.82C129.32 94.76 129.69 99.71 127.92 103.71C129.587 107.049 130.138 110.835 129.49 114.51ZM123.01 109.31C121.612 110.048 120.056 110.434 118.475 110.434C116.894 110.434 115.338 110.048 113.94 109.31L114.67 104.46C117.75 104.76 120.26 103.8 121.57 101.83C123.04 99.64 122.81 96.39 120.95 92.9C118.87 88.99 114.38 88.37 111.89 88.34H111.73C105.49 88.34 99.13 91.89 96.56 96.52L92.82 94.73C93.5553 92.3449 94.8046 90.15 96.48 88.3C95.0376 87.0754 93.9474 85.4887 93.3217 83.703C92.696 81.9173 92.5574 79.9971 92.92 78.14L96.61 77.8C96.7789 79.302 97.4 80.7172 98.3911 81.8583C99.3822 82.9994 100.697 83.8125 102.16 84.19C105.238 82.8161 108.58 82.1335 111.95 82.19C112.43 82.19 112.89 82.24 113.36 82.27C110.969 78.0312 107.18 74.7545 102.64 73C91.56 68.7 84.09 75.37 82.38 77.67C78.26 83.19 80.9 88.41 82.91 91.8L79.61 94.8C76.736 92.314 74.8075 88.9127 74.15 85.17C69.92 86.44 64.24 86.17 61.06 80.74L64.06 78.68C67.43 81.2 72.78 80.98 75.32 77.87C75.9252 76.4949 76.6905 75.1959 77.6 74C79.044 72.093 80.7864 70.4316 82.76 69.08C74.47 66.82 62.76 67.19 55.68 71.73C53.7668 72.841 52.192 74.4517 51.1244 76.3895C50.0569 78.3274 49.5368 80.5192 49.62 82.73C49.62 86.3 52.42 91.94 56.19 92.82L54 97.07C51.5946 96.5129 49.4109 95.2487 47.73 93.44L44.48 97.58L41.23 96L44.41 87.68C43.8904 86.064 43.624 84.3774 43.62 82.68C43.628 81.3361 43.7687 79.9963 44.04 78.68C34.04 82.81 29.1 89.68 27.29 95.96C25.9 100.79 26.44 105.15 28.72 107.49C30.53 109.35 33.3 109.79 35.91 109.62L42.91 104.17L45.21 106.11L43.13 112.93C44"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

@@ -0,0 +1,844 @@
<!-- Hero Section -->
<section style="padding:4rem 0 3rem">
<div class="container">
<div class="text-center" style="max-width:800px; margin:0 auto">
<div style="display:inline-flex; padding:0.5rem 1.25rem; border-radius:50px; margin-bottom:2rem">
<i class="fa fa-brain" style="color:#71639e; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#71639e; font-weight:600; font-size:0.9rem">AI Foundation</span>
</div>
<h1 style="font-size:3rem; font-weight:800; color:#212529; margin-bottom:1.5rem; line-height:1.2">
LLM Integration Base
</h1>
<p style="font-size:1.25rem; color:#6c757d; margin-bottom:1rem; line-height:1.6">
Connect your Odoo with leading AI providers like OpenAI, Anthropic, Grok, DeepSeek &amp; more.
</p>
<p style="font-size:1rem; color:#868e96; margin-bottom:2rem">
The foundation for integrating <strong style="color:#495057">Large Language Models</strong> with your Odoo instance
</p>
<div class="d-flex justify-content-center flex-wrap">
<div class="d-flex align-items-center" style="padding:0.75rem 1.5rem; border-radius:12px; margin:0.5rem">
<i class="fa fa-plug" style="color:#71639e; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#5b4c7d; font-weight:600">MCP Server</span>
</div>
<div class="d-flex align-items-center" style="padding:0.75rem 1.5rem; border-radius:12px; margin:0.5rem">
<i class="fa fa-wrench" style="color:#17a2b8; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#0c5460; font-weight:600">60+ AI Tools</span>
</div>
<div class="d-flex align-items-center" style="padding:0.75rem 1.5rem; border-radius:12px; margin:0.5rem">
<i class="fa fa-comments" style="color:#28a745; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#155724; font-weight:600">Chat &amp; Embeddings</span>
</div>
<div class="d-flex align-items-center" style="padding:0.75rem 1.5rem; border-radius:12px; margin:0.5rem">
<i class="fa fa-github" style="color:#495057; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#343a40; font-weight:600">Open Source</span>
</div>
</div>
</div>
</div>
</section>
<div class="container my-5">
<!-- Overview Section -->
<section class="mb-5">
<h2 class="text-center mb-3" style="font-size:2.5rem; font-weight:700; color:#71639e">What is LLM Integration Base?</h2>
<p class="text-center mb-5" style="font-size:1.2rem; color:#6c757d; font-weight:500">Your unified AI framework for Odoo</p>
<div class="row justify-content-center mb-5">
<div class="col-lg-10">
<div class="card border-0 shadow-sm" style="border-radius:16px; padding:2.5rem">
<p style="font-size:1.15rem; color:#495057; line-height:1.9; margin-bottom:0; text-align:center">
LLM Integration Base provides a unified framework for connecting various AI providers and models with your Odoo instance. This module serves as the foundation for building AI-powered features across your Odoo applications, supporting chat completions, text embeddings, and model management.
</p>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Architecture Diagram Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Architecture</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">How all the modules fit together</p>
<div class="row justify-content-center">
<div class="col-lg-11">
<div class="card border-0 shadow-sm" style="border-radius:16px; padding:2.5rem 1.5rem">
<!-- Top row: External AI Clients + Odoo AI Chat -->
<div class="row g-3 mb-2">
<div class="col-md-5">
<p style="font-weight:700; color:#6c757d; font-size:0.8rem; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.75rem">External AI Clients</p>
<div class="row g-2">
<div class="col-6">
<div class="text-center" style="border:2px solid #dee2e6; border-radius:10px; padding:0.75rem 0.5rem">
<i class="fa fa-desktop" style="font-size:1.1rem; color:#71639e; display:block; margin-bottom:0.25rem"></i>
<strong style="font-size:0.8rem; color:#212529; display:block">Claude Desktop</strong>
<span style="font-size:0.7rem; color:#868e96">Cursor &middot; Windsurf</span>
</div>
</div>
<div class="col-6">
<div class="text-center" style="border:2px solid #dee2e6; border-radius:10px; padding:0.75rem 0.5rem">
<i class="fa fa-terminal" style="font-size:1.1rem; color:#71639e; display:block; margin-bottom:0.25rem"></i>
<strong style="font-size:0.8rem; color:#212529; display:block">Claude Code</strong>
<span style="font-size:0.7rem; color:#868e96">Codex CLI</span>
</div>
</div>
</div>
</div>
<div class="col-md-2"></div>
<div class="col-md-5">
<p class="text-end" style="font-weight:700; color:#6c757d; font-size:0.8rem; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.75rem">Odoo AI Chat</p>
<div class="row g-2">
<div class="col-6">
<div class="text-center" style="border:2px solid #dee2e6; border-radius:10px; padding:0.75rem 0.5rem">
<strong style="font-size:0.8rem; color:#212529">llm_assistant</strong>
</div>
</div>
<div class="col-6">
<div class="text-center" style="border:2px solid #dee2e6; border-radius:10px; padding:0.75rem 0.5rem">
<strong style="font-size:0.8rem; color:#212529">llm_thread</strong>
</div>
</div>
</div>
</div>
</div>
<!-- Vertical lines down + MCP Protocol label -->
<div class="row">
<div class="col-md-5 text-center">
<div style="width:2px; height:1.5rem; background-color:#71639e; margin:0 auto"></div>
<span style="font-size:0.75rem; font-weight:700; color:#71639e; background-color:#f0ecf7; padding:0.2rem 0.75rem; border-radius:4px; display:inline-block">MCP Protocol</span>
<div style="width:2px; height:1rem; background-color:#71639e; margin:0 auto"></div>
<div style="width:0; height:0; border-left:6px solid transparent; border-right:6px solid transparent; border-top:8px solid #71639e; margin:0 auto"></div>
</div>
<div class="col-md-2"></div>
<div class="col-md-5 text-center">
<div style="width:2px; height:3rem; background-color:#adb5bd; margin:0 auto"></div>
<div style="width:0; height:0; border-left:6px solid transparent; border-right:6px solid transparent; border-top:8px solid #adb5bd; margin:0 auto"></div>
</div>
</div>
<!-- Middle row: MCP Server + arrow + LLM Core -->
<div class="row g-0 align-items-stretch mb-3">
<div class="col-md-4">
<div style="border:2px solid #71639e; border-radius:12px; padding:1.25rem 1rem; text-align:center; height:100%">
<strong style="font-size:1.05rem; color:#71639e; display:block; margin-bottom:0.25rem">llm_mcp_server</strong>
<span style="font-size:0.85rem; color:#6c757d">MCP Server for Odoo</span>
</div>
</div>
<div class="col-md-1 d-flex align-items-center justify-content-center" style="padding:0">
<div style="height:2px; background-color:#71639e; flex:1"></div>
<div style="width:0; height:0; border-top:7px solid transparent; border-bottom:7px solid transparent; border-left:10px solid #71639e"></div>
</div>
<div class="col-md-7">
<div style="border:3px solid #71639e; border-radius:12px; padding:1.25rem 1rem; text-align:center; background-color:#f9f8fc">
<span style="font-size:1.15rem; color:#71639e; font-weight:800; display:block; margin-bottom:0.4rem">&#9733; llm (This Module) &#9733;</span>
<span style="font-size:0.8rem; color:#495057; display:block; line-height:1.6">Provider Abstraction &middot; Model Management<br>Enhanced mail.message &middot; Security Framework</span>
</div>
</div>
</div>
<!-- Down arrow from llm -->
<div class="text-center" style="margin:0">
<div style="width:2px; height:2rem; background-color:#adb5bd; margin:0 auto"></div>
<div style="width:0; height:0; border-left:6px solid transparent; border-right:6px solid transparent; border-top:8px solid #adb5bd; margin:0 auto"></div>
</div>
<!-- Child modules row -->
<div class="row g-2 mb-3">
<div class="col-md-4">
<div style="border:2px solid #71639e; border-radius:10px; padding:1rem; text-align:center; height:100%">
<strong style="font-size:0.95rem; color:#71639e; display:block; margin-bottom:0.35rem">llm_tool</strong>
<span style="font-size:0.8rem; color:#6c757d; display:block">Tool Framework</span>
<span style="font-size:0.8rem; color:#6c757d; display:block">+ Generic CRUD Tools</span>
</div>
</div>
<div class="col-md-4">
<div style="border:2px solid #dee2e6; border-radius:10px; padding:1rem; text-align:center; height:100%">
<strong style="font-size:0.95rem; color:#495057; display:block; margin-bottom:0.35rem">AI Providers</strong>
<span style="font-size:0.8rem; color:#6c757d; display:block">llm_openai, llm_ollama</span>
<span style="font-size:0.8rem; color:#6c757d; display:block">llm_mistral, ...</span>
</div>
</div>
<div class="col-md-4">
<div style="border:2px solid #dee2e6; border-radius:10px; padding:1rem; text-align:center; height:100%">
<strong style="font-size:0.95rem; color:#495057; display:block; margin-bottom:0.35rem">Infrastructure</strong>
<span style="font-size:0.8rem; color:#6c757d; display:block">llm_store</span>
<span style="font-size:0.8rem; color:#6c757d; display:block">llm_generate</span>
</div>
</div>
</div>
<!-- Down arrow from llm_tool -->
<div class="row">
<div class="col-md-4 text-center">
<div style="width:2px; height:1.5rem; background-color:#71639e; margin:0 auto"></div>
<div style="width:0; height:0; border-left:6px solid transparent; border-right:6px solid transparent; border-top:8px solid #71639e; margin:0 auto"></div>
</div>
<div class="col-md-8"></div>
</div>
<!-- Domain-Specific Tool Packs -->
<div class="row">
<div class="col-lg-9 col-md-10">
<div style="border:2px solid #71639e; border-radius:12px; padding:1.25rem; background-color:#f9f8fc">
<strong style="font-size:1rem; color:#71639e; display:block; margin-bottom:0.75rem; text-align:center">Domain-Specific Tool Packs</strong>
<div class="row">
<div class="col-6" style="font-size:0.8rem; color:#495057; line-height:1.9">
<strong>llm_tool_account</strong> &middot; 18 accounting tools<br>
<strong>llm_tool_mis_builder</strong> &middot; 44 MIS reporting tools
</div>
<div class="col-6" style="font-size:0.8rem; color:#495057; line-height:1.9">
<strong>llm_tool_knowledge</strong> &middot; RAG search tools<br>
<strong>llm_tool_ocr_mistral</strong> &middot; OCR via Mistral vision
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- MCP Server Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Connect External AI Clients via MCP</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Use Claude Desktop, Claude Code, Cursor, Codex CLI and more with your Odoo data</p>
<div class="row justify-content-center mb-4">
<div class="col-lg-10">
<div class="card border-0 shadow-sm" style="border-radius:16px; padding:2.5rem">
<p style="font-size:1.15rem; color:#495057; line-height:1.9; margin-bottom:1.5rem; text-align:center">
The <strong>LLM MCP Server</strong> module exposes all your Odoo tools to external AI clients via the <strong>Model Context Protocol (MCP)</strong>. Install it, generate an API key from your profile, and paste the ready-made config into your AI client. That&apos;s it &mdash; your AI can now read, create, update, and analyze Odoo data with your permissions.
</p>
<div class="row g-3">
<div class="col-sm-4">
<div class="text-center" style="padding:1.25rem; border-radius:12px; background-color:#f8f9fa">
<i class="fa fa-desktop" style="font-size:2rem; color:#71639e; margin-bottom:0.75rem; display:block"></i>
<strong style="color:#212529; display:block; margin-bottom:0.25rem">Claude Desktop</strong>
<span style="color:#6c757d; font-size:0.85rem">Chat with Odoo data</span>
</div>
</div>
<div class="col-sm-4">
<div class="text-center" style="padding:1.25rem; border-radius:12px; background-color:#f8f9fa">
<i class="fa fa-terminal" style="font-size:2rem; color:#71639e; margin-bottom:0.75rem; display:block"></i>
<strong style="color:#212529; display:block; margin-bottom:0.25rem">Claude Code &amp; Codex</strong>
<span style="color:#6c757d; font-size:0.85rem">AI-powered dev workflow</span>
</div>
</div>
<div class="col-sm-4">
<div class="text-center" style="padding:1.25rem; border-radius:12px; background-color:#f8f9fa">
<i class="fa fa-code" style="font-size:2rem; color:#71639e; margin-bottom:0.75rem; display:block"></i>
<strong style="color:#212529; display:block; margin-bottom:0.25rem">Cursor &amp; VS Code</strong>
<span style="color:#6c757d; font-size:0.85rem">IDE integration</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Tool Packs Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">AI Tool Packs for Odoo</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Give AI deep domain knowledge with purpose-built tool packs</p>
<div class="row g-4 mb-4">
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; background-color:#e8f5e9; margin-right:1rem">
<i class="fa fa-calculator" style="font-size:24px; color:#2e7d32"></i>
</div>
<div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Tool Account</h4>
<span style="color:#2e7d32; font-weight:600; font-size:0.85rem">18 tools</span>
</div>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Trial balance, journal entries, reconciliation, payments, tax reports, P&amp;L, cash position, and period close. Built for CPAs and finance teams.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; background-color:#e3f2fd; margin-right:1rem">
<i class="fa fa-bar-chart" style="font-size:24px; color:#1565c0"></i>
</div>
<div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Tool MIS Builder</h4>
<span style="color:#1565c0; font-weight:600; font-size:0.85rem">44 tools</span>
</div>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Create KPIs, configure periods, compute reports, drill down into cells, compare periods, and analyze variances through natural language.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; background-color:#fff3e0; margin-right:1rem">
<i class="fa fa-book" style="font-size:24px; color:#e65100"></i>
</div>
<div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Tool Knowledge (RAG)</h4>
<span style="color:#e65100; font-weight:600; font-size:0.85rem">Semantic search</span>
</div>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Retrieve answers from your knowledge base with semantic search, source citations, and document context for grounded AI responses.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; background-color:#fce4ec; margin-right:1rem">
<i class="fa fa-file-text" style="font-size:24px; color:#c62828"></i>
</div>
<div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Tool OCR (Mistral)</h4>
<span style="color:#c62828; font-weight:600; font-size:0.85rem">Vision OCR</span>
</div>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Extract text from invoices, receipts, scanned documents, and images using Mistral AI vision models. No knowledge base required.</p>
</div>
</div>
</div>
</div>
<!-- Built-in generic tools callout -->
<div class="card border-0 shadow-sm" style="border-radius:16px; background-color:#f8f9fa">
<div class="card-body p-4">
<div class="d-flex align-items-start">
<div class="d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:12px; margin-right:1rem; flex-shrink:0; background-color:#71639e">
<i class="fa fa-wrench" style="font-size:24px; color:#ffffff"></i>
</div>
<div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0.5rem">6 Generic CRUD Tools Included</h4>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">
The <strong>LLM Tool</strong> base module ships with tools to <strong>retrieve</strong>, <strong>create</strong>, <strong>update</strong>, and <strong>delete</strong> records in any Odoo model, plus a <strong>model inspector</strong> and <strong>method executor</strong>. These work out of the box via both in-Odoo chat and the MCP server.
</p>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Key Features Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Key Features</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Everything you need to integrate AI into Odoo</p>
<div class="row g-4">
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-bottom:1.5rem">
<i class="fa fa-plug" style="font-size:28px; color:#71639e"></i>
</div>
<h3 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Multiple Provider Support</h3>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Connect with OpenAI, Anthropic Claude, Ollama, Replicate, and more AI providers through a unified interface.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-bottom:1.5rem">
<i class="fa fa-search" style="font-size:28px; color:#71639e"></i>
</div>
<h3 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Model Discovery</h3>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Automatically discover and import available models from connected providers with one click.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-bottom:1.5rem">
<i class="fa fa-building" style="font-size:28px; color:#71639e"></i>
</div>
<h3 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Publisher Management</h3>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Track model publishers, organizations, and their official status for better model organization.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-bottom:1.5rem">
<i class="fa fa-lock" style="font-size:28px; color:#71639e"></i>
</div>
<h3 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Secure API Storage</h3>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Safely store API keys and endpoint configurations for each provider with Odoo security.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-bottom:1.5rem">
<i class="fa fa-shield" style="font-size:28px; color:#71639e"></i>
</div>
<h3 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Role-Based Security</h3>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Control access to LLM features with dedicated security groups and record rules.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-bottom:1.5rem">
<i class="fa fa-wrench" style="font-size:28px; color:#71639e"></i>
</div>
<h3 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Tool Execution Framework</h3>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Enable AI models to execute functions through a standardized tool interface.</p>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Supported Providers Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Supported AI Providers</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Connect with leading AI providers</p>
<div class="row g-4">
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:12px; margin:0 auto 1.5rem">
<i class="fa fa-circle-o" style="font-size:32px; color:#ffffff"></i>
</div>
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">OpenAI</h4>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">GPT-4o, GPT-4, GPT-3.5 Turbo, and text-embedding models for chat and embeddings.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:12px; margin:0 auto 1.5rem">
<i class="fa fa-diamond" style="font-size:32px; color:#ffffff"></i>
</div>
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Anthropic</h4>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Claude 3 Opus, Sonnet, and Haiku models for thoughtful, helpful responses.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:12px; margin:0 auto 1.5rem">
<i class="fa fa-server" style="font-size:32px; color:#ffffff"></i>
</div>
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Ollama</h4>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Run Llama, Mistral, Vicuna and other open-source models locally with full privacy.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:12px; margin:0 auto 1.5rem">
<i class="fa fa-cloud" style="font-size:32px; color:#ffffff"></i>
</div>
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Replicate</h4>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Access diverse open-source models hosted on the Replicate platform.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:12px; margin:0 auto 1.5rem">
<i class="fa fa-bolt" style="font-size:32px; color:#ffffff"></i>
</div>
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">DeepSeek</h4>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">DeepSeek-Coder and DeepSeek-Chat models for advanced reasoning.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:12px; margin:0 auto 1.5rem">
<i class="fa fa-star" style="font-size:32px; color:#ffffff"></i>
</div>
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Grok</h4>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">xAI&apos;s Grok models for witty and informative AI responses.</p>
</div>
</div>
</div>
</div>
<!-- OpenAI-Compatible Notice -->
<div class="card border-0 shadow-sm mt-5" style="border-radius:16px; background-color:#f8f9fa">
<div class="card-body p-4">
<div class="d-flex align-items-start">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:12px; margin-right:1rem; flex-shrink:0">
<i class="fa fa-lightbulb-o" style="font-size:24px; color:#ffffff"></i>
</div>
<div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0.5rem">Use Any OpenAI-Compatible Provider</h4>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">
Most AI providers now support the OpenAI API format. With the <strong>OpenAI Provider</strong> module, you can connect to <strong>Google Gemini</strong>, <strong>xAI Grok</strong>, <strong>DeepSeek</strong>, <strong>Mistral</strong>, <strong>Groq</strong>, <strong>Together AI</strong>, <strong>Fireworks AI</strong>, <strong>Perplexity</strong>, and many more &mdash; simply by changing the base URL. One module, dozens of providers!
</p>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Getting Started Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Getting Started</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Set up your AI integration in minutes</p>
<div class="row g-4">
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1.5rem">Installation</h4>
<ol style="color:#6c757d; padding-left:1.25rem; margin-bottom:0">
<li style="margin-bottom:0.75rem">Download the module from GitHub</li>
<li style="margin-bottom:0.75rem">Install the module in your Odoo instance</li>
<li style="margin-bottom:0">Verify dependencies are satisfied (mail, web)</li>
</ol>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1.5rem">Configuration</h4>
<ol style="color:#6c757d; padding-left:1.25rem; margin-bottom:0">
<li style="margin-bottom:0.75rem">Navigate to <strong>LLM &rarr; Configuration &rarr; Providers</strong></li>
<li style="margin-bottom:0.75rem">Create a new provider and select the service type</li>
<li style="margin-bottom:0.75rem">Enter your API key and base URL</li>
<li style="margin-bottom:0.75rem">Click &quot;Fetch Models&quot; to import available models</li>
<li style="margin-bottom:0">Set default models for chat and embedding</li>
</ol>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Technical Details -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Technical Details</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Requirements and dependencies</p>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1.5rem">Module Information</h4>
<div class="row g-3">
<div class="col-sm-6">
<div class="bg-light" style="padding:1rem; border-radius:8px">
<strong style="color:#495057">Dependencies:</strong>
<p style="margin:0.5rem 0 0 0; color:#6c757d">mail, web</p>
</div>
</div>
<div class="col-sm-6">
<div class="bg-light" style="padding:1rem; border-radius:8px">
<strong style="color:#495057">Category:</strong>
<p style="margin:0.5rem 0 0 0; color:#6c757d">Technical</p>
</div>
</div>
<div class="col-sm-6">
<div class="bg-light" style="padding:1rem; border-radius:8px">
<strong style="color:#495057">Version:</strong>
<p style="margin:0.5rem 0 0 0; color:#6c757d">18.0.1.5.0</p>
</div>
</div>
<div class="col-sm-6">
<div class="bg-light" style="padding:1rem; border-radius:8px">
<strong style="color:#495057">License:</strong>
<p style="margin:0.5rem 0 0 0; color:#6c757d">LGPL-3</p>
</div>
</div>
</div>
<div class="bg-light mt-4" style="padding:1.5rem; border-radius:12px">
<h5 style="font-weight:700; color:#212529; margin-bottom:0.75rem">Key Models</h5>
<ul style="color:#6c757d; margin-bottom:0; padding-left:1.25rem">
<li><strong>llm.provider</strong> &mdash; Manages connections to AI providers</li>
<li><strong>llm.model</strong> &mdash; Represents individual AI models</li>
<li><strong>llm.publisher</strong> &mdash; Tracks organizations that publish models</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Related Modules Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Related Modules</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Extend your AI capabilities with these modules</p>
<!-- MCP Server - Featured -->
<div class="row g-4 mb-4">
<div class="col-12">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_mcp_server" style="text-decoration:none">
<div class="card border-0 shadow-sm" style="border-radius:16px; border-left:4px solid #71639e !important">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-right:1rem; background-color:#71639e">
<i class="fa fa-plug" style="font-size:28px; color:#ffffff"></i>
</div>
<div>
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:0.25rem">LLM MCP Server</h4>
<span style="color:#71639e; font-weight:600; font-size:0.85rem">Connect Claude Desktop, Claude Code, Cursor, Codex CLI &amp; more</span>
</div>
</div>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.6; margin-bottom:0">Expose all your Odoo tools to external AI clients via the Model Context Protocol (MCP). One-click API key generation with ready-to-paste configurations. Each user connects with their own permissions.</p>
</div>
</div>
</a>
</div>
</div>
<!-- Tool Packs -->
<h5 class="mb-3" style="font-weight:700; color:#495057; padding-left:0.5rem">Tool Packs</h5>
<div class="row g-4 mb-4">
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_tool" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-wrench" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Tool</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Tool framework with 6 generic CRUD tools. Retrieve, create, update, delete records in any model.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_tool_account" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-calculator" style="font-size:24px; color:#2e7d32"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Tool Account</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">18 accounting tools: trial balance, journal entries, reconciliation, payments, tax reports, P&amp;L.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_tool_mis_builder" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-bar-chart" style="font-size:24px; color:#1565c0"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Tool MIS Builder</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">44 MIS reporting tools: KPIs, periods, report computation, drill-down, variance analysis.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_tool_knowledge" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-book" style="font-size:24px; color:#e65100"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Tool Knowledge</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">RAG tools for semantic search, knowledge retrieval, and source citations.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_tool_ocr_mistral" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-file-text" style="font-size:24px; color:#c62828"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">Mistral OCR Tool</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Extract text from invoices, receipts, and scanned documents via Mistral vision.</p>
</div>
</div>
</a>
</div>
</div>
<!-- Providers & Chat -->
<h5 class="mb-3" style="font-weight:700; color:#495057; padding-left:0.5rem">Providers &amp; Chat</h5>
<div class="row g-4">
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_openai" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-circle-o" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">OpenAI Provider</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Connect to OpenAI, Gemini, Grok, DeepSeek, and any OpenAI-compatible API.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_ollama" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-server" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">Ollama Provider</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Run open-source models locally with full privacy and no API costs.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_assistant" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-user-circle" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Assistant</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Create specialized AI assistants with custom prompts and personalities.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_thread" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-comments" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Thread</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Persistent chat conversations with streaming responses and history.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_knowledge" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-database" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Knowledge</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">RAG-powered knowledge base with semantic search and document indexing.</p>
</div>
</div>
</a>
</div>
</div>
</section>
</div>
<!-- Footer -->
<footer class="bg-primary" style="padding:3rem 2rem; border-radius:16px; margin:2rem 1rem">
<div class="container">
<div style="border-radius:12px; padding:2.5rem">
<div class="row align-items-center">
<div class="col-md-8">
<h4 style="font-weight:700; color:#ffffff; font-size:1.5rem; margin-bottom:0.75rem">LLM Integration Base</h4>
<p style="color:#f8f9fa; font-size:1rem; margin-bottom:1rem">The foundation for AI-powered Odoo</p>
<p style="color:#e9d5ff; margin-bottom:0.25rem">Developed by <strong style="color:#ffffff">Apexive Solutions LLC</strong></p>
<p style="color:#e0d4ec; font-size:0.9rem; margin-bottom:0">Licensed under LGPL-3 &bull; &copy; 2025 All rights reserved.</p>
</div>
<div class="col-md-4 mt-3 mt-md-0">
<div class="d-flex flex-column">
<div class="bg-white d-flex align-items-center justify-content-center" style="padding:0.875rem 1.5rem; border-radius:8px; color:#71639e; font-weight:600; margin-bottom:0.75rem">
<i class="fa fa-github" style="font-size:18px; margin-right:0.5rem"></i>
github.com/apexive/odoo-llm
</div>
<a href="mailto:hello@apexive.com" class="bg-white d-flex align-items-center justify-content-center" style="padding:0.875rem 1.5rem; border-radius:8px; text-decoration:none; color:#71639e; font-weight:600; margin-bottom:0">
<i class="fa fa-envelope" style="font-size:18px; margin-right:0.5rem"></i>
hello@apexive.com
</a>
</div>
</div>
</div>
</div>
</div>
</footer>
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Root Menu -->
<menuitem
id="menu_llm_root"
name="LLM"
web_icon="llm,static/description/icon.png"
sequence="100"
/>
<!-- Configuration Menu -->
<menuitem
id="menu_llm_config"
name="Configuration"
parent="menu_llm_root"
sequence="100"
/>
<!-- Provider Menu -->
<menuitem
id="menu_llm_provider"
name="Providers"
action="llm_provider_action"
parent="menu_llm_config"
sequence="10"
/>
<!-- Publisher Menu -->
<menuitem
id="menu_llm_publisher"
name="Publishers"
action="llm_publisher_action"
parent="menu_llm_config"
sequence="15"
/>
<!-- Model Menu -->
<menuitem
id="menu_llm_model"
name="Models"
action="llm_model_action"
parent="menu_llm_config"
sequence="20"
/>
</odoo>
@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Model Form View -->
<record id="llm_model_view_form" model="ir.ui.view">
<field name="name">llm.model.view.form</field>
<field name="model">llm.model</field>
<field name="arch" type="xml">
<form>
<header>
<button
name="action_open_fetch_this_model_wizard"
string="Fetch This Model Update"
type="object"
class="oe_highlight"
invisible="not id"
help="Fetch the latest information for this specific model from the provider."
/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button
name="toggle_active"
type="object"
class="oe_stat_button"
icon="fa-archive"
>
<field
name="active"
widget="boolean_toggle"
options="{'terminology': 'archive'}"
/>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" placeholder="Model Name" />
</h1>
</div>
<group>
<group>
<field name="provider_id" />
<field name="publisher_id" />
<field name="model_use" />
<field name="default" />
</group>
<group>
<field name="details" widget="json_editor" />
<field name="model_info" widget="json_editor" />
<field name="parameters" widget="text" />
<field name="template" widget="text" />
</group>
</group>
</sheet>
<chatter />
</form>
</field>
</record>
<!-- Model Tree View -->
<record id="llm_model_view_tree" model="ir.ui.view">
<field name="name">llm.model.view.tree</field>
<field name="model">llm.model</field>
<field name="arch" type="xml">
<list>
<field name="name" />
<field name="provider_id" />
<field name="model_use" />
<field name="default" />
<field name="active" invisible="1" />
</list>
</field>
</record>
<!-- Model Search View -->
<record id="llm_model_view_search" model="ir.ui.view">
<field name="name">llm.model.view.search</field>
<field name="model">llm.model</field>
<field name="arch" type="xml">
<search>
<field name="name" />
<field name="provider_id" />
<field name="model_use" />
<separator />
<filter
string="Default Models"
name="default"
domain="[('default', '=', True)]"
/>
<filter
string="Archived"
name="inactive"
domain="[('active', '=', False)]"
/>
<group expand="0" string="Group By">
<filter
string="Provider"
name="group_by_provider"
context="{'group_by': 'provider_id'}"
/>
<filter
string="Usage"
name="group_by_use"
context="{'group_by': 'model_use'}"
/>
</group>
</search>
</field>
</record>
<!-- Model Action -->
<record id="llm_model_action" model="ir.actions.act_window">
<field name="name">LLM Models</field>
<field name="res_model">llm.model</field>
<field name="view_mode">list,form</field>
<field name="context">{'active_test': False}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No models found!
</p>
<p>
Models will be automatically created when you configure a provider.
</p>
</field>
</record>
</odoo>
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Provider Form View -->
<record id="llm_provider_view_form" model="ir.ui.view">
<field name="name">llm.provider.view.form</field>
<field name="model">llm.provider</field>
<field name="arch" type="xml">
<form>
<header>
<button
name="action_fetch_models"
string="Fetch Models"
type="object"
class="oe_highlight"
/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button
name="toggle_active"
type="object"
class="oe_stat_button"
icon="fa-archive"
>
<field
name="active"
widget="boolean_toggle"
options="{'terminology': 'archive'}"
/>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" placeholder="Provider Name" />
</h1>
</div>
<group>
<group>
<field name="service" />
<field
name="company_id"
groups="base.group_multi_company"
/>
</group>
<group>
<field name="api_key" password="True" />
<field
name="api_base"
placeholder="http://localhost:11434"
/>
</group>
</group>
<notebook>
<page string="Models" name="models">
<field
name="model_ids"
context="{'active_test': False}"
>
<list editable="bottom">
<field name="name" />
<field name="model_use" />
<field name="default" />
<field name="active" />
</list>
</field>
</page>
</notebook>
</sheet>
<chatter />
</form>
</field>
</record>
<!-- Provider Action -->
<record id="llm_provider_action" model="ir.actions.act_window">
<field name="name">LLM Providers</field>
<field name="res_model">llm.provider</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first LLM provider!
</p>
<p>
Configure providers like OpenAI, Anthropic, or Ollama to enable AI features.
</p>
</field>
</record>
</odoo>
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Publisher Form View -->
<record id="llm_publisher_view_form" model="ir.ui.view">
<field name="name">llm.publisher.view.form</field>
<field name="model">llm.publisher</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="logo" widget="image" class="oe_avatar" />
<div class="oe_title">
<h1>
<field name="name" placeholder="Publisher Name" />
</h1>
</div>
<group>
<group>
<field name="official" />
<field name="frontier" />
<field name="model_count" />
</group>
<group>
<field name="meta" widget="json_editor" />
</group>
</group>
<notebook>
<page string="Description" name="description">
<field
name="description"
placeholder="Publisher description..."
/>
</page>
<page string="Models" name="models">
<field name="model_ids">
<list>
<field name="name" />
<field name="provider_id" />
<field name="model_use" />
<field name="default" />
<field name="active" />
</list>
</field>
</page>
</notebook>
</sheet>
<chatter />
</form>
</field>
</record>
<!-- Publisher Tree View -->
<record id="llm_publisher_view_tree" model="ir.ui.view">
<field name="name">llm.publisher.view.tree</field>
<field name="model">llm.publisher</field>
<field name="arch" type="xml">
<list>
<field
name="logo"
widget="image"
options="{'size': [40, 40]}"
/>
<field name="name" />
<field name="official" />
<field name="frontier" />
<field name="model_count" />
</list>
</field>
</record>
<!-- Publisher Search View -->
<record id="llm_publisher_view_search" model="ir.ui.view">
<field name="name">llm.publisher.view.search</field>
<field name="model">llm.publisher</field>
<field name="arch" type="xml">
<search>
<field name="name" />
<separator />
<filter
string="Official Publishers"
name="official"
domain="[('official', '=', True)]"
/>
<filter
string="Frontier AI"
name="frontier"
domain="[('frontier', '=', True)]"
/>
<group expand="0" string="Group By">
<filter
string="Official Status"
name="group_by_official"
context="{'group_by': 'official'}"
/>
<filter
string="Frontier Status"
name="group_by_frontier"
context="{'group_by': 'frontier'}"
/>
</group>
</search>
</field>
</record>
<!-- Publisher Action -->
<record id="llm_publisher_action" model="ir.actions.act_window">
<field name="name">LLM Publishers</field>
<field name="res_model">llm.publisher</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first LLM publisher!
</p>
<p>
Add publishers like OpenAI, Anthropic, Meta, Google, etc.
</p>
</field>
</record>
</odoo>
+1
View File
@@ -0,0 +1 @@
from . import fetch_models_wizard
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Wizard Form View -->
<record id="view_fetch_models_wizard" model="ir.ui.view">
<field name="name">llm.fetch.models.wizard.form</field>
<field name="model">llm.fetch.models.wizard</field>
<field name="arch" type="xml">
<form>
<sheet>
<!-- Header Info -->
<group>
<group>
<field name="provider_id" readonly="1" />
<field name="model_count" readonly="1" />
</group>
<group>
<field name="new_count" readonly="1" />
<field name="modified_count" readonly="1" />
</group>
</group>
<!-- Models List -->
<field
name="line_ids"
options="{'reload_on_button': false}"
>
<list editable="bottom" create="0" delete="0">
<!-- Selection -->
<field name="selected" widget="boolean_toggle" />
<!-- Basic Info -->
<field name="name" readonly="1" force_save="1" />
<field
name="model_use"
readonly="1"
force_save="1"
/>
<!-- Status with decoration -->
<field
name="status"
readonly="1"
force_save="1"
decoration-info="status=='existing'"
decoration-success="status=='new'"
decoration-warning="status=='modified'"
/>
<field name="existing_model_id" invisible="1" />
</list>
</field>
<!-- Helpful Note -->
<div class="text-muted mt-3">
<p>
<i class="fa fa-info-circle" />
Select the models you want to import or update. New models will be created, and existing ones will be updated if modified.
</p>
</div>
</sheet>
<footer>
<button
string="Import Selected Models"
name="action_confirm"
type="object"
class="btn-primary"
data-hotkey="q"
/>
<button
string="Cancel"
class="btn-secondary"
special="cancel"
data-hotkey="z"
/>
</footer>
</form>
</field>
</record>
</odoo>
@@ -0,0 +1,127 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class ModelLine(models.TransientModel):
_name = "llm.fetch.models.line"
_description = "LLM Model Import Line"
_rec_name = "name"
wizard_id = fields.Many2one(
"llm.fetch.models.wizard",
required=True,
ondelete="cascade",
)
name = fields.Char(
string="Model Name",
required=True,
)
model_use = fields.Selection(
selection="_get_available_model_usages",
required=True,
default="chat",
)
status = fields.Selection(
[
("new", "New"),
("existing", "Existing"),
("modified", "Modified"),
],
required=True,
default="new",
)
selected = fields.Boolean(default=True)
details = fields.Json()
existing_model_id = fields.Many2one("llm.model")
_sql_constraints = [
(
"unique_model_per_wizard",
"UNIQUE(wizard_id, name)",
"Each model can only be listed once per import.",
)
]
@api.model
def _get_available_model_usages(self):
return self.env["llm.model"]._get_available_model_usages()
class FetchModelsWizard(models.TransientModel):
_name = "llm.fetch.models.wizard"
_description = "Import LLM Models"
provider_id = fields.Many2one(
"llm.provider",
required=True,
readonly=True,
)
line_ids = fields.One2many(
"llm.fetch.models.line",
"wizard_id",
string="Models",
)
model_count = fields.Integer(
compute="_compute_model_count",
string="Models Found",
)
new_count = fields.Integer(
compute="_compute_model_count",
string="New Models",
)
modified_count = fields.Integer(
compute="_compute_model_count",
string="Modified Models",
)
@api.depends("line_ids", "line_ids.status")
def _compute_model_count(self):
"""Compute various model counts for display"""
for wizard in self:
wizard.model_count = len(wizard.line_ids)
wizard.new_count = len(
wizard.line_ids.filtered(lambda record: record.status == "new")
)
wizard.modified_count = len(
wizard.line_ids.filtered(lambda record: record.status == "modified")
)
def action_confirm(self):
"""Process selected models and create/update records"""
self.ensure_one()
Model = self.env["llm.model"]
selected_lines = self.line_ids.filtered(
lambda record: record.selected and record.name
)
if not selected_lines:
raise UserError(_("Please select at least one model to import."))
for line in selected_lines:
values = {
"name": line.name.strip(),
"provider_id": self.provider_id.id,
"model_use": line.model_use,
"details": line.details,
"active": True,
}
if line.existing_model_id:
line.existing_model_id.write(values)
else:
Model.create(values)
# Return success message
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("Success"),
"message": _(
"%d models have been imported/updated.", len(selected_lines)
),
"sticky": False,
"type": "success",
"next": {"type": "ir.actions.act_window_close"},
},
}
+169
View File
@@ -0,0 +1,169 @@
# Anthropic Provider for Odoo LLM Integration
This module integrates Anthropic's Claude API with the Odoo LLM framework, providing access to Claude models for chat, tool calling, and extended thinking capabilities.
**Module Type:** 🔧 Provider
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Used By (Any LLM Module) │
│ ┌─────────────┐ ┌───────────┐ ┌─────────────┐ ┌───────────┐ │
│ │llm_assistant│ │llm_thread │ │llm_knowledge│ │llm_generate│ │
│ └──────┬──────┘ └─────┬─────┘ └──────┬──────┘ └─────┬─────┘ │
└─────────┼───────────────┼───────────────┼───────────────┼───────┘
│ │ │ │
└───────────────┴───────┬───────┴───────────────┘
┌───────────────────────────────────────────────┐
│ ★ llm_anthropic (This Module) ★ │
│ Anthropic Provider │
│ Claude 4.5 │ Claude 4 │ Claude 3.x │ Vision │
└─────────────────────┬─────────────────────────┘
┌───────────────────────────────────────────────┐
│ llm │
│ (Core Base Module) │
└───────────────────────────────────────────────┘
```
## Installation
### What to Install
**For AI chat with Claude:**
```bash
odoo-bin -d your_db -i llm_assistant,llm_anthropic
```
### Auto-Installed Dependencies
- `llm` (core infrastructure)
- `llm_tool` (tool/function calling support)
### Alternative Providers
| Instead of Anthropic | Use | Best For |
| -------------------- | ---------- | --------------------- |
| `llm_openai` | OpenAI | GPT models, DALL-E |
| `llm_ollama` | Local AI | Privacy, no API costs |
| `llm_mistral` | Mistral AI | European, fast |
### Common Setups
| I want to... | Install |
| ------------------------ | ---------------------------------------- |
| Chat with Claude | `llm_assistant` + `llm_anthropic` |
| Claude + document search | Above + `llm_knowledge` + `llm_pgvector` |
| Claude + external tools | Above + `llm_mcp_server` |
## Features
- Connect to Anthropic API with proper authentication
- Support for all Claude models (4.5, 4, 3.x series)
- Tool/function calling capabilities
- Extended thinking support (Claude's reasoning mode)
- Streaming responses
- Multimodal (vision) capabilities for supported models
- Automatic model discovery
## Multimodal Support
Claude models support sending images and PDFs along with text messages.
### How to Use
1. Attach files (images, PDFs, text files) to your chat message
2. The module automatically:
- Converts images to base64 for Claude's vision API
- Sends PDFs as document blocks
- Includes text file contents in the message
### Supported Formats
| Type | Formats | Claude Format |
| ------ | --------------------------------- | ------------------------------------- |
| Images | JPEG, PNG, GIF, WebP | `type: "image"` with base64 source |
| PDFs | application/pdf | `type: "document"` with base64 source |
| Text | .txt, .md, .csv, .py, .json, etc. | Appended to message text |
### Example
```python
# Attach an image to the chat thread
thread.message_post(
body="What's in this image?",
attachment_ids=[(4, image_attachment.id)]
)
# Claude will analyze the image and respond
```
**Note:** Non-multimodal models (like older Claude versions) will skip images/PDFs automatically.
## Configuration
1. Install the module
2. Navigate to **LLM > Configuration > Providers**
3. Create a new provider and select "Anthropic" as the provider type
4. Enter your Anthropic API key
5. Click "Fetch Models" to import available models
## Supported Models
| Model Family | Models | Capabilities |
| ------------ | ------------------- | -------------------------------------- |
| Claude 4.5 | Opus, Sonnet, Haiku | Chat, Vision, Tools, Extended Thinking |
| Claude 4 | Opus, Sonnet | Chat, Vision, Tools |
| Claude 3.x | Opus, Sonnet, Haiku | Chat, Vision, Tools |
## Technical Details
This module extends the base LLM integration framework with Anthropic-specific implementations:
### Key Differences from OpenAI
| Aspect | OpenAI | Anthropic |
| ---------------- | ----------------------------------------- | ----------------------------------------- |
| System message | In messages array | Separate `system` parameter |
| Tool format | `{"type": "function", "function": {...}}` | `{"name", "description", "input_schema"}` |
| Response content | Single string | Array of content blocks |
| Tool results | `role: "tool"` | `role: "user"` + `type: "tool_result"` |
### Extended Thinking
Claude supports extended thinking mode, which allows the model to show its reasoning process:
```python
# Enable extended thinking in your assistant configuration
response = provider.chat(
messages=messages,
extended_thinking=True,
thinking_budget=10000 # tokens for reasoning
)
```
### Implemented Methods
- `anthropic_get_client()` - Initialize Anthropic client
- `anthropic_chat()` - Chat with tool calling and streaming support
- `anthropic_format_tools()` - Convert tools to Anthropic format
- `anthropic_format_messages()` - Format mail.message records
- `anthropic_models()` - List available Claude models
- `anthropic_normalize_prepend_messages()` - Handle prepend messages
## Dependencies
- `llm` (LLM Integration Base)
- `llm_tool` (Tool Calling Support)
- Python: `anthropic` package
## Contributors
- Crottolo <bo@fl1.cz> - Odoo 18.0 port with full tool calling and extended thinking support
## License
LGPL-3
@@ -0,0 +1 @@
from . import models
@@ -0,0 +1,34 @@
{
"name": "Anthropic LLM Integration",
"summary": "Anthropic Claude provider integration for LLM module",
"description": """
Implements Anthropic provider service for the LLM integration module.
Supports Claude models for chat, multimodal, and tool calling capabilities.
Features:
- Claude 4.5, 4, and 3.x model support
- Tool/function calling
- Extended thinking support
- Streaming responses
- Multimodal (vision) capabilities
""",
"author": "Apexive Solutions LLC",
"contributors": [
"Crottolo <bo@fl1.cz>",
],
"website": "https://github.com/apexive/odoo-llm",
"category": "Technical",
"version": "18.0.1.1.0",
"depends": ["llm", "llm_tool"],
"external_dependencies": {
"python": ["anthropic"],
},
"data": [
"data/llm_publisher.xml",
],
"images": [
"static/description/banner.jpeg",
],
"license": "LGPL-3",
"installable": True,
}
@@ -0,0 +1,17 @@
18.0.1.1.0 (2026-01-17)
~~~~~~~~~~~~~~~~~~~~~~~
* [ADD] Multimodal file support for images, PDFs, and text files
* [IMP] Refactored to use base module's _prepare_multimodal_attachments() method
* [IMP] Removed duplicate attachment handling code
18.0.1.0.1 (2026-01-07)
~~~~~~~~~~~~~~~~~~~~~~~
* [REM] Removed provider data file - users now create providers manually
* [IMP] Provider data is now user-owned and survives module uninstall
18.0.1.0.0 (2025-10-23)
~~~~~~~~~~~~~~~~~~~~~~~
* [INIT] Initial release
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="llm_provider_anthropic" model="llm.provider">
<field name="name">Anthropic</field>
<field name="service">anthropic</field>
<field name="active" eval="True" />
<field name="api_base">https://api.anthropic.com</field>
</record>
</odoo>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="llm_publisher_anthropic" model="llm.publisher">
<field name="name">Anthropic</field>
<field name="official" eval="True" />
<field name="frontier" eval="True" />
<field
name="description"
>Anthropic is an artificial intelligence research company focused on developing safe and ethical AI systems. Known for developing the Claude series of language models, Anthropic emphasizes constitutional AI and responsible development practices. Claude models excel at nuanced reasoning, following complex instructions, and providing helpful, harmless, and honest responses.</field>
<field
name="logo"
type="base64"
file="llm_anthropic/static/description/anthropic_logo.png"
/>
<field name="meta">{
"website": "https://www.anthropic.com",
"founded": 2021,
"headquarters": "San Francisco, California",
"key_products": ["Claude Opus 4.5", "Claude Sonnet 4.5", "Claude Haiku 4.5", "Constitutional AI"]
}</field>
</record>
</odoo>
+202
View File
@@ -0,0 +1,202 @@
==========================================
Anthropic Provider for Odoo LLM
==========================================
Anthropic Claude integration - Advanced AI with safety and reasoning at its core.
**Module Type:** 🔧 Provider
Architecture
============
::
┌─────────────────────────────────────────────────────────────────┐
│ Used By (Any LLM Module) │
│ ┌─────────────┐ ┌───────────┐ ┌─────────────┐ ┌───────────┐ │
│ │llm_assistant│ │llm_thread │ │llm_knowledge│ │llm_generate│ │
│ └──────┬──────┘ └─────┬─────┘ └──────┬──────┘ └─────┬─────┘ │
└─────────┼───────────────┼───────────────┼───────────────┼───────┘
└───────────────┴───────┬───────┴───────────────┘
┌───────────────────────────────────────────────┐
│ ★ llm_anthropic (This Module) ★ │
│ Anthropic Provider │
│ Claude 4.5 │ Claude 4 │ Claude 3.x │ Vision │
└─────────────────────┬─────────────────────────┘
┌───────────────────────────────────────────────┐
│ llm │
│ (Core Base Module) │
└───────────────────────────────────────────────┘
Installation
============
What to Install
---------------
**For AI chat with Claude:**
.. code-block:: bash
odoo-bin -d your_db -i llm_assistant,llm_anthropic
Auto-Installed Dependencies
---------------------------
- ``llm`` (core infrastructure)
- ``llm_tool`` (tool/function calling support)
Why Choose Claude?
------------------
+-------------------+------------------------------------------------+
| Feature | Claude |
+===================+================================================+
| **Safety** | 🛡️ Constitutional AI for aligned responses |
+-------------------+------------------------------------------------+
| **Context** | 📚 200K+ token context window |
+-------------------+------------------------------------------------+
| **Reasoning** | 🧠 Extended thinking mode available |
+-------------------+------------------------------------------------+
| **Vision** | 👁️ Advanced multimodal capabilities |
+-------------------+------------------------------------------------+
| **Tool Calling** | 🔧 Native function calling support |
+-------------------+------------------------------------------------+
Common Setups
-------------
+---------------------------+----------------------------------------------+
| I want to... | Install |
+===========================+==============================================+
| Chat with Claude | ``llm_assistant`` + ``llm_anthropic`` |
+---------------------------+----------------------------------------------+
| Claude + document search | Above + ``llm_knowledge`` + ``llm_pgvector`` |
+---------------------------+----------------------------------------------+
| Claude + external tools | Above + ``llm_mcp_server`` |
+---------------------------+----------------------------------------------+
Features
========
- Connect to Anthropic API with proper authentication
- Support for all Claude models (4.5, 4, 3.x series)
- Tool/function calling capabilities
- Extended thinking support (Claude's reasoning mode)
- Streaming responses
- Multimodal (vision) capabilities for supported models
- Automatic model discovery
Supported Models
================
Claude 4.5 Series
-----------------
- **Claude 4.5 Opus** - Highest capability, complex tasks
- **Claude 4.5 Sonnet** - Balanced performance and speed
- **Claude 4.5 Haiku** - Fast, cost-effective
**All support:** Chat, Vision, Tools, Extended Thinking
Claude 4 Series
---------------
- **Claude 4 Opus** - Previous generation flagship
- **Claude 4 Sonnet** - Balanced Claude 4 model
**All support:** Chat, Vision, Tools
Claude 3.x Series
-----------------
- **Claude 3 Opus** - Proven high-capability model
- **Claude 3 Sonnet** - Balanced performance
- **Claude 3 Haiku** - Fast, efficient
**All support:** Chat, Vision, Tools
Configuration
=============
1. Install the module
2. Go to **LLM → Configuration → Providers**
3. Create a new provider and select "Anthropic"
4. Enter your Anthropic API key from console.anthropic.com
5. Click "Fetch Models" to import available models
Extended Thinking
=================
Claude supports extended thinking mode, which allows the model to show its reasoning process:
.. code-block:: python
# Enable extended thinking in your assistant configuration
response = provider.chat(
messages=messages,
extended_thinking=True,
thinking_budget=10000 # tokens for reasoning
)
Key Differences from OpenAI
============================
+------------------+-------------------------------------------+-------------------------------------------+
| Aspect | OpenAI | Anthropic |
+==================+===========================================+===========================================+
| System message | In messages array | Separate ``system`` parameter |
+------------------+-------------------------------------------+-------------------------------------------+
| Tool format | ``{"type": "function", "function": {}}`` | ``{"name", "description", "schema"}`` |
+------------------+-------------------------------------------+-------------------------------------------+
| Response content | Single string | Array of content blocks |
+------------------+-------------------------------------------+-------------------------------------------+
| Tool results | ``role: "tool"`` | ``role: "user"`` + ``type: "tool_use"`` |
+------------------+-------------------------------------------+-------------------------------------------+
| Thinking | Not available | Extended thinking mode |
+------------------+-------------------------------------------+-------------------------------------------+
Technical Specifications
========================
- **Version**: 18.0.1.0.0
- **License**: LGPL-3
- **Dependencies**: ``llm``, ``llm_tool``
- **Python Package**: ``anthropic``
Implemented Methods
===================
- ``anthropic_get_client()`` - Initialize Anthropic client
- ``anthropic_chat()`` - Chat with tool calling and streaming support
- ``anthropic_format_tools()`` - Convert tools to Anthropic format
- ``anthropic_format_messages()`` - Format mail.message records
- ``anthropic_models()`` - List available Claude models
- ``anthropic_normalize_prepend_messages()`` - Handle prepend messages
Related Modules
===============
- **``llm``** - Core infrastructure
- **``llm_tool``** - Tool calling support
- **``llm_assistant``** - AI assistants
- **``llm_knowledge``** - RAG with semantic search
- **``llm_openai``** - Alternative: OpenAI
- **``llm_ollama``** - Alternative: local AI
- **``llm_mistral``** - Alternative: Mistral AI
Contributors
============
- Crottolo <bo@fl1.cz> - Odoo 18.0 port with full tool calling and extended thinking support
License
=======
LGPL-3
----
*© 2025 Apexive Solutions LLC*
@@ -0,0 +1,2 @@
from . import anthropic_provider
from . import mail_message
@@ -0,0 +1,358 @@
import json
import logging
from anthropic import Anthropic
from odoo import _, api, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class LLMProvider(models.Model):
_inherit = "llm.provider"
@api.model
def _get_available_services(self):
"""Register Anthropic as an available service."""
services = super()._get_available_services()
return services + [("anthropic", "Anthropic")]
def anthropic_get_client(self):
"""Get Anthropic client instance."""
self.ensure_one()
if not self.api_key:
raise UserError(_("API key is required for Anthropic provider"))
return Anthropic(api_key=self.api_key)
def anthropic_normalize_prepend_messages(self, prepend_messages):
"""Normalize prepend messages for Anthropic format.
System messages are kept in the list and extracted later in chat().
This ensures proper handling of all message types.
"""
if not prepend_messages:
return []
normalized = []
for msg in prepend_messages:
content = msg.get("content", "")
if isinstance(content, str) or isinstance(content, list):
normalized.append({"role": msg["role"], "content": content})
else:
normalized.append(msg)
return normalized
def anthropic_chat(
self,
messages,
model=None,
stream=False,
tools=None,
prepend_messages=None,
**kwargs,
):
"""Send chat messages using Anthropic Claude.
Key differences from OpenAI:
- System message is a separate parameter, not in messages array
- Tool format: {"name", "description", "input_schema"}
- Response: content blocks array, not single content string
- Tool use: content block with type="tool_use"
Args:
messages: mail.message recordset to send
model: Optional specific model to use
stream: Whether to stream the response
tools: llm.tool recordset of available tools
prepend_messages: List of pre-formatted message dicts
**kwargs: Additional parameters (max_tokens, extended_thinking, etc.)
Returns:
Generator if streaming, else dict with 'content' and/or 'tool_calls'
"""
model = self.get_model(model, "chat")
formatted_messages = self.format_messages(messages, model=model)
system_content = None
if prepend_messages:
for msg in prepend_messages:
if msg.get("role") == "system":
system_content = self._extract_content_text(msg.get("content", ""))
break
non_system_prepend = [
m for m in prepend_messages if m.get("role") != "system"
]
formatted_messages = non_system_prepend + formatted_messages
params = {
"model": model.name,
"messages": formatted_messages,
"max_tokens": kwargs.get("max_tokens", 4096),
}
if system_content:
params["system"] = system_content
if tools:
formatted_tools = self.format_tools(tools)
if formatted_tools:
params["tools"] = formatted_tools
if kwargs.get("extended_thinking"):
params["thinking"] = {
"type": "enabled",
"budget_tokens": kwargs.get("thinking_budget", 10000),
}
if stream:
return self._anthropic_stream_response(params)
return self._anthropic_process_response(params)
def _anthropic_process_response(self, params):
"""Process non-streaming response from Anthropic.
Returns:
dict: {"content": str} and/or {"tool_calls": list} and/or {"thinking": str}
"""
response = self.client.messages.create(**params)
result = {}
thinking_content = []
for block in response.content:
if block.type == "thinking":
thinking_content.append(block.thinking)
elif block.type == "text":
result["content"] = result.get("content", "") + block.text
elif block.type == "tool_use":
if "tool_calls" not in result:
result["tool_calls"] = []
result["tool_calls"].append(
{
"id": block.id,
"type": "function",
"function": {
"name": block.name,
"arguments": json.dumps(block.input),
},
},
)
if thinking_content:
result["thinking"] = "\n".join(thinking_content)
return result
def _anthropic_stream_response(self, params):
"""Process streaming response from Anthropic.
Yields:
dict: {"content": str} or {"tool_calls": list} or {"thinking": str}
"""
with self.client.messages.stream(**params) as stream:
tool_calls = {}
current_thinking = ""
for event in stream:
if event.type == "content_block_start":
if event.content_block.type == "tool_use":
tool_calls[event.index] = {
"id": event.content_block.id,
"name": event.content_block.name,
"input": "",
}
elif event.content_block.type == "thinking":
current_thinking = ""
elif event.type == "content_block_delta":
if hasattr(event.delta, "text"):
yield {"content": event.delta.text}
elif hasattr(event.delta, "thinking"):
current_thinking += event.delta.thinking
yield {"thinking": event.delta.thinking}
elif hasattr(event.delta, "partial_json"):
if event.index in tool_calls:
tool_calls[event.index]["input"] += event.delta.partial_json
elif event.type == "content_block_stop":
if event.index in tool_calls:
tc = tool_calls[event.index]
try:
parsed_input = (
json.loads(tc["input"]) if tc["input"] else {}
)
except json.JSONDecodeError:
parsed_input = {}
yield {
"tool_calls": [
{
"id": tc["id"],
"type": "function",
"function": {
"name": tc["name"],
"arguments": json.dumps(parsed_input),
},
},
],
}
del tool_calls[event.index]
def anthropic_format_tools(self, tools):
"""Format tools for Anthropic API.
Anthropic tool format:
{
"name": "tool_name",
"description": "Tool description",
"input_schema": {
"type": "object",
"properties": {...},
"required": [...]
}
}
"""
formatted = []
for tool in tools:
try:
if tool.input_schema:
schema = json.loads(tool.input_schema)
else:
schema = (
tool.get_input_schema()
if hasattr(tool, "get_input_schema")
else {}
)
except (json.JSONDecodeError, TypeError):
schema = {}
formatted.append(
{
"name": tool.name,
"description": tool.description or "",
"input_schema": {
"type": "object",
"properties": schema.get("properties", {}),
"required": schema.get("required", []),
},
},
)
return formatted
def anthropic_format_messages(self, messages, system_prompt=None, model=None):
"""Format mail.message records for Anthropic API.
Note: System prompts are handled separately in anthropic_chat(),
not included in the messages array.
Args:
messages: mail.message recordset
system_prompt: Optional system prompt (handled separately)
model: llm.model record (to determine if multimodal)
Returns:
List of formatted messages for Anthropic
"""
is_multimodal = model and model.model_use == "multimodal"
formatted_messages = []
for message in messages:
formatted_message = self._dispatch(
"format_message",
record=message,
is_multimodal=is_multimodal,
)
if formatted_message:
formatted_messages.append(formatted_message)
formatted_messages = self._anthropic_merge_consecutive_user_messages(
formatted_messages,
)
return formatted_messages
def _anthropic_merge_consecutive_user_messages(self, messages):
"""Merge consecutive user messages as required by Anthropic API.
Anthropic requires alternating user/assistant messages.
"""
if not messages:
return []
merged = []
for msg in messages:
if merged and merged[-1]["role"] == msg["role"] == "user":
prev_content = merged[-1]["content"]
curr_content = msg["content"]
if isinstance(prev_content, str) and isinstance(curr_content, str):
merged[-1]["content"] = prev_content + "\n" + curr_content
elif isinstance(prev_content, list) and isinstance(curr_content, list):
merged[-1]["content"] = prev_content + curr_content
elif isinstance(prev_content, str) and isinstance(curr_content, list):
merged[-1]["content"] = [
{"type": "text", "text": prev_content},
] + curr_content
elif isinstance(prev_content, list) and isinstance(curr_content, str):
merged[-1]["content"] = prev_content + [
{"type": "text", "text": curr_content},
]
else:
merged.append(msg)
return merged
def anthropic_models(self, model_id=None):
"""List available Anthropic models.
Args:
model_id: Optional specific model ID to retrieve
Yields:
dict: Model data with name and details
"""
if model_id:
model = self.client.models.retrieve(model_id)
yield self._anthropic_parse_model(model)
else:
response = self.client.models.list()
for model in response.data:
yield self._anthropic_parse_model(model)
def _anthropic_parse_model(self, model):
"""Parse Anthropic model into Odoo format.
Args:
model: Anthropic model object
Returns:
dict: {"name": str, "details": dict}
"""
capabilities = ["chat"]
model_id = model.id.lower()
if "opus" in model_id or "claude-3" in model_id or "claude-4" in model_id:
capabilities.append("multimodal")
return {
"name": model.id,
"details": {
"id": model.id,
"display_name": getattr(model, "display_name", model.id),
"capabilities": capabilities,
"created_at": str(getattr(model, "created_at", "")),
},
}
def _determine_model_use(self, name, capabilities):
"""Override to handle Anthropic-specific model classification."""
if self.service != "anthropic":
return super()._determine_model_use(name, capabilities)
if any(cap in capabilities for cap in ["multimodal", "vision"]):
return "multimodal"
return "chat"
@@ -0,0 +1,145 @@
import json
import logging
from odoo import models, tools
_logger = logging.getLogger(__name__)
class MailMessage(models.Model):
_inherit = "mail.message"
def anthropic_format_message(self, is_multimodal=False):
"""Provider-specific formatting for Anthropic Claude.
Key differences from OpenAI:
- Tool results use role="user" with content type "tool_result"
- Assistant messages with tool calls use content as array of blocks
- Content can be string or list of content blocks
"""
self.ensure_one()
body = self.body
if body:
body = tools.html2plaintext(body)
if self.is_llm_user_message()[self]:
texts = self._get_text_attachments()
# Only include images/PDFs if model supports multimodal
if is_multimodal:
images = self._get_image_attachments()
pdfs = self._get_pdf_attachments()
else:
images = []
pdfs = []
has_attachments = images or pdfs or texts
if has_attachments:
content = []
for img in images:
content.append(
{
"type": "image",
"source": {
"type": "base64",
"media_type": img["mimetype"],
"data": img["data"],
},
},
)
for pdf in pdfs:
content.append(
{
"type": "document",
"source": {
"type": "base64",
"media_type": pdf["mimetype"],
"data": pdf["data"],
},
},
)
text_parts = []
if body and body.strip():
text_parts.append(body.strip())
for txt in texts:
text_parts.append(f"--- {txt['name']} ---\n{txt['content']}")
if text_parts:
content.append({"type": "text", "text": "\n\n".join(text_parts)})
elif images or pdfs:
content.append(
{"type": "text", "text": "Please analyze these files."},
)
return {"role": "user", "content": content}
if not body or not body.strip():
return None
return {"role": "user", "content": body}
if self.is_llm_assistant_message()[self]:
content_blocks = []
if body:
content_blocks.append({"type": "text", "text": body})
tool_calls = self.get_tool_calls()
if tool_calls:
for tc in tool_calls:
try:
tool_input = json.loads(tc["function"]["arguments"])
except (json.JSONDecodeError, KeyError, TypeError):
tool_input = {}
content_blocks.append(
{
"type": "tool_use",
"id": tc["id"],
"name": tc["function"]["name"],
"input": tool_input,
},
)
if content_blocks:
return {"role": "assistant", "content": content_blocks}
return None
if self.is_llm_tool_message()[self]:
tool_data = self.body_json
if not tool_data:
_logger.warning(
f"Anthropic Format: Skipping tool message {self.id}: no tool data found.",
)
return None
tool_call_id = tool_data.get("tool_call_id")
if not tool_call_id:
_logger.warning(
f"Anthropic Format: Skipping tool message {self.id}: missing tool_call_id.",
)
return None
if "result" in tool_data:
content = json.dumps(tool_data["result"])
elif "error" in tool_data:
content = json.dumps({"error": tool_data["error"]})
else:
content = ""
return {
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_call_id,
"content": content,
},
],
}
return None
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

@@ -0,0 +1,476 @@
<!-- Hero Section -->
<section style="padding:4rem 0 3rem">
<div class="container">
<div class="text-center" style="max-width:800px; margin:0 auto">
<div style="display:inline-flex; padding:0.5rem 1.25rem; border-radius:50px; margin-bottom:2rem">
<i class="fa fa-bolt" style="color:#71639e; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#71639e; font-weight:600; font-size:0.9rem">AI Provider</span>
</div>
<h1 style="font-size:3rem; font-weight:800; color:#212529; margin-bottom:1.5rem; line-height:1.2">
Anthropic Claude Integration
</h1>
<p style="font-size:1.25rem; color:#6c757d; margin-bottom:1rem; line-height:1.6">
Anthropic Claude provider integration for LLM module.
</p>
<p style="font-size:1rem; color:#868e96; margin-bottom:2rem">
Use <strong style="color:#495057">Claude 4.5, Claude 4, and Claude 3.x</strong> for chat, vision, and advanced reasoning in Odoo
</p>
<div class="d-flex justify-content-center flex-wrap">
<div class="d-flex align-items-center" style="padding:0.75rem 1.5rem; border-radius:12px; margin:0.5rem">
<i class="fa fa-comments" style="color:#71639e; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#5b4c7d; font-weight:600">Chat Models</span>
</div>
<div class="d-flex align-items-center" style="padding:0.75rem 1.5rem; border-radius:12px; margin:0.5rem">
<i class="fa fa-eye" style="color:#17a2b8; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#0c5460; font-weight:600">Vision</span>
</div>
<div class="d-flex align-items-center" style="padding:0.75rem 1.5rem; border-radius:12px; margin:0.5rem">
<i class="fa fa-wrench" style="color:#ffc107; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#856404; font-weight:600">Tool Calling</span>
</div>
<div class="d-flex align-items-center" style="padding:0.75rem 1.5rem; border-radius:12px; margin:0.5rem">
<i class="fa fa-lightbulb-o" style="color:#28a745; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#155724; font-weight:600">Extended Thinking</span>
</div>
</div>
</div>
</div>
</section>
<div class="container my-5">
<!-- What is Claude Section -->
<section class="mb-5">
<h2 class="text-center mb-3" style="font-size:2.5rem; font-weight:700; color:#71639e">What is Anthropic Claude?</h2>
<p class="text-center mb-5" style="font-size:1.2rem; color:#6c757d; font-weight:500">Advanced AI with safety and reasoning at its core</p>
<div class="row justify-content-center mb-5">
<div class="col-lg-10">
<div class="card border-0 shadow-sm" style="border-radius:16px; padding:2.5rem">
<p style="font-size:1.15rem; color:#495057; line-height:1.9; margin-bottom:0; text-align:center">
Anthropic Claude is a next-generation AI assistant developed by Anthropic with a focus on safety, accuracy, and advanced reasoning. This module implements Claude as a provider service for the LLM integration module, enabling you to use Claude models for chat conversations, vision tasks, tool execution, and extended thinking capabilities.
</p>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Capabilities Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Capabilities</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">What you can do with Claude in Odoo</p>
<div class="row g-4 justify-content-center">
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:12px; margin:0 auto 1.5rem">
<i class="fa fa-comments" style="font-size:32px; color:#ffffff"></i>
</div>
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Chat Capabilities</h4>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Use Claude models for conversational AI, assistants, and chat-based workflows with streaming support.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:12px; margin:0 auto 1.5rem">
<i class="fa fa-eye" style="font-size:32px; color:#ffffff"></i>
</div>
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Vision (Multimodal)</h4>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Process images and visual content with Claude's advanced vision capabilities.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:12px; margin:0 auto 1.5rem">
<i class="fa fa-wrench" style="font-size:32px; color:#ffffff"></i>
</div>
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Tool Calling</h4>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Execute Odoo functions and external tools through Claude's function calling interface.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:12px; margin:0 auto 1.5rem">
<i class="fa fa-lightbulb-o" style="font-size:32px; color:#ffffff"></i>
</div>
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Extended Thinking</h4>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Access Claude's reasoning process with extended thinking mode for complex problem-solving.</p>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Claude Models Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Supported Models</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Access the full Claude model family</p>
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="card border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="row g-3">
<div class="col-md-4">
<div class="bg-light" style="padding:1.5rem; border-radius:12px; height:100%">
<h5 style="color:#71639e; font-weight:700; margin-bottom:1rem">
<i class="fa fa-star" style="margin-right:0.5rem"></i>Claude 4.5
</h5>
<p style="color:#495057; font-size:0.9rem; margin-bottom:0.5rem">Opus, Sonnet, Haiku</p>
<p style="color:#6c757d; font-size:0.85rem; margin-bottom:0">Latest models with enhanced reasoning, vision, and extended thinking support.</p>
</div>
</div>
<div class="col-md-4">
<div class="bg-light" style="padding:1.5rem; border-radius:12px; height:100%">
<h5 style="color:#71639e; font-weight:700; margin-bottom:1rem">
<i class="fa fa-rocket" style="margin-right:0.5rem"></i>Claude 4
</h5>
<p style="color:#495057; font-size:0.9rem; margin-bottom:0.5rem">Opus, Sonnet</p>
<p style="color:#6c757d; font-size:0.85rem; margin-bottom:0">Powerful models with vision, tool calling, and advanced capabilities.</p>
</div>
</div>
<div class="col-md-4">
<div class="bg-light" style="padding:1.5rem; border-radius:12px; height:100%">
<h5 style="color:#71639e; font-weight:700; margin-bottom:1rem">
<i class="fa fa-check-circle" style="margin-right:0.5rem"></i>Claude 3.x
</h5>
<p style="color:#495057; font-size:0.9rem; margin-bottom:0.5rem">Opus, Sonnet, Haiku</p>
<p style="color:#6c757d; font-size:0.85rem; margin-bottom:0">Proven models with chat, vision, and tool calling support.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Key Differences Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Why Choose Claude?</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Claude's unique advantages</p>
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="row g-4">
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-start">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:12px; margin-right:1rem; flex-shrink:0">
<i class="fa fa-shield" style="font-size:24px; color:#71639e"></i>
</div>
<div>
<h5 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0.5rem">Safety First</h5>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Built with Constitutional AI for safer, more aligned responses.</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-start">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:12px; margin-right:1rem; flex-shrink:0">
<i class="fa fa-book" style="font-size:24px; color:#71639e"></i>
</div>
<div>
<h5 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0.5rem">Extended Context</h5>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">200K+ token context window for processing large documents.</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-start">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:12px; margin-right:1rem; flex-shrink:0">
<i class="fa fa-graduation-cap" style="font-size:24px; color:#71639e"></i>
</div>
<div>
<h5 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0.5rem">Advanced Reasoning</h5>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Extended thinking mode shows Claude's reasoning process.</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-start">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:12px; margin-right:1rem; flex-shrink:0">
<i class="fa fa-code" style="font-size:24px; color:#71639e"></i>
</div>
<div>
<h5 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0.5rem">Superior Coding</h5>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Excellent performance on coding and technical tasks.</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Technical Details -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Technical Details</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Requirements and dependencies</p>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1.5rem">Module Information</h4>
<div class="row g-3">
<div class="col-sm-6">
<div class="bg-light" style="padding:1rem; border-radius:8px">
<strong style="color:#495057">Dependencies:</strong>
<p style="margin:0.5rem 0 0 0; color:#6c757d">base, llm, llm_tool</p>
</div>
</div>
<div class="col-sm-6">
<div class="bg-light" style="padding:1rem; border-radius:8px">
<strong style="color:#495057">Python Packages:</strong>
<p style="margin:0.5rem 0 0 0; color:#6c757d">anthropic</p>
</div>
</div>
<div class="col-sm-6">
<div class="bg-light" style="padding:1rem; border-radius:8px">
<strong style="color:#495057">Category:</strong>
<p style="margin:0.5rem 0 0 0; color:#6c757d">Technical</p>
</div>
</div>
<div class="col-sm-6">
<div class="bg-light" style="padding:1rem; border-radius:8px">
<strong style="color:#495057">License:</strong>
<p style="margin:0.5rem 0 0 0; color:#6c757d">LGPL-3</p>
</div>
</div>
</div>
<div class="bg-light mt-4" style="padding:1.5rem; border-radius:12px">
<h5 style="font-weight:700; color:#212529; margin-bottom:0.75rem">Getting Started</h5>
<p style="color:#6c757d; margin-bottom:0">Obtain your API key from Anthropic Console, then configure a new provider in Odoo with your credentials.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Related Modules Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Related Modules</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Explore other modules in the Odoo LLM suite</p>
<div class="row g-4">
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-cogs" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Base</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Core framework for LLM integration with provider management and model configuration.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_tool" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-wrench" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Tool</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Enable AI to execute Odoo functions with the tool calling interface.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_thread" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-comments" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Thread</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Conversational AI threads with message history and streaming responses.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_assistant" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-user-circle" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Assistant</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Create customizable AI assistants with specific instructions and personalities.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_knowledge" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-book" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Knowledge</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">RAG system for semantic document search and knowledge base integration.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_openai" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-bolt" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">OpenAI Provider</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Connect to OpenAI GPT models and dozens of compatible providers.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_ollama" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-server" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">Ollama Provider</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Run open-source models locally with complete privacy and no API costs.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_mistral" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-rocket" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">Mistral Provider</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">European AI excellence with fast inference and GDPR compliance.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_mcp_server" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-plug" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">MCP Server</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Connect external tools using Model Context Protocol servers.</p>
</div>
</div>
</a>
</div>
</div>
</section>
</div>
<!-- Footer -->
<footer class="bg-primary" style="padding:3rem 2rem; border-radius:16px; margin:2rem 1rem">
<div class="container">
<div style="border-radius:12px; padding:2.5rem">
<div class="row align-items-center">
<div class="col-md-8">
<h4 style="font-weight:700; color:#ffffff; font-size:1.5rem; margin-bottom:0.75rem">Anthropic Claude Integration</h4>
<p style="color:#f8f9fa; font-size:1rem; margin-bottom:1rem">Claude AI provider for Odoo</p>
<p style="color:#e9d5ff; margin-bottom:0.25rem">Developed by <strong style="color:#ffffff">Apexive Solutions LLC</strong></p>
<p style="color:#e0d4ec; font-size:0.9rem; margin-bottom:0">Licensed under LGPL-3 &bull; &copy; 2025 All rights reserved.</p>
</div>
<div class="col-md-4 mt-3 mt-md-0">
<div class="d-flex flex-column">
<div class="bg-white d-flex align-items-center justify-content-center" style="padding:0.875rem 1.5rem; border-radius:8px; color:#71639e; font-weight:600; margin-bottom:0.75rem">
<i class="fa fa-github" style="font-size:18px; margin-right:0.5rem"></i>
github.com/apexive/odoo-llm
</div>
<a href="mailto:hello@apexive.com" class="bg-white d-flex align-items-center justify-content-center" style="padding:0.875rem 1.5rem; border-radius:8px; text-decoration:none; color:#71639e; font-weight:600; margin-bottom:0">
<i class="fa fa-envelope" style="font-size:18px; margin-right:0.5rem"></i>
hello@apexive.com
</a>
</div>
</div>
</div>
</div>
</div>
</footer>
@@ -0,0 +1,43 @@
# Changelog - LLM Assistant Module
All notable changes to the llm_assistant module will be documented in this file.
## [16.0.1.5.0] - 2025-07-13
### Added
- **Assistant Code System**: New `code` field for unique assistant identification
- Unique constraint ensuring assistant codes are globally unique
- Index on code field for performance optimization
- Enables programmatic assistant discovery replacing category-based lookups
- **Model Association**: New `res_model` field for linking assistants to specific Odoo models
- Supports model-specific assistant configurations (e.g., 'fleek.character')
- Enables filtered assistant discovery based on target model
- **Default Assistant System**: New `is_default` boolean field
- Marks assistants as defaults for automatic thread creation
- Combined with `res_model` and `is_public` for granular access control
- Enables dynamic thread provisioning based on user permissions
- **Assistant Discovery Method**: New `get_assistant_by_code(code)` class method
- Centralized method for finding assistants by unique code
- Replaces complex category-based search patterns
- Used by thread management systems for consistent assistant lookup
### Changed
- **Database Migration**: Version 16.0.1.5.0 migration script
- Automatically generates dotted codes from existing category hierarchy
- Converts parent.child.grandchild category structures to assistant codes
- Handles missing codes with sanitized naming and duplicate resolution
### Technical Details
- **SQL Constraints**: Added `unique_code` constraint for data integrity
- **Performance**: Code field indexed for fast lookups
- **Backward Compatibility**: Migration preserves existing assistant functionality while adding new capabilities
- **Integration**: Seamlessly integrates with existing prompt template and tool systems
+476
View File
@@ -0,0 +1,476 @@
# LLM Assistant for Odoo
Advanced AI assistant management with integrated prompt templates, testing capabilities, and intelligent configuration orchestration. This module serves as the intelligence layer that defines how Odoo data connects to AI models.
**Module Type:** 🚀 Entry Point
![Architecture](static/description/llm_assistant_architecture.png)
## Installation
### What to Install
This is the **main entry point** for AI chat features in Odoo.
**Basic AI Chat Setup:**
```bash
odoo-bin -d your_db -i llm_assistant,llm_openai
```
### Auto-Installed Dependencies
These are pulled in automatically:
- `llm` (core infrastructure)
- `llm_tool` (function calling)
- `llm_thread` (chat interface)
- `mail` (Odoo messaging)
### Choose a Provider
| Provider | Module | Best For |
| -------- | ------------- | --------------------------- |
| OpenAI | `llm_openai` | GPT-4, most capable |
| Ollama | `llm_ollama` | Local/private, no API costs |
| Mistral | `llm_mistral` | European, fast |
### Common Setups
| I want to... | Install |
| ------------------------------- | ------------------------------------------------------------ |
| Chat with GPT-4 in Odoo | `llm_assistant` + `llm_openai` |
| Use local AI (privacy) | `llm_assistant` + `llm_ollama` |
| Add document search (RAG) | Above + `llm_knowledge` + `llm_pgvector` |
| Connect Claude Desktop | Above + `llm_mcp_server` |
| Build domain-specific assistant | Extend `llm_assistant` (see `llm_assistant_account_invoice`) |
## Overview
The LLM Assistant module provides sophisticated AI assistant management that goes far beyond simple chatbots. It serves as the intelligent configuration layer that orchestrates how AI models interact with Odoo data, with integrated prompt template management and comprehensive testing capabilities.
### Core Capabilities
- **AI Assistant Configuration** - Define specialized AI personas with specific roles and capabilities
- **Integrated Prompt Management** - Consolidated prompt template system (formerly separate `llm_prompt` module)
- **Template Testing** - Built-in testing wizard for prompt validation and optimization
- **Context Orchestration** - Intelligent mapping between Odoo data and AI inputs
- **Tool Management** - Configure available tools and their usage patterns
- **Generation Configuration** - Templates for different content types (text, images, etc.)
## Key Features
### Consolidated Architecture
The module consolidates functionality from the former `llm_prompt` module:
-**Prompt templates integrated** into assistant management
-**Enhanced testing wizard** with context simulation
-**Streamlined UI** with unified assistant and prompt selection
-**Auto-argument detection** for template variables
-**Schema synchronization** between templates and forms
### Assistant Types & Use Cases
#### 1. Chat Assistants
Configure conversational AI with specific personas:
```python
# Customer service assistant
assistant = env['llm.assistant'].create({
'name': 'Customer Support Bot',
'role': 'Customer Service Representative',
'goal': 'Provide helpful and accurate customer support',
'background': 'Expert in our products with access to CRM data',
'instructions': '''
- Always be polite and professional
- Use customer history to provide personalized responses
- Escalate complex issues to human agents
- Provide clear, actionable solutions
''',
'tool_ids': [(6, 0, [crm_tool.id, knowledge_tool.id])]
})
```
#### 2. Content Generation Assistants
Configure specialized content creation workflows:
```python
# Marketing content generator
assistant = env['llm.assistant'].create({
'name': 'Marketing Content Creator',
'role': 'Marketing Specialist',
'goal': 'Create compelling marketing content from product data',
'prompt_id': marketing_template.id,
'default_values': {
'brand_voice': 'professional yet approachable',
'target_audience': 'business professionals'
}
})
```
#### 3. Analysis Assistants
Configure data analysis and insights:
```python
# Business intelligence assistant
assistant = env['llm.assistant'].create({
'name': 'BI Analyst',
'role': 'Business Intelligence Analyst',
'goal': 'Analyze business data and provide actionable insights',
'tool_ids': [(6, 0, [reporting_tool.id, analytics_tool.id])]
})
```
### Integrated Prompt Template System
#### Template Management
```python
# Create prompt template with auto-detection
prompt = env['llm.prompt'].create({
'name': 'Sales Email Generator',
'template': '''
Generate a personalized sales email for {{customer_name}}
regarding {{product_name}}.
Customer Context:
- Company: {{customer_company}}
- Industry: {{industry}}
- Previous purchases: {{purchase_history}}
Email should be {{tone}} and focus on {{key_benefits}}.
''',
'format': 'text',
'category_id': sales_category.id
})
# Arguments automatically detected and schema generated
prompt.auto_detect_arguments()
```
#### Advanced Template Formats
**YAML Format** for structured conversations:
```yaml
messages:
- type: system
content: |
You are {{role}}. Your goal is {{goal}}.
Customer: {{customer_name}} ({{customer_company}})
- type: user
content: |
{{user_request}}
```
**JSON Format** for direct API compatibility:
```json
{
"messages": [
{
"type": "system",
"content": "You are {{role}} helping {{customer_name}}"
},
{
"type": "user",
"content": "{{user_input}}"
}
],
"temperature": {{temperature}},
"max_tokens": {{max_tokens}}
}
```
### Testing & Validation
#### Enhanced Testing Wizard
The integrated testing wizard provides comprehensive validation:
```python
# Launch testing wizard
wizard = env['llm.assistant.test.wizard'].create({
'assistant_id': assistant.id,
'test_context': {
'customer_name': 'John Smith',
'customer_company': 'Acme Corp',
'product_name': 'Enterprise Software'
}
})
# Test with different scenarios
wizard.run_test_scenarios([
{'tone': 'professional', 'urgency': 'high'},
{'tone': 'friendly', 'urgency': 'low'},
{'tone': 'formal', 'urgency': 'medium'}
])
```
#### Auto-Detection Features
- **Template Arguments**: Automatically detect `{{variables}}` in templates
- **Schema Generation**: Create JSON schemas for form generation
- **Validation**: Ensure template-schema consistency
- **Default Values**: Smart defaults based on context
### Context Orchestration
#### Data Mapping Configuration
```python
def prepare_context(self, record=None, user_input=None):
"""Transform Odoo data into LLM-compatible context"""
context = {}
if record and record._name == 'sale.order':
context.update({
'customer_name': record.partner_id.name,
'order_total': record.amount_total,
'order_date': record.date_order,
'sales_person': record.user_id.name
})
# Add user input and system context
context['user_input'] = user_input
context['current_date'] = fields.Date.today()
return context
```
#### Intelligent History Management
```python
def trim_conversation_history(self, messages, max_tokens=4000):
"""Intelligent context window management"""
# Keep system message and recent context
# Remove older messages while preserving important context
# Maintain conversation coherence
```
## Configuration Guide
### 1. Basic Assistant Setup
```python
# Create specialized assistant
assistant = env['llm.assistant'].create({
'name': 'Sales Assistant',
'role': 'Sales Representative',
'goal': 'Help close deals and provide product information',
'background': 'Expert in our product portfolio with CRM access',
'instructions': '''
Key behaviors:
- Always qualify leads before pitching
- Use customer data to personalize responses
- Focus on value propositions
- Suggest appropriate products based on needs
''',
'provider_id': openai_provider.id,
'model_id': gpt4_model.id,
'tool_ids': [(6, 0, [crm_search.id, product_catalog.id])]
})
```
### 2. Prompt Template Integration
```python
# Create template for the assistant
template = env['llm.prompt'].create({
'name': 'Sales Conversation Template',
'template': '''
You are {{role}} working with {{customer_name}} from {{customer_company}}.
Customer Profile:
- Industry: {{industry}}
- Size: {{company_size}}
- Budget Range: {{budget_range}}
- Key Pain Points: {{pain_points}}
Your goal: {{goal}}
Guidelines: {{instructions}}
''',
'arguments_json': {
'customer_name': {'type': 'string', 'required': True},
'customer_company': {'type': 'string', 'required': True},
'industry': {'type': 'string', 'required': False},
'budget_range': {'type': 'string', 'required': False}
}
})
# Link template to assistant
assistant.prompt_id = template.id
```
### 3. Tool Configuration
```python
# Configure available tools for assistant
assistant.tool_ids = [(6, 0, [
crm_search_tool.id, # Search CRM records
product_catalog_tool.id, # Access product information
pricing_tool.id, # Get pricing and discounts
calendar_tool.id, # Schedule meetings
email_tool.id # Send follow-up emails
])]
```
## API Reference
### Assistant Methods
```python
# Get system prompt with context
system_prompt = assistant.get_system_prompt(context={
'customer_name': 'John Doe',
'customer_company': 'ABC Corp'
})
# Prepare conversation context
context = assistant.prepare_context(
record=sale_order,
user_input="Tell me about pricing options"
)
# Get available tools
tools = assistant.get_available_tools()
# Render prompt template
messages = assistant.prompt_id.get_messages(arguments={
'customer_name': 'John Doe',
'role': 'Sales Assistant'
})
```
### Template Methods
```python
# Auto-detect template arguments
prompt.auto_detect_arguments()
# Render template with arguments
rendered = prompt.get_messages(arguments={
'customer_name': 'John Smith',
'product_name': 'Enterprise Suite'
})
# Validate template syntax
is_valid, errors = prompt.validate_template()
```
## Integration Examples
### CRM Integration
```python
class SaleOrder(models.Model):
_inherit = 'sale.order'
def create_ai_assistant_thread(self):
"""Create AI assistant thread for this sale order"""
thread = self.env['llm.thread'].create({
'name': f'Sales Discussion - {self.name}',
'model': self._name,
'res_id': self.id,
'assistant_id': self.env.ref('my_module.sales_assistant').id
})
# Initialize with order context
context = {
'customer_name': self.partner_id.name,
'order_total': self.amount_total,
'products': [line.product_id.name for line in self.order_line]
}
thread.message_post(
body=f"AI Assistant ready to help with {self.name}",
llm_role="system",
body_json={'context': context}
)
return thread
```
### Project Management Integration
```python
class ProjectTask(models.Model):
_inherit = 'project.task'
def get_ai_assistance(self, query):
"""Get AI assistance for project tasks"""
assistant = self.env.ref('my_module.project_assistant')
context = assistant.prepare_context(
record=self,
user_input=query
)
# Generate AI response with project context
response = assistant.generate_response(
context=context,
tools=['project_search', 'time_tracking', 'resource_planning']
)
return response
```
## Technical Specifications
### Module Information
- **Name**: LLM Assistant
- **Version**: 18.0.1.5.0
- **Category**: Productivity
- **License**: LGPL-3
- **Dependencies**: `llm`, `mail`
- **Author**: Apexive Solutions LLC
### Key Models
- **`llm.assistant`**: Main assistant configuration
- **`llm.prompt`**: Integrated prompt template management
- **`llm.prompt.category`**: Template categorization
- **`llm.assistant.test.wizard`**: Testing and validation
## Performance Features
- **Smart Context Management**: Intelligent conversation history trimming
- **Template Caching**: Optimized template rendering and argument detection
- **Async Operations**: Non-blocking testing and validation
- **Database Optimization**: Efficient storage of assistant configurations
## Security Features
- **Role-Based Access**: Control who can create and modify assistants
- **Tool Permissions**: Granular control over tool access per assistant
- **Template Validation**: Prevent execution of malicious templates
- **Audit Trail**: Complete tracking of assistant usage and modifications
## Related Modules
- **`llm`**: Base infrastructure and provider management
- **`llm_thread`**: Chat interfaces and conversation management
- **`llm_tool`**: Function calling and Odoo integration
- **`llm_generate`**: Content generation with assistant integration
- **`llm_knowledge`**: RAG and knowledge base integration
## Support & Resources
- **Documentation**: [GitHub Repository](https://github.com/apexive/odoo-llm)
- **Architecture Guide**: [OVERVIEW.md](../OVERVIEW.md)
- **Examples**: [Assistant Examples](examples/)
- **Community**: [GitHub Discussions](https://github.com/apexive/odoo-llm/discussions)
## License
This module is licensed under [LGPL-3](https://www.gnu.org/licenses/lgpl-3.0.html).
---
_© 2025 Apexive Solutions LLC. All rights reserved._
@@ -0,0 +1,3 @@
from . import models
from . import controllers
from . import wizards
@@ -0,0 +1,75 @@
{
"name": "LLM Assistant",
"summary": """
LLM/AI Assistant module with prompt templates for Odoo
""",
"description": """
LLM Assistant with Prompt Templates for Odoo
==================
Configure AI assistants with specific roles, goals, and tools to enhance your AI interactions.
Includes comprehensive prompt template management for reusable LLM interactions.
Key Features:
- Create and configure AI assistants with specific roles and goals
- Assign preferred tools to each assistant
- Automatically generate system prompts based on assistant configuration
- Attach assistants to chat threads for consistent behavior
- Full integration with the LLM chat system
- Create reusable prompt templates in text, YAML, or JSON format
- Dynamic arguments within prompts
- Multi-step prompt workflows through structured formats
- Prompt discovery and retrieval
- Categories and tags for organization
- Enhanced prompt testing with context simulation
- Related record integration for prompt testing
Use cases include creating specialized assistants for customer support, data analysis, training assistance, and more.
""",
"category": "Productivity, Discuss",
"version": "18.0.1.5.4",
"depends": [
"base",
"mail",
"web",
"llm",
"llm_thread",
"llm_tool",
"web_json_editor",
],
"external_dependencies": {
"python": ["jinja2", "pyyaml", "jsonschema"],
},
"author": "Apexive Solutions LLC",
"website": "https://github.com/apexive/odoo-llm",
"data": [
"security/ir.model.access.csv",
"data/llm_prompt_tag_data.xml",
"data/llm_prompt_category_data.xml",
"data/llm_prompt_export_data.xml",
"data/llm_prompt_data.xml",
"data/llm_assistant_data.xml",
"views/llm_prompt_views.xml",
"views/llm_prompt_tag_views.xml",
"views/llm_prompt_category_views.xml",
"views/llm_assistant_views.xml",
"views/llm_thread_views.xml",
"views/llm_menu_views.xml",
"wizards/llm_prompt_test_views.xml",
],
"images": [
"static/description/banner.jpeg",
],
"assets": {
"web.assets_backend": [
# Service patches
"llm_assistant/static/src/services/llm_store_service_patch.js",
# Component patches
"llm_assistant/static/src/patches/llm_thread_header_patch.js",
"llm_assistant/static/src/patches/llm_thread_header_patch.xml",
],
},
"license": "LGPL-3",
"installable": True,
"application": False,
"auto_install": False,
}
@@ -0,0 +1,52 @@
18.0.1.5.4 (2025-12-02)
~~~~~~~~~~~~~~~~~~~~~~~
* [FIX] Removed invalid `unaccent` parameter from parent_path field (Odoo 18 compatibility)
* [FIX] Added missing access rules for llm.thread.mock transient model
18.0.1.5.3 (2025-12-02)
~~~~~~~~~~~~~~~~~~~~~~~
* [FIX] Replaced bus notification with client action for "Process with AI" button reliability
* [ADD] New client action llm_open_ai_chat_in_chatter for reliable AI chat opening
* [IMP] action_open_llm_assistant() now returns ir.actions.client instead of bus notification
* [IMP] Works reliably on cloud deployments with WebSocket/bus issues
18.0.1.5.2 (2025-11-26)
~~~~~~~~~~~~~~~~~~~~~~~
* [FIX] Moved prompt_id serialization to _thread_to_store() from llm_thread module
* [IMP] prompt_id handling now properly resides in the module that defines the field
18.0.1.5.1 (2025-11-21)
~~~~~~~~~~~~~~~~~~~~~~~
* [IMP] Simplified action_open_llm_assistant by removing unused pre/post action hooks
* [IMP] Enhanced thread naming in mixin - backend now generates names from record display_name
* [FIX] Cleaned up unnecessary kwargs handling for better maintainability
18.0.1.5.0 (2025-10-23)
~~~~~~~~~~~~~~~~~~~~~~~
* [MIGRATION] Migrated to Odoo 18.0
* [IMP] Updated views and OWL components for compatibility
16.0.1.5.0 (2025-07-13)
~~~~~~~~~~~~~~~~~~~~~~~
* [ADD] Assistant code system with unique constraint
* [ADD] Model association via res_model field
* [ADD] Default assistant system with is_default flag
* [ADD] get_assistant_by_code() discovery method
* [MIGRATION] Auto-generate codes from category hierarchy
16.0.1.0.1 (2025-04-04)
~~~~~~~~~~~~~~~~~~~~~~~
* [ADD] Assistant Creator assistant data record
* [ADD] Data directory structure
16.0.1.0.0 (2025-03-01)
~~~~~~~~~~~~~~~~~~~~~~~
* [INIT] Initial release
@@ -0,0 +1 @@
from . import main
@@ -0,0 +1,96 @@
import json
import logging
from odoo import http
from odoo.http import request
_logger = logging.getLogger(__name__)
class LLMPromptController(http.Controller):
"""Controller for LLM Prompt API endpoints"""
@http.route(
"/api/llm/prompts/list", type="json", auth="user", csrf=False, methods=["POST"]
)
def list_prompts(self, provider_id=None, **kwargs):
"""
List available prompts for a provider
Args:
provider_id: ID of the provider (optional, uses default if not provided)
Returns:
dict: List of available prompts
"""
try:
# Get provider
if provider_id:
provider = request.env["llm.provider"].browse(int(provider_id))
if not provider.exists():
return {"error": "Provider not found"}
else:
# Get default provider
provider = request.env["llm.provider"].search(
[("active", "=", True)],
limit=1,
)
if not provider:
return {"error": "No active provider found"}
# Get prompts
prompts = provider.list_prompts()
return {"prompts": prompts}
except Exception as e:
_logger.exception("Error listing prompts: %s", str(e))
return {"error": str(e)}
@http.route(
"/api/llm/prompts/get", type="json", auth="user", csrf=False, methods=["POST"]
)
def get_prompt(self, name, provider_id=None, arguments=None, **kwargs):
"""
Get a specific prompt with arguments
Args:
name: Name of the prompt to get
provider_id: ID of the provider (optional, uses default if not provided)
arguments: Dictionary of argument values
Returns:
dict: Prompt result with messages
"""
try:
# Parse arguments if provided as string
if arguments and isinstance(arguments, str):
try:
arguments = json.loads(arguments)
except json.JSONDecodeError:
return {"error": "Invalid arguments JSON format"}
# Get provider
if provider_id:
provider = request.env["llm.provider"].browse(int(provider_id))
if not provider.exists():
return {"error": "Provider not found"}
else:
# Get default provider
provider = request.env["llm.provider"].search(
[("active", "=", True)],
limit=1,
)
if not provider:
return {"error": "No active provider found"}
# Get prompt
try:
result = provider.get_prompt(name, arguments)
return result
except Exception as e:
return {"error": str(e)}
except Exception as e:
_logger.exception("Error getting prompt: %s", str(e))
return {"error": str(e)}
@@ -0,0 +1,26 @@
import logging
from odoo import http
from odoo.http import request
_logger = logging.getLogger(__name__)
class LLMThreadControllerExtended(http.Controller):
@http.route("/llm/thread/set_prompt", type="json", auth="user")
def set_thread_prompt(self, thread_id, prompt_id):
"""Set the prompt for a thread
Args:
thread_id (int): ID of the thread to update
prompt_id (int): ID of the prompt to set, or False to clear
Returns:
bool: True if successful, False otherwise
"""
thread = request.env["llm.thread"].browse(int(thread_id))
if not thread.exists():
return False
# Update the thread with the prompt
return thread.write({"prompt_id": prompt_id or False})
@@ -0,0 +1,57 @@
from odoo import http
from odoo.http import request
class LLMAssistantController(http.Controller):
@http.route("/llm/thread/set_assistant", type="json", auth="user")
def set_thread_assistant(self, thread_id, assistant_id=False):
"""Set the assistant for a thread and return thread-specific evaluated default values
Args:
thread_id (int): ID of the thread to update
assistant_id (int, optional): ID of the assistant to set, or False to clear
Returns:
dict: Result of the operation with evaluated default values if successful
"""
# Get thread and assistant using the model method
thread, assistant, error = request.env["llm.thread"].get_thread_and_assistant(
thread_id, assistant_id
)
if error:
return error
# Set the assistant on the thread
result = thread.set_assistant(assistant_id if assistant else False)
# Return basic result if no assistant was set or operation failed
if not assistant or not result:
return {
"success": bool(result),
"thread_id": thread_id,
"assistant_id": assistant_id if assistant else False,
}
# Get assistant values with the thread context using the model method
return assistant.get_assistant_values(thread)
@http.route("/llm/thread/get_assistant_values", type="json", auth="user")
def get_thread_assistant_values(self, thread_id, assistant_id):
"""Get thread-specific evaluated default values for an assistant
Args:
thread_id (int): ID of the thread
assistant_id (int): ID of the assistant
Returns:
dict: Result with evaluated default values
"""
# Get thread and assistant using the model method
thread, assistant, error = request.env["llm.thread"].get_thread_and_assistant(
thread_id, assistant_id
)
if error:
return error
# Get assistant values with the thread context using the model method
return assistant.get_assistant_values(thread)
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Assistant Creator Assistant -->
<record id="llm_assistant_creator" model="llm.assistant">
<field name="name">Assistant Creator</field>
<field name="prompt_id" ref="llm_prompt_assistant_creator" />
<field name="active" eval="True" />
<field
name="tool_ids"
eval="[(6, 0, [
ref('llm_tool.llm_tool_odoo_model_inspector'),
ref('llm_tool.llm_tool_odoo_record_retriever'),
ref('llm_tool.llm_tool_odoo_record_creator'),
ref('llm_tool.llm_tool_odoo_record_updater')
])]"
/>
</record>
<!-- Website Builder Assistant -->
<record id="llm_assistant_website_builder" model="llm.assistant">
<field name="name">Website Builder Assistant</field>
<field name="prompt_id" ref="llm_prompt_website_builder" />
<field name="active" eval="True" />
<field
name="tool_ids"
eval="[(6, 0, [
ref('llm_tool.llm_tool_odoo_model_inspector'),
ref('llm_tool.llm_tool_odoo_record_retriever'),
ref('llm_tool.llm_tool_odoo_record_creator'),
ref('llm_tool.llm_tool_odoo_record_updater')
])]"
/>
</record>
</odoo>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<!-- Default Prompt Categories -->
<record id="category_general" model="llm.prompt.category">
<field name="name">General</field>
<field name="code">general</field>
<field name="sequence">10</field>
</record>
<record id="category_technical" model="llm.prompt.category">
<field name="name">Technical</field>
<field name="code">technical</field>
<field name="sequence">20</field>
</record>
<record id="category_business" model="llm.prompt.category">
<field name="name">Business</field>
<field name="code">business</field>
<field name="sequence">30</field>
</record>
<record id="category_creative" model="llm.prompt.category">
<field name="name">Creative</field>
<field name="code">creative</field>
<field name="sequence">40</field>
</record>
</odoo>
@@ -0,0 +1,240 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Assistant Creator Prompt Template -->
<record id="llm_prompt_assistant_creator" model="llm.prompt">
<field name="name">Assistant Creator Prompt</field>
<field
name="description"
>Specialized prompt template for the Assistant Creator assistant</field>
<field name="category_id" ref="llm_assistant.category_general" />
<field
name="tag_ids"
eval="[(6, 0, [ref('llm_assistant.tag_assistant')])]"
/>
<field name="format">text</field>
<field
name="template"
><![CDATA[You are an Assistant Creator Assistant.
Your goal is to guide users through the complete process of creating and configuring specialized AI assistants in Odoo, ensuring all required fields are properly set, appropriate tools are attached, and the assistant's purpose is clearly defined with optimal prompting.
Background: Expert in the llm.assistant model structure and relationships with other LLM models in Odoo.
Familiar with best practices for designing effective AI assistants including role definition, goal setting, and instruction crafting.
Understanding of the available tools and their appropriate use cases for different assistant types.
Knowledge of system prompt templates and variable substitution patterns for creating effective assistant behaviors.
Committed to continuous self-improvement by learning from past assistant creation mistakes, tracking common errors, and refining approaches based on user feedback.
Instructions: When helping users create a new llm.assistant:
1. INSPECTION PHASE:
- Use the odoo_model_inspector tool to examine the llm.assistant model structure
- Use the odoo_fields_inspector tool to identify all required and optional fields
- Use the odoo_record_retriever tool to find examples of existing assistants when needed
2. PLANNING PHASE:
- Help the user define a clear purpose for their assistant
- Guide them in crafting an effective role, goal, and background
- Create detailed, step-by-step instructions that clearly outline the assistant's workflow and decision-making process
- Structure instructions with numbered steps, clear sections, and explicit guidance on how to handle different scenarios
- Recommend appropriate tools based on the assistant's intended function:
* Knowledge tools for information retrieval assistants
* Odoo tools for record manipulation assistants
* Web tools for internet-connected assistants
- Always include technical context about relevant models and fields in the assistant's background and instructions if applicable.
* Use odoo_model_inspector and odoo_fields_inspector to gather this technical information for required data only
* Document field types, relationships, and constraints that are relevant to the assistant's function
* Include information about model methods and business logic when applicable
3. CREATION PHASE:
- Use the odoo_record_creator tool to create the llm.assistant record with all required fields
- Ensure the provider_id and model_id (these are ids of record) are available, you can use odoo_record_retriever to look for already available providers and models using ilike operator for best results.
- Set appropriate tool_ids(llm.tool's ids), you can use odoo_record_retriever to look for already available tools(llm.tool) using ilike operator for best results.
4. VALIDATION PHASE:
- Verify the created assistant has all necessary components
- Test the system_prompt with the defined variables
- Ensure instructions are comprehensive, detailed, and provide clear guidance for all expected use cases
- Suggest improvements to the assistant configuration
- If applicable ensure the assistant has sufficient technical context about the models and fields it will work with
- Recommend additional technical details if the assistant's domain knowledge seems incomplete
5. CONTINUOUS IMPROVEMENT:
- Include instructions for the assistant to learn from mistakes and adjust its behavior
- Add specific guidance on how to identify errors or misunderstandings
- Provide mechanisms for the assistant to correct itself when it makes mistakes
- Encourage the assistant to seek feedback and improve based on user interactions
- Document common pitfalls and how to avoid them in the assistant's instructions
TOOL USAGE GUIDE:
- odoo_model_inspector: For understanding model structure and relationships
- odoo_fields_inspector: For identifying required and optional fields
- odoo_record_retriever: For finding examples and references
- odoo_record_creator: For creating new assistant records
- odoo_record_updater: For modifying existing assistant records
IMPORTANT TOOL USAGE RULES:
- Before using any tool, always verify you're using the correct schema and parameter format
- Check tool documentation to understand required and optional parameters
- Ensure all parameter values are properly formatted (strings, integers, booleans, etc.)
- Use proper data structures (lists, dictionaries) as required by each tool
Available fields for llm.assistant model:
- name: Name of the assistant
- provider_id: LLM provider to use, related to llm.provider model (e.g., OpenAI)
- model_id: Specific model to use, related to llm.model model (must be compatible with the provider)
- prompt_id: Prompt template to use for generating system prompt
- default_values: JSON with default values for prompt variables
- tool_ids: Tools that the assistant can use, related to llm.tool model
IMPORTANT: Learn from your own mistakes during the assistant creation process. When errors occur or users provide feedback, document these issues and adjust your approach accordingly. Continuously improve your guidance by tracking what works well and what doesn't. Apply these lessons to future assistant creation tasks to provide increasingly effective assistance.]]></field>
<field name="active" eval="True" />
</record>
<!-- Website Builder Prompt Template -->
<record id="llm_prompt_website_builder" model="llm.prompt">
<field name="name">Website Builder Prompt</field>
<field
name="description"
>Specialized prompt template for the Website Builder assistant</field>
<field name="category_id" ref="llm_assistant.category_general" />
<field
name="tag_ids"
eval="[(6, 0, [ref('llm_assistant.tag_assistant')])]"
/>
<field name="format">text</field>
<field
name="template"
><![CDATA[You are a Website Builder Assistant.
Your goal is to ensure the website's content, structure, and functionality are updated correctly within the Odoo system.
Implement changes safely and methodically, keeping the web page in a is_published = False state until stakeholders verify that all updates are correct and complete.
Provide clear preview links (assuming localhost:8069 by default unless a different domain is specified) for easy verification of the changes before publishing.
Background: The website is built on the Odoo 16 system, meaning that web content (like pages, forms, and layouts) is managed through records and XML view definitions.
Common models involved include website.page for page content, ir.ui.view for XML structure and templates, website.menu for navigation, and theme specific models/utils.
Updates vary from simple field additions (as in the case of a contact form update) to more complex layout adjustments or new feature implementations.
Since the system's data (models and views) drive the website's presentation, any changes should first be tested in is_published = False mode. This allows stakeholders to review updates before they go live.
Instructions:
1. Planning and Discovery
- Identify the target webpage and its underlying model (for example, website.page) that holds the required XML view definition.
- Review the current XML architecture (form layout, field definitions, etc.) to fully understand where and how changes should be implemented.
- Inform the user about the identified model and XML structure and what is your plan to implement the changes.
2. Gathering Data and Proposing Changes
- Retrieve the view record(s) for the specific page or feature you need to update (e.g., by filtering on the URL or page identifier).
- Use ilike operator with at least limit set to 5 to get better context from odoo 16 system. If desired result is not present, increase limit or change other params to get more results.
- ALWAYS inspect the XML to pinpoint the appropriate insertion points for your changes.
- After updating anything if it is related to a page, then make the page is_published = False so stakeholders can preview the changes.
- Always verify which view or template is actually in use on the live website. Even though multiple views can be related to a similar functionality (like the Contact Us form), updating the wrong one won't affect the visible UI.
- Confirm the active view used by the live site.
- Double-check domain filters and view names when applying updates.
3. Execution
- Inspect to understand what kind of changes you need to make. Make small changes in step by step rather than making a lot of change at once.
- If any error is encountered, inform the user and provide a clear explanation of the issue. And try again to fix the issue.
- Always ensure the updated page remains in is_published = False state after the change so that users can preview it before publishing.
- Provide the URL for review (defaulting to "http://localhost:8069/[page_url]" unless a different website domain is specified).
4. Final Review and Publishing
- Inform stakeholders that the change has been implemented in is_published = False state for website.page and provide the preview URL.
- Once feedback is received and adjustments are made if necessary, publish the changes to make them live.
Example Case:
Objective: Add a "Date of Birth" field to the existing contact form on the "Contact Us" page of an Odoo website.
Step-by-Step Process:
1. Identify the Web Page Record:
* Model: website.page
* Fields Needed: id, url, view_id, is_published
* Action: Query the website.page model for URL containing "/contactus" to locate properties such as ID (3), associated view ID (491), and publication status (published).
2. Retrieve the XML Structure:
* Model: ir.ui.view
* Fields Needed: id, arch_db (to access and edit the HTML/XML structure)
* Action: Use the view ID (491) extracted from the website.page record to fetch the form's XML layout, identifying where to insert the new field.
3. Publish State Adjustment:
* Model: website.page
* Action: Set is_published to False for the page with ID 3.
* Purpose: To keep the page out of production, allowing for stakeholder review without affecting the live environment.
4. Modify the XML View:
* Model: ir.ui.view
* Target Field: arch_db
* Modification: Insert the new Date of Birth input field into the form's structure within the XML as follows:
<div class="mb-0 py-2 col-12 s_website_form_field s_website_form_custom" data-type="date" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="contact8">
<span class="s_website_form_label_content">Date of Birth</span>
</label>
<div class="col-sm">
<input id="contact8" type="date" class="form-control s_website_form_input" name="date_of_birth"/>
</div>
</div>
</div>
5. Preview and Review:
* Provision: Provided the link http://localhost:8069/contactus to review the changes before publishing.
* Feedback Loop: Await stakeholder approval or further modification requests before setting is_published to True for going live.
IMPORTANT: Always ensure changes are made in is_published = False state for the related website.page first and provide preview links for verification before publishing whenever any changes were made for website pages.]]></field>
<field name="active" eval="True" />
</record>
<!-- General Assistant Prompt Template (for backwards compatibility) -->
<record id="llm_prompt_assistant" model="llm.prompt">
<field name="name">General Assistant Template</field>
<field
name="description"
>General template for assistant system prompts with role, goal, background, and instructions</field>
<field name="category_id" ref="llm_assistant.category_general" />
<field
name="tag_ids"
eval="[(6, 0, [ref('llm_assistant.tag_assistant')])]"
/>
<field name="format">text</field>
<field
name="template"
><![CDATA[You are a {{ role }}.
Your goal is to {{ goal }}
Background: {{ background }}
Instructions: {{ instructions }}
{{ footer }}]]></field>
<field
name="arguments_json"
><![CDATA[{
"role": {
"type": "string",
"description": "The role of the assistant (e.g., 'Assistant', 'Customer Support', 'Data Analyst')",
"required": true
},
"goal": {
"type": "string",
"description": "The primary goal or objective of this assistant",
"required": true
},
"background": {
"type": "string",
"description": "Background information for the assistant to understand its context",
"required": true
},
"instructions": {
"type": "string",
"description": "Specific instructions for the assistant to follow",
"required": true
},
"footer": {
"type": "string",
"description": "Optional footer text to add to the end of the prompt",
"required": false,
"default": ""
}
}]]></field>
<field name="active" eval="True" />
</record>
</odoo>
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<!-- Export Template for LLM Prompts -->
<record id="llm_prompt_export_template" model="ir.exports">
<field name="name">LLM Prompt Export</field>
<field name="resource">llm.prompt</field>
</record>
<!-- Export Fields for LLM Prompt Export -->
<record id="llm_prompt_export_line_name" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_export_template" />
<field name="name">name</field>
</record>
<record id="llm_prompt_export_line_description" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_export_template" />
<field name="name">description</field>
</record>
<record id="llm_prompt_export_line_active" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_export_template" />
<field name="name">active</field>
</record>
<record id="llm_prompt_export_line_category" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_export_template" />
<field name="name">category_id</field>
</record>
<record id="llm_prompt_export_line_tags" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_export_template" />
<field name="name">tag_ids</field>
</record>
<record id="llm_prompt_export_line_template" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_export_template" />
<field name="name">template</field>
</record>
<record id="llm_prompt_export_line_format" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_export_template" />
<field name="name">format</field>
</record>
<record id="llm_prompt_export_line_arguments_json" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_export_template" />
<field name="name">arguments_json</field>
</record>
<record id="llm_prompt_export_line_example_args" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_export_template" />
<field name="name">example_args</field>
</record>
<!-- Additional Export Template for Categories and Tags (for reference) -->
<record id="llm_prompt_category_export_template" model="ir.exports">
<field name="name">LLM Prompt Categories Export</field>
<field name="resource">llm.prompt.category</field>
</record>
<record id="llm_prompt_category_export_line_name" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_category_export_template" />
<field name="name">name</field>
</record>
<record id="llm_prompt_category_export_line_code" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_category_export_template" />
<field name="name">code</field>
</record>
<record id="llm_prompt_category_export_line_parent" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_category_export_template" />
<field name="name">parent_id</field>
</record>
<record
id="llm_prompt_category_export_line_description"
model="ir.exports.line"
>
<field name="export_id" ref="llm_prompt_category_export_template" />
<field name="name">description</field>
</record>
<record
id="llm_prompt_category_export_line_sequence"
model="ir.exports.line"
>
<field name="export_id" ref="llm_prompt_category_export_template" />
<field name="name">sequence</field>
</record>
<!-- Tags Export Template -->
<record id="llm_prompt_tag_export_template" model="ir.exports">
<field name="name">LLM Prompt Tags Export</field>
<field name="resource">llm.prompt.tag</field>
</record>
<record id="llm_prompt_tag_export_line_name" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_tag_export_template" />
<field name="name">name</field>
</record>
<record id="llm_prompt_tag_export_line_color" model="ir.exports.line">
<field name="export_id" ref="llm_prompt_tag_export_template" />
<field name="name">color</field>
</record>
</odoo>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<!-- Default Tags -->
<record id="tag_technical" model="llm.prompt.tag">
<field name="name">Technical</field>
</record>
<record id="tag_business" model="llm.prompt.tag">
<field name="name">Business</field>
</record>
<record id="tag_creative" model="llm.prompt.tag">
<field name="name">Creative</field>
</record>
<record id="tag_data" model="llm.prompt.tag">
<field name="name">Data</field>
</record>
<record id="tag_system" model="llm.prompt.tag">
<field name="name">System</field>
</record>
<record id="tag_assistant" model="llm.prompt.tag">
<field name="name">Assistant</field>
</record>
</odoo>
+255
View File
@@ -0,0 +1,255 @@
==============================
LLM Assistant for Odoo
==============================
Advanced AI assistant management with integrated prompt templates, testing capabilities, and intelligent configuration orchestration.
**Module Type:** 🚀 Entry Point
.. image:: ../static/description/llm_assistant_architecture.png
:alt: LLM Assistant Architecture
:width: 100%
Installation
============
What to Install
---------------
This is the **main entry point** for AI chat features in Odoo.
**Basic AI Chat Setup:**
.. code-block:: bash
odoo-bin -d your_db -i llm_assistant,llm_openai
Auto-Installed Dependencies
---------------------------
These are pulled in automatically:
- ``llm`` (core infrastructure)
- ``llm_tool`` (function calling)
- ``llm_thread`` (chat interface)
- ``mail`` (Odoo messaging)
Choose a Provider
-----------------
+----------+--------------+----------------------------+
| Provider | Module | Best For |
+==========+==============+============================+
| OpenAI | ``llm_openai`` | GPT-4, most capable |
+----------+--------------+----------------------------+
| Ollama | ``llm_ollama`` | Local/private, no API costs|
+----------+--------------+----------------------------+
| Mistral | ``llm_mistral``| European, fast |
+----------+--------------+----------------------------+
Common Setups
-------------
+----------------------------------+------------------------------------------------------+
| I want to... | Install |
+==================================+======================================================+
| Chat with GPT-4 in Odoo | ``llm_assistant`` + ``llm_openai`` |
+----------------------------------+------------------------------------------------------+
| Use local AI (privacy) | ``llm_assistant`` + ``llm_ollama`` |
+----------------------------------+------------------------------------------------------+
| Add document search (RAG) | Above + ``llm_knowledge`` + ``llm_pgvector`` |
+----------------------------------+------------------------------------------------------+
| Connect Claude Desktop | Above + ``llm_mcp_server`` |
+----------------------------------+------------------------------------------------------+
| Build domain-specific assistant | Extend (see ``llm_assistant_account_invoice``) |
+----------------------------------+------------------------------------------------------+
Overview
========
The LLM Assistant module provides sophisticated AI assistant management that goes far beyond simple chatbots. It serves as the intelligent configuration layer that orchestrates how AI models interact with Odoo data.
Core Capabilities
-----------------
- **AI Assistant Configuration** - Define specialized AI personas with specific roles
- **Integrated Prompt Management** - Consolidated prompt template system
- **Template Testing** - Built-in testing wizard for prompt validation
- **Context Orchestration** - Intelligent mapping between Odoo data and AI inputs
- **Tool Management** - Configure available tools and their usage patterns
Key Features
============
Consolidated Architecture
-------------------------
- Prompt templates integrated into assistant management
- Enhanced testing wizard with context simulation
- Streamlined UI with unified assistant and prompt selection
- Auto-argument detection for template variables
- Schema synchronization between templates and forms
Assistant Types
---------------
Chat Assistants
~~~~~~~~~~~~~~~
.. code-block:: python
assistant = env['llm.assistant'].create({
'name': 'Customer Support Bot',
'role': 'Customer Service Representative',
'goal': 'Provide helpful and accurate customer support',
'background': 'Expert in our products with access to CRM data',
'instructions': '''
- Always be polite and professional
- Use customer history for personalized responses
- Escalate complex issues to human agents
''',
'tool_ids': [(6, 0, [crm_tool.id, knowledge_tool.id])]
})
Content Generation Assistants
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
assistant = env['llm.assistant'].create({
'name': 'Marketing Content Creator',
'role': 'Marketing Specialist',
'goal': 'Create compelling marketing content',
'prompt_id': marketing_template.id,
})
Prompt Template System
======================
Template Management
-------------------
.. code-block:: python
prompt = env['llm.prompt'].create({
'name': 'Sales Email Generator',
'template': '''
Generate a personalized sales email for {{customer_name}}
regarding {{product_name}}.
Customer Context:
- Company: {{customer_company}}
- Industry: {{industry}}
Email should be {{tone}} and focus on {{key_benefits}}.
''',
'format': 'text',
})
# Arguments automatically detected
prompt.auto_detect_arguments()
Testing & Validation
--------------------
.. code-block:: python
wizard = env['llm.assistant.test.wizard'].create({
'assistant_id': assistant.id,
'test_context': {
'customer_name': 'John Smith',
'product_name': 'Enterprise Software'
}
})
Configuration Guide
===================
Basic Setup
-----------
1. **Set up AI Provider:**
Navigate to **LLM → Configuration → Providers**:
- Create a new provider with your API credentials
- Click **Fetch Models** to import available models
2. **Create AI Assistants:**
Go to **LLM → Configuration → Assistants**:
- Configure assistants with specific roles
- Assign prompt templates and available tools
3. **Start Chatting:**
Navigate to **LLM → Chat** to start conversations
API Reference
=============
Assistant Methods
-----------------
.. code-block:: python
# Get system prompt with context
system_prompt = assistant.get_system_prompt(context={
'customer_name': 'John Doe'
})
# Prepare conversation context
context = assistant.prepare_context(
record=sale_order,
user_input="Tell me about pricing"
)
# Get available tools
tools = assistant.get_available_tools()
Technical Specifications
========================
Module Information
------------------
- **Name**: LLM Assistant
- **Version**: 18.0.1.5.0
- **Category**: Productivity
- **License**: LGPL-3
- **Dependencies**: ``llm``, ``mail``
- **Author**: Apexive Solutions LLC
Key Models
----------
- **``llm.assistant``**: Main assistant configuration
- **``llm.prompt``**: Integrated prompt template management
- **``llm.prompt.category``**: Template categorization
- **``llm.assistant.test.wizard``**: Testing and validation
Related Modules
===============
- **``llm``** - Base infrastructure and provider management
- **``llm_thread``** - Chat interfaces and conversation management
- **``llm_tool``** - Function calling and Odoo integration
- **``llm_generate``** - Content generation with assistant integration
- **``llm_knowledge``** - RAG and knowledge base integration
- **``llm_assistant_account_invoice``** - Example domain-specific assistant
Resources
=========
- `GitHub Repository <https://github.com/apexive/odoo-llm>`_
- `Architecture Overview <../OVERVIEW.md>`_
License
=======
This module is licensed under `LGPL-3 <https://www.gnu.org/licenses/lgpl-3.0.html>`_.
----
*© 2025 Apexive Solutions LLC. All rights reserved.*
+348
View File
@@ -0,0 +1,348 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * llm_assistant
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-19 02:54+0000\n"
"PO-Revision-Date: 2025-03-19 02:54+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__message_needaction
msgid "Action Needed"
msgstr "Ação Necessária"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__active
msgid "Active"
msgstr "Ativo"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_thread__assistant_id
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_form
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_thread_search_inherit_assistant
msgid "Assistant"
msgstr "Assistante"
#. module: llm_assistant
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_form
msgid "Assistant Configuration"
msgstr "Configuração do Assistante"
#. module: llm_assistant
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_form
msgid "Assistant Name"
msgstr "Nome do Assistante"
#. module: llm_assistant
#: model:ir.actions.act_window,name:llm_assistant.action_llm_assistant
#: model:ir.ui.menu,name:llm_assistant.menu_llm_assistant
msgid "Assistants"
msgstr "Assistantes"
#. module: llm_assistant
#: model_terms:ir.actions.act_window,help:llm_assistant.action_llm_assistant
msgid ""
"Assistants can be configured with specific roles, goals, and tools to enhance "
"your AI interactions."
msgstr "Os assistantes podem ser configurados com funções, objetivos e ferramentas específicas para aprimorar suas interações com a IA."
#. module: llm_assistant
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_search
msgid "Archived"
msgstr "Arquivado"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__message_attachment_count
msgid "Attachment Count"
msgstr "Contagem de anexos"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__background
msgid "Background"
msgstr ""
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_assistant__background
msgid "Background information for the assistant to understand its context"
msgstr ""
#. module: llm_assistant
#: model_terms:ir.actions.act_window,help:llm_assistant.action_llm_assistant
msgid "Create your first AI Assistant!"
msgstr ""
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__create_uid
msgid "Created by"
msgstr "Criado Por"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__create_date
msgid "Created on"
msgstr "Criado em"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__display_name
msgid "Display Name"
msgstr "Nome Exibido"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__message_follower_ids
msgid "Followers"
msgstr "Seguidores"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__message_partner_ids
msgid "Followers (Partners)"
msgstr "Seguidores (Parceiros)"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__goal
msgid "Goal"
msgstr "Objetivo"
#. module: llm_assistant
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_search
msgid "Group By"
msgstr "Agrupar por"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__has_message
msgid "Has Message"
msgstr "Tem Mensagem"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__id
msgid "ID"
msgstr "ID"
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_assistant__message_needaction
msgid "If checked, new messages require your attention."
msgstr "Se marcado, novas mensagens requerem sua atenção."
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_assistant__message_has_error
msgid "If checked, some messages have a delivery error."
msgstr "Se marcado, algumas mensagens apresentaram erro de entrega."
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__instructions
msgid "Instructions"
msgstr "Instruções"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__message_is_follower
msgid "Is Follower"
msgstr "É Seguidor"
#. module: llm_assistant
#: model:ir.model,name:llm_assistant.model_llm_assistant
msgid "LLM Assistant"
msgstr "Assistante LLM"
#. module: llm_assistant
#: model:ir.model,name:llm_assistant.model_llm_thread
msgid "LLM Chat Thread"
msgstr "Tópico de Chat LLM"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant____last_update
msgid "Last Modified on"
msgstr "Última Modificação em"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__write_uid
msgid "Last Updated by"
msgstr "Última Atualização por"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__write_date
msgid "Last Updated on"
msgstr "Última Atualização em"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__message_main_attachment_id
msgid "Main Attachment"
msgstr "Anexo Principal"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__message_has_error
msgid "Message Delivery error"
msgstr "Erro na Entrega da Mensagem"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__message_ids
msgid "Messages"
msgstr "Mensagens"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__model_id
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_search
msgid "Model"
msgstr "Modelo"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__name
msgid "Name"
msgstr "Nome"
#. module: llm_assistant
#. odoo-javascript
#: code:addons/llm_assistant/static/src/components/llm_chat_thread_header/llm_chat_thread_header.xml:0
#, python-format
msgid "No Assistant"
msgstr "Nenhum Assistante"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__message_needaction_counter
msgid "Number of Actions"
msgstr "Número de Ações"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__message_has_error_counter
msgid "Number of errors"
msgstr "Número de Erros"
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_assistant__message_needaction_counter
msgid "Number of messages requiring action"
msgstr "Número de mensagens que exigem ação"
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_assistant__message_has_error_counter
msgid "Number of messages with delivery error"
msgstr "Número de mensagens com erro de entrega"
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_assistant__thread_count
msgid "Number of threads using this assistant"
msgstr "Número de tópicos usando este assistante"
#. module: llm_assistant
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_form
msgid "Open"
msgstr "Abrir"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__tool_ids
msgid "Preferred Tools"
msgstr "Ferramentas Preferidas"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__provider_id
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_search
msgid "Provider"
msgstr "Provedor"
#. module: llm_assistant
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_form
msgid "Related Threads"
msgstr "Tópicos Relacionados"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__role
msgid "Role"
msgstr "Função"
#. module: llm_assistant
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_search
msgid "Search Assistants"
msgstr "Buscar Assistantes"
#. module: llm_assistant
#. odoo-javascript
#: code:addons/llm_assistant/static/src/components/llm_chat_thread_header/llm_chat_thread_header.xml:0
#, python-format
msgid "Select Assistant"
msgstr "Selecionar Assistante"
#. module: llm_assistant
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_form
msgid ""
"Select the tools this assistant may need. These tools will be auto selected when"
" you assign this assistant on LLM Chat Thread"
msgstr "Selecione as ferramentas que este assistante pode precisar. Essas ferramentas serão selecionadas automaticamente quando você atribuir este assistante a um Tópico de Chat LLM."
#. module: llm_assistant
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_form
msgid "Select tools for this assistant..."
msgstr "Selecione ferramentas para este assistante..."
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_assistant__instructions
msgid "Specific instructions for the assistant to follow"
msgstr "Instruções específicas que o assistante deve seguir"
#. module: llm_assistant
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_form
msgid "System Prompt"
msgstr "Prompt do Sistema"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__system_prompt
msgid "System Prompt Template"
msgstr "Modelo de Prompt do Sistema"
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_assistant__system_prompt
msgid ""
"Template for the system prompt. Use {{ field_name }} placeholders for "
"variable substitution."
msgstr "Modelo para o prompt do sistema. Use {{ field_name }} como espaço reservado para substituição de variáveis."
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_thread__assistant_id
msgid "The assistant used for this thread"
msgstr "O assistante usado para este tópico"
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_assistant__goal
msgid "The primary goal or objective of this assistant"
msgstr "O objetivo principal deste assistante"
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_assistant__role
msgid ""
"The role of the assistant (e.g., 'Assistant', 'Customer Support', 'Data "
"Analyst')"
msgstr "O papel do assistante (ex.: 'Assistente', 'Suporte ao Cliente', 'Analista de Dados')"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__thread_count
msgid "Thread Count"
msgstr "Contagem de Tópicos"
#. module: llm_assistant
#: model:ir.model.fields,field_description:llm_assistant.field_llm_assistant__thread_ids
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_form
msgid "Threads"
msgstr "Tópicos"
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_assistant__thread_ids
msgid "Threads using this assistant"
msgstr "Tópicos que usam este assistante"
#. module: llm_assistant
#: model_terms:ir.ui.view,arch_db:llm_assistant.view_llm_assistant_form
msgid "Tools"
msgstr "Ferramentas"
#. module: llm_assistant
#: model:ir.model.fields,help:llm_assistant.field_llm_assistant__tool_ids
msgid "Tools that this assistant can use"
msgstr "Ferramentas que este assistante pode usar"
@@ -0,0 +1,75 @@
def migrate(cr, version):
"""
Pre-migration script to rename llm_prompt XML IDs to llm_assistant XML IDs
to avoid conflicts during consolidation.
"""
import logging
_logger = logging.getLogger(__name__)
_logger.info("Starting llm_prompt to llm_assistant XML ID migration")
# Mapping of old llm_prompt XML IDs to new llm_assistant XML IDs
xml_id_mappings = {
# Tags
"llm_prompt.tag_technical": "llm_assistant.tag_technical",
"llm_prompt.tag_business": "llm_assistant.tag_business",
"llm_prompt.tag_creative": "llm_assistant.tag_creative",
"llm_prompt.tag_data": "llm_assistant.tag_data",
"llm_prompt.tag_system": "llm_assistant.tag_system",
"llm_prompt.tag_assistant": "llm_assistant.tag_assistant",
# Categories
"llm_prompt.category_general": "llm_assistant.category_general",
"llm_prompt.category_technical": "llm_assistant.category_technical",
"llm_prompt.category_business": "llm_assistant.category_business",
"llm_prompt.category_creative": "llm_assistant.category_creative",
# Export templates
"llm_prompt.llm_prompt_export_template": "llm_assistant.llm_prompt_export_template",
"llm_prompt.llm_prompt_export_line_name": "llm_assistant.llm_prompt_export_line_name",
"llm_prompt.llm_prompt_export_line_description": "llm_assistant.llm_prompt_export_line_description",
"llm_prompt.llm_prompt_export_line_active": "llm_assistant.llm_prompt_export_line_active",
"llm_prompt.llm_prompt_export_line_category": "llm_assistant.llm_prompt_export_line_category",
"llm_prompt.llm_prompt_export_line_tags": "llm_assistant.llm_prompt_export_line_tags",
"llm_prompt.llm_prompt_export_line_template": "llm_assistant.llm_prompt_export_line_template",
"llm_prompt.llm_prompt_export_line_format": "llm_assistant.llm_prompt_export_line_format",
"llm_prompt.llm_prompt_export_line_arguments_json": "llm_assistant.llm_prompt_export_line_arguments_json",
"llm_prompt.llm_prompt_export_line_example_args": "llm_assistant.llm_prompt_export_line_example_args",
"llm_prompt.llm_prompt_category_export_template": "llm_assistant.llm_prompt_category_export_template",
"llm_prompt.llm_prompt_category_export_line_name": "llm_assistant.llm_prompt_category_export_line_name",
"llm_prompt.llm_prompt_category_export_line_code": "llm_assistant.llm_prompt_category_export_line_code",
"llm_prompt.llm_prompt_category_export_line_parent": "llm_assistant.llm_prompt_category_export_line_parent",
"llm_prompt.llm_prompt_category_export_line_description": "llm_assistant.llm_prompt_category_export_line_description",
"llm_prompt.llm_prompt_category_export_line_sequence": "llm_assistant.llm_prompt_category_export_line_sequence",
"llm_prompt.llm_prompt_tag_export_template": "llm_assistant.llm_prompt_tag_export_template",
"llm_prompt.llm_prompt_tag_export_line_name": "llm_assistant.llm_prompt_tag_export_line_name",
"llm_prompt.llm_prompt_tag_export_line_color": "llm_assistant.llm_prompt_tag_export_line_color",
}
# Update XML IDs in ir_model_data
for old_xml_id, new_xml_id in xml_id_mappings.items():
old_module, old_name = old_xml_id.split(".")
new_module, new_name = new_xml_id.split(".")
# Check if the old XML ID exists
cr.execute(
"""
SELECT id FROM ir_model_data
WHERE module = %s AND name = %s
""",
(old_module, old_name),
)
if cr.fetchone():
# Update the XML ID
cr.execute(
"""
UPDATE ir_model_data
SET module = %s, name = %s
WHERE module = %s AND name = %s
""",
(new_module, new_name, old_module, old_name),
)
_logger.info(f"Renamed XML ID: {old_xml_id} -> {new_xml_id}")
_logger.info("Completed llm_prompt to llm_assistant XML ID migration")
@@ -0,0 +1,85 @@
import logging
from odoo import SUPERUSER_ID, api
_logger = logging.getLogger(__name__)
def migrate(cr, version):
"""
Generate dotted codes for llm.assistant records based on their category hierarchy.
The code format will be: parent_category.code.subcategory.code.subcategory.code...
"""
with api.Environment.manage():
env = api.Environment(cr, SUPERUSER_ID, {})
# Get all assistants that don't have a code yet
assistants = env["llm.assistant"].search(
["|", ("code", "=", False), ("code", "=", "")]
)
_logger.info(f"Found {len(assistants)} assistants without codes")
for assistant in assistants:
# Skip if assistant doesn't have a category
if not assistant.category_id:
_logger.warning(
f"Assistant '{assistant.name}' (ID: {assistant.id}) has no category"
)
continue
# Build the dotted code from category hierarchy
code_parts = []
current_category = assistant.category_id
# Walk up the category tree collecting codes
while current_category:
if current_category.code:
code_parts.insert(0, current_category.code)
else:
# If category doesn't have a code, use a sanitized version of its name
sanitized_name = current_category.name.lower()
sanitized_name = sanitized_name.replace(" ", "_")
sanitized_name = "".join(
c for c in sanitized_name if c.isalnum() or c == "_"
)
code_parts.insert(0, sanitized_name)
current_category = current_category.parent_id
# Generate the dotted code
if code_parts:
new_code = ".".join(code_parts)
# Check if this code already exists
existing = env["llm.assistant"].search(
[("code", "=", new_code), ("id", "!=", assistant.id)]
)
if existing:
# Add a suffix to make it unique
suffix = 1
while existing:
test_code = f"{new_code}_{suffix}"
existing = env["llm.assistant"].search(
[("code", "=", test_code), ("id", "!=", assistant.id)]
)
if not existing:
new_code = test_code
suffix += 1
assistant.code = new_code
_logger.info(
f"Set code '{new_code}' for assistant '{assistant.name}' (ID: {assistant.id})"
)
else:
_logger.warning(
f"Could not generate code for assistant '{assistant.name}' (ID: {assistant.id})"
)
# Commit the changes
cr.commit()
_logger.info(
"Migration completed: Generated dotted codes for assistants based on category hierarchy"
)
@@ -0,0 +1,6 @@
from . import llm_thread
from . import llm_assistant
from . import llm_assistant_action_mixin
from . import llm_prompt
from . import llm_prompt_tag
from . import llm_prompt_category
@@ -0,0 +1,61 @@
import jsonschema
from odoo import _
# Schema for individual argument objects
ARGUMENT_SCHEMA = {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"string",
"number",
"integer",
"boolean",
"array",
"object",
"resource",
"context",
],
},
"description": {"type": "string"},
"required": {"type": "boolean"},
"default": {}, # Any type allowed
"examples": {"type": "array"},
},
"additionalProperties": True, # Allow any other properties for flexibility
}
# Schema for the complete arguments_json field
ARGUMENTS_JSON_SCHEMA = {
"type": "object",
"patternProperties": {"^[a-zA-Z0-9_]+$": ARGUMENT_SCHEMA},
"additionalProperties": False,
}
def validate_arguments_schema(schema_text):
"""
Validate an arguments schema against the ARGUMENTS_JSON_SCHEMA.
Args:
schema_text (str): JSON string to validate
Returns:
tuple: (is_valid, error_message)
"""
import json
try:
schema = json.loads(schema_text)
jsonschema.validate(instance=schema, schema=ARGUMENTS_JSON_SCHEMA)
return True, ""
except json.JSONDecodeError as e:
return False, _("Invalid JSON: %s") % str(e)
except jsonschema.exceptions.ValidationError as e:
path = ".".join(str(p) for p in e.path) if e.path else ""
message = f"{path}: {e.message}" if path else e.message
return False, _("Schema validation error: %s") % message
except Exception as e:
return False, _("Validation error: %s") % str(e)
@@ -0,0 +1,546 @@
import json
import logging
from odoo import api, fields, models
from ..utils import render_template
_logger = logging.getLogger(__name__)
class LLMAssistant(models.Model):
_name = "llm.assistant"
_description = "LLM Assistant"
_inherit = ["mail.thread"]
_order = "name"
name = fields.Char(
string="Name",
required=True,
tracking=True,
)
active = fields.Boolean(default=True, tracking=True)
# Assistant configuration
provider_id = fields.Many2one(
"llm.provider",
string="Provider",
ondelete="restrict",
tracking=True,
)
model_id = fields.Many2one(
"llm.model",
string="Model",
domain="[('provider_id', '=', provider_id)]",
ondelete="restrict",
tracking=True,
required=False,
)
is_public = fields.Boolean(
string="Public",
default=False,
help="If checked, this assistant will be available to all users",
)
allowed_group_ids = fields.Many2many(
"res.groups",
"llm_assistant_group_rel",
"assistant_id",
"group_id",
string="Allowed Groups",
help="Groups that can access this assistant. If empty and not public, only internal users can access it.",
)
code = fields.Char(
string="Code",
help="Unique code identifier for the assistant (e.g., roleplay, avatar_generation)",
index=True,
)
res_model = fields.Char(
string="Related Model",
help="Model that this assistant is associated with (e.g., fleek.character)",
)
is_default = fields.Boolean(
string="Is Default",
default=False,
help="If enabled, this assistant will be used as the default for its model/category",
)
# Prompt template integration
prompt_id = fields.Many2one(
"llm.prompt",
string="Prompt Template",
ondelete="restrict",
tracking=True,
required=True,
auto_join=True,
help="Prompt template to use for generating system prompts",
)
# Related fields from prompt - for display and search
category_id = fields.Many2one(
"llm.prompt.category",
string="Category",
related="prompt_id.category_id",
store=True,
readonly=True,
help="Category inherited from the prompt template",
)
tag_ids = fields.Many2many(
"llm.prompt.tag",
string="Tags",
related="prompt_id.tag_ids",
readonly=True,
help="Tags inherited from the prompt template",
)
# Default values for prompt variables as JSON
default_values = fields.Text(
string="Default Values",
help="JSON object with default values for prompt variables. Can include template expressions that will be evaluated.",
default="{}",
tracking=True,
)
# Whether default values contain expressions to be evaluated
has_dynamic_defaults = fields.Boolean(
string="Has Dynamic Defaults",
default=False,
help="Enable if your default values contain template expressions that should be evaluated",
tracking=True,
)
# Tools configuration
tool_ids = fields.Many2many(
"llm.tool",
string="Preferred Tools",
help="Tools that this assistant can use",
tracking=True,
)
tool_calls_max = fields.Integer(
string="Max Tool Calls",
default=5,
help="Maximum number of consecutive tool calls allowed before breaking the loop to prevent infinite tool calling",
tracking=True,
)
# Stats
thread_count = fields.Integer(
string="Thread Count",
compute="_compute_thread_count",
help="Number of threads using this assistant",
)
thread_ids = fields.One2many(
"llm.thread",
"assistant_id",
string="Threads",
help="Threads using this assistant",
)
system_prompt_preview = fields.Text(
string="System Prompt Preview",
compute="_compute_system_prompt_preview",
help="Preview of the formatted system prompt based on the prompt template",
)
# Template fields - computed from prompt
template = fields.Text(
string="Template",
related="prompt_id.template",
readonly=True,
help="Template content from the associated prompt",
)
template_format = fields.Selection(
string="Template Format",
related="prompt_id.format",
readonly=True,
help="Format of the template (text, yaml, json)",
)
_sql_constraints = [
("unique_code", "UNIQUE(code)", "Assistant code must be unique."),
]
@api.depends("prompt_id", "default_values")
def _compute_system_prompt_preview(self):
"""Compute preview of the formatted system prompt"""
for assistant in self:
try:
if assistant.prompt_id:
# Get evaluated default values for preview
default_values = assistant.get_evaluated_default_values({})
messages = assistant.prompt_id.get_messages(default_values)
if messages:
# Find system message or use first message
system_msg = next(
(msg for msg in messages if msg.get("role") == "system"),
messages[0] if messages else None,
)
if system_msg and system_msg.get("content"):
content = system_msg["content"]
if isinstance(content, list) and content:
assistant.system_prompt_preview = content[0].get(
"text", ""
)
elif isinstance(content, str):
assistant.system_prompt_preview = content
else:
assistant.system_prompt_preview = str(content)
else:
assistant.system_prompt_preview = (
"No system prompt generated"
)
else:
assistant.system_prompt_preview = "No messages generated"
else:
assistant.system_prompt_preview = "No prompt template selected"
except Exception as e:
_logger.error(
"Error computing system prompt preview for assistant %s: %s",
assistant.name,
str(e),
)
assistant.system_prompt_preview = f"Error: {str(e)}"
@api.depends("thread_ids")
def _compute_thread_count(self):
"""Compute the number of threads using this assistant"""
for assistant in self:
assistant.thread_count = len(assistant.thread_ids)
def action_view_prompt(self):
"""Open the associated prompt for advanced template management"""
self.ensure_one()
if not self.prompt_id:
return False
return {
"name": "Prompt Template",
"type": "ir.actions.act_window",
"res_model": "llm.prompt",
"view_mode": "form",
"res_id": self.prompt_id.id,
"target": "current",
}
def action_view_threads(self):
"""Open the threads using this assistant"""
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id(
"llm_thread.llm_thread_action"
)
action["domain"] = [("assistant_id", "=", self.id)]
action["context"] = {"default_assistant_id": self.id}
return action
def _generate_template_json_from_schema(self, args_schema):
"""
Generate a template JSON structure from the prompt's argument schema.
Creates placeholders for all arguments, using defaults when available.
Args:
args_schema (dict): The arguments schema from the prompt
Returns:
dict: Template values with placeholders or defaults
"""
template_values = {}
for arg_name, arg_schema in args_schema.items():
# If there's a default value, use it
if "default" in arg_schema:
template_values[arg_name] = arg_schema["default"]
else:
# Generate appropriate placeholder based on type
arg_type = arg_schema.get("type", "string")
description = arg_schema.get("description", f"Value for {arg_name}")
if arg_type == "string":
# Create a descriptive placeholder
template_values[arg_name] = f"<Enter {description.lower()}>"
elif arg_type == "boolean":
template_values[arg_name] = False
elif arg_type in ["integer", "number"]:
template_values[arg_name] = 0
elif arg_type == "array":
template_values[arg_name] = []
elif arg_type == "object":
template_values[arg_name] = {}
else:
# Default to descriptive string placeholder
template_values[arg_name] = f"<Enter {description.lower()}>"
return template_values
def action_reset_defaults(self):
"""Reset default values to create template JSON from prompt's arguments schema"""
self.ensure_one()
if not self.prompt_id:
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": "No Prompt Template",
"message": "Please select a prompt template first.",
"type": "warning",
},
}
try:
# Get the prompt arguments schema
args_schema = json.loads(self.prompt_id.arguments_json or "{}")
if not args_schema:
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": "No Arguments Schema",
"message": "The selected prompt template has no arguments schema defined.",
"type": "info",
},
}
# Generate template JSON from schema
template_values = self._generate_template_json_from_schema(args_schema)
# Update default_values field with pretty-formatted JSON
self.default_values = json.dumps(template_values, indent=2)
# Count how many were defaults vs placeholders
defaults_count = sum(
1 for arg_schema in args_schema.values() if "default" in arg_schema
)
placeholders_count = len(template_values) - defaults_count
message_parts = []
if defaults_count > 0:
message_parts.append(f"{defaults_count} default values")
if placeholders_count > 0:
message_parts.append(f"{placeholders_count} placeholder values")
message = f"Template JSON created with {' and '.join(message_parts)}."
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": "Template JSON Generated",
"message": message,
"type": "success",
"next": {
"type": "ir.actions.client",
"tag": "reload",
},
},
}
except json.JSONDecodeError:
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": "Error",
"message": "Invalid JSON in prompt arguments schema.",
"type": "danger",
},
}
except Exception as e:
_logger.error(
"Error resetting defaults for assistant %s: %s", self.name, str(e)
)
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": "Error",
"message": f"Error generating template JSON: {str(e)}",
"type": "danger",
},
}
def get_evaluated_default_values(self, context):
"""
Evaluate default values using the provided context.
This is used by llm.thread to get assistant's default values with thread context.
Args:
context (dict): Context for template rendering
Returns:
dict: Evaluated default values
"""
self.ensure_one()
# Parse the default values JSON
try:
default_values = json.loads(self.default_values or "{}")
except json.JSONDecodeError:
_logger.warning(
"Invalid JSON in default_values for assistant %s", self.name
)
return {}
if not default_values:
return {}
# If we don't have dynamic defaults, return as-is
if not self.has_dynamic_defaults:
return default_values
# Render each default value as a template
evaluated_values = {}
for key, value in default_values.items():
if isinstance(value, str) and "{{" in value and "}}" in value:
try:
evaluated_values[key] = render_template(
template=value, context=context
)
except Exception as e:
_logger.warning(
"Error evaluating default value '%s' for assistant %s: %s",
key,
self.name,
str(e),
)
evaluated_values[key] = value # Keep original on error
else:
evaluated_values[key] = value
return evaluated_values
@api.model_create_multi
def create(self, vals_list):
"""Override create to ensure default_values is valid JSON"""
for vals in vals_list:
if "default_values" in vals and vals["default_values"]:
try:
json.loads(vals["default_values"])
except json.JSONDecodeError:
vals["default_values"] = "{}"
return super().create(vals_list)
@api.onchange("prompt_id")
def _onchange_prompt_id(self):
"""Update default_values when prompt_id changes to create template JSON
ONLY triggers when prompt_id actually changes, not on every field change.
"""
# Only proceed if we have a prompt_id and this is actually a change in prompt_id
if not self.prompt_id:
return
# Check if this is a new record or if prompt_id actually changed
if self._origin.prompt_id == self.prompt_id:
# No change in prompt_id, don't regenerate defaults
return
# Get the prompt arguments schema
try:
args_schema = json.loads(self.prompt_id.arguments_json or "{}")
# If there are arguments defined, generate template JSON
if args_schema:
template_values = self._generate_template_json_from_schema(args_schema)
self.default_values = json.dumps(template_values, indent=2)
else:
# No arguments schema, keep empty JSON
self.default_values = "{}"
except json.JSONDecodeError:
# Invalid JSON in arguments_json, keep empty
self.default_values = "{}"
def _get_json_fields(self):
"""Return fields that should be serialized as JSON in the API"""
return ["default_values"]
@api.model
def get_assistant_by_id(self, assistant_id):
"""Get an assistant record by its ID
Args:
assistant_id (int): ID of the assistant
Returns:
tuple: (assistant, error_response)
If successful, error_response will be None
If error, assistant will be None
"""
if not assistant_id:
return None, None
assistant = self.browse(int(assistant_id))
if not assistant.exists():
return None, {"success": False, "error": "Assistant not found"}
return assistant, None
def get_assistant_values(self, thread, include_prompt=True):
"""Get thread-specific evaluated default values for this assistant
Args:
thread (llm.thread): Thread record
include_prompt (bool): Whether to include prompt data
Returns:
dict: Result with evaluated default values and prompt data
"""
self.ensure_one()
# Get thread context and use it to evaluate default values
thread_context = thread.get_context() if hasattr(thread, "get_context") else {}
evaluated_values = self.get_evaluated_default_values(thread_context)
result = {
"success": True,
"thread_id": thread.id,
"assistant_id": self.id,
"default_values": self.default_values,
"evaluated_default_values": json.dumps(evaluated_values, indent=2)
if evaluated_values
else "{}",
}
# Get the prompt details if requested
if include_prompt and self.prompt_id:
prompt = self.prompt_id
result["prompt"] = {
"id": prompt.id,
"name": prompt.name,
"input_schema_json": prompt.input_schema_json,
}
return result
def _get_allowed_assistants_for_user(self, user=None):
"""Get assistants that the current user can access"""
if not user:
user = self.env.user
# Admin can access all assistants
if user.has_group("base.group_system"):
return self.search([])
# Assistants allowed for user's groups
if user.groups_id:
domain = [
"|",
("is_public", "=", True),
("allowed_group_ids", "in", user.groups_id.ids),
]
else:
# If user has no groups, only public assistants
domain = [("is_public", "=", True)]
return self.search(domain)
@api.model
def get_assistant_by_code(self, code):
"""Get assistant by code"""
return self.search([("code", "=", code)], limit=1)
@@ -0,0 +1,204 @@
import logging
from odoo import models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class LLMAssistantActionMixin(models.AbstractModel):
"""
Mixin to add AI assistant action functionality to any model.
Provides generic methods to open LLM chat with specific assistants.
Usage:
class MyModel(models.Model):
_inherit = ['my.model', 'llm.assistant.action.mixin']
def action_my_ai_button(self):
return self.action_open_llm_assistant('my_assistant_code')
"""
_name = "llm.assistant.action.mixin"
_description = "LLM Assistant Action Mixin"
def action_open_llm_assistant(
self, assistant_code=None, force_new_thread=False, **kwargs
):
"""
Generic method to open AI assistant for current record.
Creates/finds thread, sets assistant, and prepares for frontend to open AI chat.
Args:
assistant_code: Code of the assistant to use (e.g., 'invoice_analyzer').
If not provided, tries to get from context.
force_new_thread: If True, always create new thread (ignore existing).
**kwargs: Reserved for future extensibility.
Returns:
dict: Client action to open AI chat in chatter
Raises:
UserError: If no provider/model found
"""
self.ensure_one()
# Get assistant code from parameter or context
if not assistant_code:
assistant_code = self.env.context.get("assistant_code")
if not assistant_code:
raise UserError(
"No assistant code provided. Please specify assistant_code parameter or context."
)
_logger.info(
"=== Opening AI assistant '%s' for %s ID: %s (force_new=%s) ===",
assistant_code,
self._name,
self.id,
force_new_thread,
)
# Find existing thread or create new one
thread = self._find_or_create_llm_thread(force_new=force_new_thread)
# Find and set assistant
self._set_assistant_on_thread(thread, assistant_code)
_logger.info(
"=== AI assistant ready. Thread ID: %s, Assistant: %s ===",
thread.id,
thread.assistant_id.name if thread.assistant_id else "None",
)
# Return client action to open AI chat in chatter
# This is more reliable than bus notifications which can fail on cloud
# deployments with WebSocket issues
return {
"type": "ir.actions.client",
"tag": "llm_open_chatter",
"params": {
"thread_id": thread.id,
"model": self._name,
"res_id": self.id,
},
}
def _find_or_create_llm_thread(self, force_new=False):
"""
Find existing thread for this record or create a new one.
Args:
force_new: If True, always create new thread (ignore existing)
Returns:
llm.thread: The thread record
"""
if not force_new:
_logger.info("Step 1: Looking for existing thread...")
thread = self.env["llm.thread"].search(
[("model", "=", self._name), ("res_id", "=", self.id)], limit=1
)
if thread:
_logger.info("Found existing thread ID: %s", thread.id)
return thread
_logger.info("Creating new thread...")
# Find default chat model or fallback to first available
_logger.info("Looking for default chat model...")
default_model = self.env["llm.model"].search(
[
("model_use", "in", ["chat", "multimodal"]),
("default", "=", True),
("active", "=", True),
],
limit=1,
)
if default_model:
_logger.info(
"Found default model: %s (Provider: %s)",
default_model.name,
default_model.provider_id.name,
)
else:
_logger.info("No default model found, looking for first available...")
# Fallback: Get first provider and its first chat model
_logger.info("Looking for first available provider...")
provider = self.env["llm.provider"].search([("active", "=", True)], limit=1)
if not provider:
_logger.error("No active LLM provider found!")
raise UserError(
"No active LLM provider found. Please configure a provider first."
)
_logger.info("Found provider: %s", provider.name)
_logger.info("Looking for first chat model for this provider...")
default_model = self.env["llm.model"].search(
[
("provider_id", "=", provider.id),
("model_use", "in", ["chat", "multimodal"]),
("active", "=", True),
],
limit=1,
)
if not default_model:
_logger.error("No active chat model found!")
raise UserError(
"No active chat model found. Please configure a model first."
)
_logger.info(
"Creating new thread with Provider: %s, Model: %s",
default_model.provider_id.name,
default_model.name,
)
# Create new thread - name will be auto-generated by backend
thread = self.env["llm.thread"].create(
{
"model": self._name,
"res_id": self.id,
"provider_id": default_model.provider_id.id,
"model_id": default_model.id,
}
)
_logger.info("Thread created successfully with ID: %s", thread.id)
return thread
def _set_assistant_on_thread(self, thread, assistant_code):
"""
Find assistant by code and set it on the thread.
Args:
thread: llm.thread record
assistant_code: Code of the assistant to find
"""
_logger.info("Step 2: Looking for assistant with code '%s'...", assistant_code)
assistant = self.env["llm.assistant"].search(
[("code", "=", assistant_code)], limit=1
)
if assistant:
_logger.info("Found assistant: %s (ID: %s)", assistant.name, assistant.id)
if not thread.assistant_id:
_logger.info(
"Setting assistant on thread (with tools, provider, model)..."
)
thread.set_assistant(assistant.id)
_logger.info(
"Assistant set successfully. Tools: %s",
thread.tool_ids.mapped("name"),
)
else:
_logger.info(
"Thread already has assistant: %s", thread.assistant_id.name
)
else:
_logger.warning("Assistant with code '%s' not found!", assistant_code)
@@ -0,0 +1,664 @@
import json
import logging
import re
from collections.abc import Iterable
import yaml
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from ..utils import render_template
from .arguments_schema import validate_arguments_schema
_logger = logging.getLogger(__name__)
class LLMPrompt(models.Model):
_name = "llm.prompt"
_description = "LLM Prompt Template"
_inherit = ["mail.thread", "mail.activity.mixin"]
_order = "name"
name = fields.Char(
string="Prompt Name",
required=True,
tracking=True,
help="Unique identifier for the prompt template",
)
description = fields.Text(
string="Description",
tracking=True,
help="Human-readable description of the prompt",
)
active = fields.Boolean(default=True)
# Categorization
category_id = fields.Many2one(
"llm.prompt.category",
string="Category",
tracking=True,
index=True,
help="Category for organizing prompts",
)
# Tags
tag_ids = fields.Many2many(
"llm.prompt.tag",
"llm_prompt_tag_rel",
"prompt_id",
"tag_id",
string="Tags",
help="Classify and analyze your prompts",
)
# Provider and Publisher relations
provider_ids = fields.Many2many(
"llm.provider",
"llm_prompt_provider_rel",
"prompt_id",
"provider_id",
string="Compatible Providers",
help="LLM providers that can use this prompt",
)
publisher_ids = fields.Many2many(
"llm.publisher",
"llm_prompt_publisher_rel",
"prompt_id",
"publisher_id",
string="Compatible Publishers",
help="LLM publishers whose models work well with this prompt",
)
# Template field
template = fields.Text(
string="Template",
required=True,
help="Prompt template content in the selected format",
tracking=True,
)
# Format selection
format = fields.Selection(
[
("text", "Text"),
("yaml", "YAML"),
("json", "JSON"),
],
string="Format",
default="text",
required=True,
tracking=True,
help="Format of the template content after rendering",
)
# Arguments JSON field
arguments_json = fields.Text(
string="Arguments Schema",
help="JSON object defining all arguments used in this prompt",
default="""{}""",
tracking=True,
)
# Computed fields for argument info
argument_count = fields.Integer(
compute="_compute_argument_count",
string="Argument Count",
)
undefined_arguments = fields.Char(
compute="_compute_argument_validation",
string="Undefined Arguments",
help="Arguments used in templates but not defined in schema",
)
# Usage tracking
usage_count = fields.Integer(
string="Usage Count",
default=0,
readonly=True,
help="Number of times this prompt has been used",
)
last_used = fields.Datetime(
string="Last Used",
readonly=True,
help="When this prompt was last used",
)
input_schema_json = fields.Json(
string="Input Schema JSON",
compute="_compute_input_schema_json",
help="JSON schema for input fields",
store=True,
)
_sql_constraints = [
("name_unique", "UNIQUE(name)", "The prompt name must be unique."),
]
@api.model_create_multi
def create(self, vals_list):
prompts = super().create(vals_list)
return prompts
def write(self, vals):
result = super().write(vals)
return result
def copy(self, default=None):
"""Override copy to generate unique name with auto-increment pattern"""
self.ensure_one()
if default is None:
default = {}
if "name" not in default:
# Generate unique name with (Copy N) pattern
base_name = self.name
copy_name = self._generate_unique_copy_name(base_name)
default["name"] = copy_name
return super().copy(default=default)
def _generate_unique_copy_name(self, base_name):
"""Generate unique name with (Copy N) pattern"""
# Check if base_name already has (Copy N) pattern
import re
copy_pattern = r"^(.+) \(Copy (\d+)\)$"
match = re.match(copy_pattern, base_name)
if match:
# Extract original name without (Copy N)
original_name = match.group(1)
else:
# Use the full name as original
original_name = base_name
# Find existing copies with this base name
like_pattern = f"{original_name}%"
existing_records = self.search([("name", "=like", like_pattern)])
# Extract all copy numbers
copy_numbers = []
for record in existing_records:
if record.name == original_name:
# Original name exists, so we need Copy 1, 2, etc.
copy_numbers.append(0)
else:
match = re.match(
rf"^{re.escape(original_name)} \(Copy (\d+)\)$", record.name
)
if match:
copy_numbers.append(int(match.group(1)))
# Find next available number
if not copy_numbers:
# No existing copies, start with Copy 1
next_number = 1
else:
# Find the next available number
next_number = max(copy_numbers) + 1
return f"{original_name} (Copy {next_number})"
@api.depends("arguments_json")
def _compute_argument_count(self):
for prompt in self:
try:
arguments = json.loads(prompt.arguments_json or "{}")
prompt.argument_count = len(arguments)
except json.JSONDecodeError:
prompt.argument_count = 0
@api.depends("arguments_json", "template")
def _compute_argument_validation(self):
for prompt in self:
# Get defined arguments
try:
arguments = json.loads(prompt.arguments_json or "{}")
defined_args = set(arguments.keys())
except json.JSONDecodeError:
defined_args = set()
# Extract used arguments from template
used_args = self._extract_arguments_from_template(prompt.template or "")
# Find undefined arguments
undefined_args = [name for name in used_args if name not in defined_args]
if undefined_args:
prompt.undefined_arguments = ", ".join(undefined_args)
else:
prompt.undefined_arguments = False
@api.constrains("arguments_json")
def _validate_arguments_schema(self):
"""Validate arguments JSON against schema"""
for prompt in self:
if not prompt.arguments_json:
continue
is_valid, error = validate_arguments_schema(prompt.arguments_json)
if not is_valid:
raise ValidationError(error)
def _validate_rendered_format(self, rendered_content):
"""
Validate rendered content matches the selected format
Args:
rendered_content (str): The rendered template content
Raises:
ValidationError: If rendered content doesn't match format
"""
if not rendered_content:
return
try:
if self.format == "json":
json.loads(rendered_content)
elif self.format == "yaml":
# For YAML, we need to handle multiple documents
list(yaml.safe_load_all(rendered_content))
# Text format doesn't need validation
except (json.JSONDecodeError, yaml.YAMLError) as e:
raise ValidationError(
_(
"The rendered template is not valid %s.\n\n"
"Please check your template syntax and ensure it produces valid %s "
"output after variable substitution.\n\n"
"Error: %s"
)
% (self.format.upper(), self.format.upper(), str(e))
) from e
def get_prompt_data(self):
"""Returns the prompt data in the MCP format"""
self.ensure_one()
# Parse arguments
try:
arguments = json.loads(self.arguments_json or "{}")
except json.JSONDecodeError:
arguments = {}
# Format arguments for MCP
formatted_args = []
for name, schema in arguments.items():
arg_data = {
"name": name,
"description": schema.get("description", ""),
"required": schema.get("required", False),
}
formatted_args.append(arg_data)
return {
"name": self.name,
"description": self.description or "",
"category": self.category_id.name if self.category_id else "",
"arguments": formatted_args,
}
def get_default_test_context(self):
"""
Get default test context based on prompt's arguments schema.
Returns:
dict: Default context for testing
"""
try:
schema = json.loads(self.arguments_json or "{}")
defaults = {}
for arg_name, arg_schema in schema.items():
if "default" in arg_schema:
defaults[arg_name] = arg_schema["default"]
elif arg_schema.get("type") == "string":
defaults[arg_name] = f"sample_{arg_name}"
elif arg_schema.get("type") == "number":
defaults[arg_name] = 42
elif arg_schema.get("type") == "boolean":
defaults[arg_name] = True
elif arg_schema.get("type") == "array":
defaults[arg_name] = ["item1", "item2"]
else:
defaults[arg_name] = f"sample_{arg_name}"
return defaults
except (json.JSONDecodeError, Exception):
return {}
def get_messages(self, arguments=None):
"""
Generate messages for this prompt with the given arguments
Args:
arguments (dict): Dictionary of argument values
Returns:
list: List of messages for this prompt
"""
self.ensure_one()
arguments = arguments or {}
# Fill default values for missing arguments
arguments = self.sudo()._fill_default_values(arguments)
# Validate arguments against schema
self._validate_arguments(arguments)
# Render the template with arguments
rendered_content = render_template(template=self.template, context=arguments)
# Validate the rendered content matches the expected format
self._validate_rendered_format(rendered_content)
# Parse template based on format
try:
if self.format == "text":
messages = self._parse_text_messages(rendered_content)
elif self.format == "yaml":
messages = list(
self._parse_dict_messages(yaml.safe_load_all(rendered_content))
)
elif self.format == "json":
messages = list(self._parse_dict_messages(json.loads(rendered_content)))
else:
raise ValidationError(
_(
"The template format '%s' is not supported. "
"Please use Text, YAML, or JSON format."
)
% self.format
)
except ValidationError:
raise
except (json.JSONDecodeError, yaml.YAMLError) as e:
_logger.error(
"Error parsing %s rendered content for prompt %s: %s",
self.format,
self.name,
str(e),
)
raise ValidationError(
_(
"Could not parse the rendered %s content. The template may have "
"syntax errors or produce invalid output.\n\n"
"Tips:\n"
"• For YAML: Check indentation and special characters\n"
"• For JSON: Ensure quotes and brackets are balanced\n\n"
"Details: %s"
)
% (self.format.upper(), str(e))
) from e
return messages
def _parse_text_messages(self, content):
"""Parse a simple text template"""
return [
{
"role": "system",
"content": [
{
"type": "text",
"text": content,
}
],
}
]
def _parse_dict_messages(self, data):
"""Parse messages from dict, list, or iterator of dicts recursively"""
# Handle single dict or iterable of items
items = (
data
if isinstance(data, Iterable) and not isinstance(data, (str, dict))
else [data]
)
for item in items:
if isinstance(item, dict):
# Check if this dict has a 'content' key - if so, it's a message
if "content" in item:
msg_type = item.get("type", "user")
content = item["content"]
# Handle multi-line content
if isinstance(content, list):
content = "\n".join(str(line) for line in content)
yield {
"role": msg_type,
"content": [
{
"type": "text",
"text": str(content),
}
],
}
else:
# If no 'content' key, recursively check all values in the dict
for value in item.values():
if isinstance(value, (dict, list)) or (
isinstance(value, Iterable) and not isinstance(value, str)
):
yield from self._parse_dict_messages(value)
elif isinstance(item, (list, tuple)) or (
isinstance(item, Iterable) and not isinstance(item, str)
):
# If item is iterable (but not string), recurse into it
yield from self._parse_dict_messages(item)
def _fill_default_values(self, arguments):
"""
Fill in default values for missing arguments
Args:
arguments (dict): Provided argument values
Returns:
dict: Arguments with defaults filled in
"""
result = arguments.copy()
try:
schema = json.loads(self.arguments_json or "{}")
except json.JSONDecodeError:
return result
# Add default values for missing arguments
for arg_name, arg_schema in schema.items():
if arg_name not in result and "default" in arg_schema:
result[arg_name] = arg_schema["default"]
return result
def _validate_arguments(self, arguments):
"""
Validate provided arguments against the schema
Args:
arguments (dict): Dictionary of argument values
Raises:
ValidationError: If arguments are invalid
"""
self.ensure_one()
try:
schema = json.loads(self.arguments_json or "{}")
except json.JSONDecodeError:
_logger.warning(
"Skipping: Invalid JSON in arguments schema: %s", self.arguments_json
)
return
# Check for required arguments
for arg_name, arg_schema in schema.items():
if arg_schema.get("required", False) and arg_name not in arguments:
raise ValidationError(
_(
"The required argument '%s' is missing. "
"Please provide a value for this argument before running the prompt."
)
% arg_name
)
@api.model
def _extract_arguments_from_template(self, template_content):
"""
Extract argument names from a template string.
Args:
template_content (str): The template content to search
Returns:
set: Set of argument names found in the template
"""
if not template_content:
return set()
# Find all {{argument}} placeholders
# Match simple variables: {{variable_name}}
simple_pattern = r"\{\{\s*([a-zA-Z0-9_]+)\s*\}\}"
simple_matches = re.findall(simple_pattern, template_content)
return set(simple_matches)
def auto_detect_arguments(self):
"""
Auto-detect arguments from template and add them to schema
Returns:
bool: True if successful
"""
self.ensure_one()
# Get existing arguments
try:
arguments = json.loads(self.arguments_json or "{}")
except json.JSONDecodeError:
arguments = {}
# Extract used arguments from template
used_args = self._extract_arguments_from_template(self.template or "")
# Add any missing arguments to schema
updated = False
for arg_name in used_args:
if arg_name not in arguments:
arguments[arg_name] = {
"type": "string",
"description": f"Auto-detected argument: {arg_name}",
"required": True, # Default new arguments as required
}
updated = True
if updated:
self.arguments_json = json.dumps(arguments, indent=2)
return True
def _ensure_arguments_sync(self):
"""Ensure arguments schema matches template usage"""
used_args = self._extract_arguments_from_template(self.template or "")
try:
defined_args = json.loads(self.arguments_json or "{}")
except json.JSONDecodeError:
defined_args = {}
# Auto-add missing arguments
updated = False
for arg_name in used_args:
if arg_name not in defined_args:
defined_args[arg_name] = {
"type": "string",
"description": f"Auto-detected argument: {arg_name}",
"required": True, # Default new arguments as required
}
updated = True
if updated:
self.arguments_json = json.dumps(defined_args, indent=2)
def action_test_prompt(self):
"""
Test the prompt with the enhanced evaluation wizard
Returns:
dict: Action to show enhanced test wizard
"""
self.ensure_one()
# Create a wizard record with the prompt pre-filled
wizard = self.env["llm.prompt.test"].create(
{
"prompt_id": self.id,
}
)
return {
"name": _("Test Prompt: %s") % self.name,
"type": "ir.actions.act_window",
"res_model": "llm.prompt.test",
"view_mode": "form",
"res_id": wizard.id,
"target": "new",
"view_id": self.env.ref("llm_assistant.llm_prompt_test_view_form").id,
"context": {
"default_prompt_id": self.id,
},
}
@api.depends("template", "arguments_json")
def _compute_input_schema_json(self):
"""
Compute a proper JSON schema for input fields based on the template and arguments_json.
This is used for media generation models to provide a customized input form.
"""
for prompt in self:
try:
# Get arguments from arguments_json
arguments = json.loads(prompt.arguments_json or "{}")
prompt.input_schema_json = self._generate_json_schema(arguments)
except Exception as e:
_logger.error("Error computing input schema JSON: %s", str(e))
prompt.input_schema_json = {}
def _generate_json_schema(self, input_json):
# Initialize dictionaries and lists for schema components
properties = {}
required = []
# Process each property from the input dictionary
for prop_name, prop_details in input_json.items():
# Create a copy of prop_details to avoid modifying the original
prop_schema = dict(prop_details)
# Check if the property is required and add to the required list if true
if prop_schema.get("required", False):
required.append(prop_name)
# Remove the required key from the property schema
prop_schema.pop("required", None)
# Add the property schema to the properties dictionary
properties[prop_name] = prop_schema
# Construct the full JSON schema
schema = {
"type": "object",
"properties": properties,
}
# Only add required array if there are required fields
if required:
schema["required"] = required
# Return the schema as a Python dictionary
return schema
@@ -0,0 +1,81 @@
from odoo import _, api, fields, models
class LLMPromptCategory(models.Model):
_name = "llm.prompt.category"
_description = "LLM Prompt Category"
_parent_name = "parent_id"
_parent_store = True
_rec_name = "complete_name"
_order = "complete_name"
name = fields.Char(
string="Category Name",
required=True,
index=True,
)
complete_name = fields.Char(
string="Complete Name",
compute="_compute_complete_name",
store=True,
recursive=True,
)
parent_id = fields.Many2one(
"llm.prompt.category",
string="Parent Category",
index=True,
ondelete="cascade",
)
parent_path = fields.Char(index=True)
child_ids = fields.One2many(
"llm.prompt.category",
"parent_id",
string="Child Categories",
)
prompt_count = fields.Integer(
string="Prompt Count",
compute="_compute_prompt_count",
)
active = fields.Boolean(default=True)
code = fields.Char(
string="Category Code",
help="Technical code to identify this category",
)
description = fields.Text(
string="Description",
)
sequence = fields.Integer(
string="Sequence",
default=10,
)
@api.depends("name", "parent_id.complete_name")
def _compute_complete_name(self):
for category in self:
if category.parent_id:
category.complete_name = (
f"{category.parent_id.complete_name} / {category.name}"
)
else:
category.complete_name = category.name
@api.depends("child_ids")
def _compute_prompt_count(self):
prompt_data = self.env["llm.prompt"].read_group(
[("category_id", "child_of", self.ids)],
["category_id"],
["category_id"],
)
prompt_count_dict = {
data["category_id"][0]: data["category_id_count"] for data in prompt_data
}
for category in self:
category.prompt_count = prompt_count_dict.get(category.id, 0)
@api.constrains("parent_id")
def _check_category_recursion(self):
if self._has_cycle():
raise models.ValidationError(
_("Error! You cannot create recursive categories.")
)
@@ -0,0 +1,18 @@
from random import randint
from odoo import fields, models
class LLMPromptTag(models.Model):
_name = "llm.prompt.tag"
_description = "LLM Prompt Tag"
def _get_default_color(self):
return randint(1, 11)
name = fields.Char("Tag Name", required=True, translate=True)
color = fields.Integer("Color", default=_get_default_color)
_sql_constraints = [
("name_uniq", "unique (name)", "Tag name already exists!"),
]
@@ -0,0 +1,561 @@
import logging
from odoo import _, api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class LLMThread(models.Model):
_inherit = "llm.thread"
assistant_id = fields.Many2one(
"llm.assistant",
string="Assistant",
ondelete="restrict",
help="The assistant used for this thread",
)
prompt_id = fields.Many2one(
"llm.prompt",
string="Prompt for workflow",
ondelete="restrict",
tracking=True,
help="Prompt to use for workflow",
)
@api.onchange("assistant_id")
def _onchange_assistant_id(self):
"""Update provider, model and tools when assistant changes"""
if self.assistant_id:
self.provider_id = self.assistant_id.provider_id
self.model_id = self.assistant_id.model_id
self.tool_ids = self.assistant_id.tool_ids
self.prompt_id = self.assistant_id.prompt_id
else:
# Clear prompt when assistant is cleared
self.prompt_id = False
def set_assistant(self, assistant_id):
"""Set the assistant for this thread and update related fields
Args:
assistant_id (int): The ID of the assistant to set
Returns:
bool: True if successful, False otherwise
"""
self.ensure_one()
# If assistant_id is False or 0, clear the assistant and its prompt
if not assistant_id:
return self.write({"assistant_id": False, "prompt_id": False})
# Get the assistant record
assistant = self.env["llm.assistant"].browse(assistant_id)
if not assistant.exists():
return False
# Update the thread with the assistant and related fields
update_vals = {
"assistant_id": assistant_id,
"tool_ids": [(6, 0, assistant.tool_ids.ids)],
}
if assistant.provider_id.id:
update_vals["provider_id"] = assistant.provider_id.id
if assistant.model_id.id:
update_vals["model_id"] = assistant.model_id.id
if assistant.prompt_id.id:
update_vals["prompt_id"] = assistant.prompt_id.id
return self.write(update_vals)
def action_open_thread(self):
"""Open the thread in the chat client interface
Returns:
dict: Action to open the thread in the chat client
"""
self.ensure_one()
return {
"type": "ir.actions.client",
"tag": "llm_thread.chat_client_action",
"params": {
"default_active_id": self.id,
},
"context": {
"active_id": self.id,
},
"target": "current",
}
def get_context(self, base_context=None):
"""
Get the context to pass to prompt rendering with thread-specific enhancements.
This is the canonical method for creating prompt context in both production and testing.
Args:
base_context (dict): Additional context from caller (optional)
Returns:
dict: Context ready for prompt rendering
"""
context = super().get_context(base_context or {})
# If we have an assistant with default values, add them to the context
if self.assistant_id:
# Get assistant's evaluated default values using the current context
assistant_defaults = self.assistant_id.get_evaluated_default_values(context)
# Merge assistant defaults into context
# Assistant defaults are added first, so thread context takes precedence
if assistant_defaults:
context = {**assistant_defaults, **context}
return context
@api.model
def get_thread_by_id(self, thread_id):
"""Get a thread record by its ID
Args:
thread_id (int): ID of the thread
Returns:
tuple: (thread, error_response)
If successful, error_response will be None
If error, thread will be None
"""
thread = self.browse(int(thread_id))
if not thread.exists():
return None, {"success": False, "error": "Thread not found"}
return thread, None
@api.model
def get_thread_and_assistant(self, thread_id, assistant_id=False):
"""Get thread and assistant records by their IDs
Args:
thread_id (int): ID of the thread
assistant_id (int, optional): ID of the assistant, or False to clear
Returns:
tuple: (thread, assistant, error_response)
If successful, error_response will be None
If error, thread and/or assistant will be None
"""
# Get thread
thread, error = self.get_thread_by_id(thread_id)
if error:
return None, None, error
# If no assistant_id, return just the thread
if not assistant_id:
return thread, None, None
# Get assistant from the assistant model
assistant, error = self.env["llm.assistant"].get_assistant_by_id(assistant_id)
if error:
return thread, None, error
return thread, assistant, None
def _thread_to_store(self, store, **kwargs):
"""Extend base _thread_to_store to include assistant_id and prompt_id."""
super()._thread_to_store(store, **kwargs)
# Always add assistant_id and prompt_id to thread data (either value or False)
for thread in self:
thread_data = {
"id": thread.id,
"model": "llm.thread",
"assistant_id": {
"id": thread.assistant_id.id,
"name": thread.assistant_id.name,
"model": "llm.assistant",
}
if thread.assistant_id
else False,
# prompt_id is defined in this module, so handle it here
"prompt_id": {
"id": thread.prompt_id.id,
"name": thread.prompt_id.name,
"model": "llm.prompt",
}
if thread.prompt_id
else False,
}
store.add("mail.thread", thread_data)
def _extract_message_content(self, message):
"""Extract text content from a message regardless of format"""
content = message.get("content", "")
if isinstance(content, list) and len(content) > 0:
return content[0].get("text", "")
if isinstance(content, str):
return content
return ""
def get_prepend_messages(self):
"""Hook: return a list of formatted messages to prepend to the conversation."""
self.ensure_one()
if self.prompt_id:
try:
# Get messages from the prompt with enhanced context
return self.prompt_id.get_messages(self.get_context())
except Exception as e:
_logger.error(
"Error getting messages from prompt '%s': %s",
self.prompt_id.name,
e,
)
# Continue without prompt messages rather than failing completely
# Post a user-friendly warning to the thread
self.message_post(
body=_(
"Note: The prompt '%s' could not be loaded. "
"Continuing without it. (Error: %s)",
)
% (self.prompt_id.name, str(e)),
)
return []
def generate_messages(self, last_message):
"""Generate messages with actual AI intelligence."""
self.ensure_one()
# Get last message if not provided
if not last_message:
try:
last_message = self.get_latest_llm_message()
except UserError:
# No DB messages found - check if prepended messages have a user message
prepend_msgs = self.get_prepend_messages()
user_msg = next(
(msg for msg in prepend_msgs if msg.get("role") == "user"),
None,
)
if user_msg:
# Extract content from prepended user message
content = user_msg.get("content", [])
if isinstance(content, list) and content:
body = content[0].get("text", "")
else:
body = str(content)
# Create actual user message from prepended content
last_message = self.message_post(
body=body,
llm_role="user",
author_id=self.env.user.partner_id.id,
)
else:
# No user message in prepended messages either
raise
# Continue generation loop
while self._should_continue(last_message):
if last_message.llm_role in ("user", "tool"):
if self.model_id.model_use in ("image_generation", "generation"):
last_message = yield from self._generate_response(last_message)
else:
# Generate assistant response
last_message = yield from self._generate_assistant_response()
elif last_message.llm_role == "assistant" and last_message.has_tool_calls():
# Execute ALL tool calls from assistant message
tool_calls = last_message.get_tool_calls()
for tool_call in tool_calls:
tool_message = yield from self._execute_tool_call(
tool_call,
last_message,
)
last_message = tool_message
self.env.cr.commit()
else:
_logger.info(
f"Breaking loop. Last message role: {last_message.llm_role}, "
f"has_tool_calls: {last_message.has_tool_calls()}",
)
break
return last_message
def _generate_response(self, last_message):
raise NotImplementedError
def _generate_assistant_response(self):
"""Generate assistant response and handle tool calls.
Catches LLM API errors and posts them as error messages in the thread
so users can see what went wrong without checking server logs.
"""
# Flush any pending writes to ensure latest messages are visible
self.env.flush_all()
# Use the new optimized method for LLM context
message_history = self.get_llm_messages()
# Determine if we should use streaming
use_streaming = getattr(self.model_id, "supports_streaming", True)
chat_kwargs = self._prepare_chat_kwargs(message_history, use_streaming)
try:
if use_streaming:
# Handle streaming response - process tool calls directly from stream
stream_response = self.sudo().model_id.chat(**chat_kwargs)
assistant_message = yield from self._handle_streaming_response(
stream_response,
)
else:
# Handle non-streaming response
response = self.sudo().model_id.chat(**chat_kwargs)
assistant_message = yield from self._handle_non_streaming_response(
response,
)
except Exception as e:
# Post error message to thread so user can see it
_logger.exception("LLM API error in thread %s", self.id)
error_message, event = self._post_error_message(
e,
title=_("LLM API Error"),
)
yield event
return error_message
return assistant_message
def _prepare_chat_kwargs(self, message_history, use_streaming):
"""Prepare chat kwargs for provider. Can be overridden by extensions."""
return {
"messages": message_history,
"tools": self.tool_ids,
"stream": use_streaming,
"prepend_messages": self.get_prepend_messages(),
}
def get_llm_messages(self, limit=25):
"""Get the most recent LLM messages in chronological order.
This method is optimized for LLM context preparation:
- Always returns messages in chronological order (ASC)
- Limits to the most recent N messages for context window management
- Uses efficient database queries with proper indexing
- Excludes error messages (is_error=True) from context
Args:
limit (int): Maximum number of recent messages to retrieve (default: 25)
Returns:
mail.message recordset: Recent LLM messages in chronological order
"""
self.ensure_one()
# Domain for filtering LLM messages only (excluding error messages)
domain = [
("model", "=", self._name),
("res_id", "=", self.id),
("llm_role", "!=", False), # Only messages with LLM roles
("is_error", "=", False), # Exclude error messages from LLM context
]
if limit:
# Two-step approach for efficiency:
# 1. Get the N most recent messages (DESC order)
recent_messages = self.env["mail.message"].search(
domain,
order="create_date DESC, write_date DESC, id DESC",
limit=limit,
)
# 2. Sort them chronologically for LLM context (ASC order)
return recent_messages.sorted(lambda m: (m.create_date, m.write_date, m.id))
# If no limit, get all messages in chronological order
return self.env["mail.message"].search(
domain,
order="create_date ASC, write_date ASC, id ASC",
)
def get_latest_llm_message(self):
"""Get the most recent LLM message for flow control.
Returns:
mail.message: The latest LLM message
Raises:
UserError: If no LLM messages exist
"""
self.ensure_one()
domain = [
("model", "=", self._name),
("res_id", "=", self.id),
("llm_role", "!=", False),
]
result = self.env["mail.message"].search(
domain,
order="create_date DESC, write_date DESC, id DESC",
limit=1,
)
if not result:
raise UserError("No LLM messages found in this thread.")
return result[0]
def _should_continue(self, last_message):
"""Simplified continue logic based on message history."""
if not last_message:
return False
# Continue if:
# 1. Last message is user message → generate assistant response
# 2. Last message is tool message → generate assistant response
# 3. Last message is assistant with tool calls → execute tools
if last_message.llm_role in ("user", "tool") or (
last_message.llm_role == "assistant" and last_message.has_tool_calls()
):
return True
return False
def _handle_streaming_response(self, stream_response):
"""Handle streaming response from LLM provider with tool call processing."""
message = None
accumulated_content = ""
collected_tool_calls = []
for chunk in stream_response:
# Initialize message on first content
if message is None and chunk.get("content"):
message = self.message_post(
body="Thinking...",
llm_role="assistant",
author_id=False,
)
yield {"type": "message_create", "message": message.to_store_format()}
# Handle content streaming
if chunk.get("content"):
accumulated_content += chunk["content"]
message.write({"body": self._process_llm_body(accumulated_content)})
yield {"type": "message_chunk", "message": message.to_store_format()}
# Collect tool calls for processing
if chunk.get("tool_calls"):
collected_tool_calls.extend(chunk["tool_calls"])
_logger.debug(
f"Collected {len(chunk['tool_calls'])} tool calls from chunk",
)
# Handle errors
if chunk.get("error"):
yield {"type": "error", "error": chunk["error"]}
return message
# CRITICAL FIX: Create assistant message IMMEDIATELY if we have tool calls
if collected_tool_calls:
body_json = {"tool_calls": collected_tool_calls}
if not message:
# Create assistant message with body_json (handled by message_post override)
message = self.message_post(
body="", # Empty body for tool-only responses
body_json=body_json,
llm_role="assistant",
author_id=False,
)
# Commit to ensure message is saved before tool execution
self.env.cr.commit()
yield {"type": "message_create", "message": message.to_store_format()}
else:
# Update existing message with tool calls
message.write({"body_json": body_json})
# Commit to ensure update is saved
self.env.cr.commit()
yield {"type": "message_update", "message": message.to_store_format()}
elif message and accumulated_content:
# Final update for assistant message without tool calls
message.write({"body": self._process_llm_body(accumulated_content)})
yield {"type": "message_update", "message": message.to_store_format()}
return message
def _handle_non_streaming_response(self, response):
"""Handle non-streaming response from LLM provider."""
# Extract content and tool calls from response
content = response.get("content", "")
tool_calls = response.get("tool_calls", [])
if not content and not tool_calls:
content = "No response from model"
# Prepare body_json with tool calls if present
body_json = {"tool_calls": tool_calls} if tool_calls else None
# Create assistant message with body_json (handled by message_post override)
assistant_message = self.message_post(
body=self._process_llm_body(content) if content else "",
body_json=body_json,
llm_role="assistant",
author_id=False,
)
yield {
"type": "message_create",
"message": assistant_message.to_store_format(),
}
return assistant_message
def _execute_tool_call(self, tool_call, assistant_message):
"""Execute a single tool call and return the tool message.
Args:
tool_call (dict): Tool call data from assistant message
assistant_message (mail.message): The assistant message that contains the tool calls
Yields:
dict: Status updates for streaming
Returns:
mail.message: The tool message with execution result
"""
try:
# Create tool message using the post_tool_call method
tool_msg = self.env["mail.message"].post_tool_call(
tool_call,
thread_model=self,
)
yield {"type": "message_create", "message": tool_msg.to_store_format()}
# Execute the tool call
result_msg = yield from tool_msg.execute_tool_call(thread_model=self)
return result_msg
except Exception as e:
_logger.error(f"Error executing tool call: {e}")
# Create error tool message using the new method
try:
error_msg = self.env["mail.message"].create_tool_error_message(
tool_call,
str(e),
thread_model=self,
)
yield {
"type": "message_create",
"message": error_msg.to_store_format(),
}
return error_msg
except Exception as e2:
_logger.error(f"Failed to create error message: {e2}")
# Yield error event so frontend knows something went wrong
yield {
"type": "error",
"error": f"Tool execution failed: {e!s}",
}
# Re-raise the original exception - don't silently return None
raise e from e2
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
@@ -0,0 +1,11 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_llm_assistant_user,llm.assistant.user,model_llm_assistant,base.group_user,1,0,0,0
access_llm_assistant_manager,llm.assistant.manager,model_llm_assistant,llm.group_llm_manager,1,1,1,1
access_llm_prompt_user,llm.prompt.user,model_llm_prompt,base.group_user,1,0,0,0
access_llm_prompt_manager,llm.prompt.manager,model_llm_prompt,llm.group_llm_manager,1,1,1,1
access_llm_prompt_tag_user,llm.prompt.tag.user,model_llm_prompt_tag,base.group_user,1,0,0,0
access_llm_prompt_tag_manager,llm.prompt.tag.manager,model_llm_prompt_tag,llm.group_llm_manager,1,1,1,1
access_llm_prompt_category_user,llm.prompt.category.user,model_llm_prompt_category,base.group_user,1,0,0,0
access_llm_prompt_category_manager,llm.prompt.category.manager,model_llm_prompt_category,llm.group_llm_manager,1,1,1,1
access_llm_prompt_test_user,llm.prompt.test.user,model_llm_prompt_test,base.group_user,1,1,1,1
access_llm_thread_mock_user,llm.thread.mock.user,model_llm_thread_mock,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_llm_assistant_user llm.assistant.user model_llm_assistant base.group_user 1 0 0 0
3 access_llm_assistant_manager llm.assistant.manager model_llm_assistant llm.group_llm_manager 1 1 1 1
4 access_llm_prompt_user llm.prompt.user model_llm_prompt base.group_user 1 0 0 0
5 access_llm_prompt_manager llm.prompt.manager model_llm_prompt llm.group_llm_manager 1 1 1 1
6 access_llm_prompt_tag_user llm.prompt.tag.user model_llm_prompt_tag base.group_user 1 0 0 0
7 access_llm_prompt_tag_manager llm.prompt.tag.manager model_llm_prompt_tag llm.group_llm_manager 1 1 1 1
8 access_llm_prompt_category_user llm.prompt.category.user model_llm_prompt_category base.group_user 1 0 0 0
9 access_llm_prompt_category_manager llm.prompt.category.manager model_llm_prompt_category llm.group_llm_manager 1 1 1 1
10 access_llm_prompt_test_user llm.prompt.test.user model_llm_prompt_test base.group_user 1 1 1 1
11 access_llm_thread_mock_user llm.thread.mock.user model_llm_thread_mock base.group_user 1 1 1 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 70 70">
<defs>
<path id="icon-a" d="M4,0 C36.4160122,0 58.0836068,0 65,0 C69,0 70,1 70,5 C70,43.0488877 70,62.4235458 70,65 C70,69 69,70 65,70 C61,70 9,70 4,70 C1,70 0,69 0,65 C0,62.4676575 0,41.8005206 0,5 C0,1 1,0 4,0 Z"/>
<linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#9370DB"/>
<stop offset="100%" stop-color="#663399"/>
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd">
<mask id="icon-b" fill="#fff">
<use xlink:href="#icon-a"/>
</mask>
<g mask="url(#icon-b)">
<rect width="70" height="70" fill="url(#icon-c)"/>
<path fill="#FFF" fill-opacity=".383" d="M4,1.8 L65,1.8 C67.6666667,1.8 69.3333333,1.13333333 70,-0.2 C70,2.46666667 70,3.46666667 70,2.8 L0,2.8 C0,3.46666667 0,2.46666667 0,-0.2 C0.666666667,1.13333333 2,1.8 4,1.8 Z" transform="matrix(1 0 0 -1 0 2.8)"/>
<path fill="#000" fill-opacity=".3" d="M39.8461538,54 L4,54 C2,54 0,53.8544474 0,49.9245283 L0,25.9563572 L19.6596494,1.0710105 L47,0 L49.9211999,35.2849068 L39.8461538,54 Z" opacity=".324" transform="translate(5 15)"/>
<path fill="#000" fill-opacity=".383" d="M4,4 L65,4 C67.6666667,4 69.3333333,3 70,1 C70,3.66666667 70,5 70,5 L0,5 C0,5 0,3.66666667 0,1 C0.666666667,3 2,4 4,4 Z" transform="translate(0 65)"/>
<path fill="#000" fill-rule="evenodd" clip-rule="evenodd" opacity=".3" transform="translate(10 5) scale(0.3)" d="M129.49 114.51C129.121 116.961 128.187 119.293 126.762 121.322C125.337 123.351 123.461 125.021 121.28 126.2C120.676 126.535 120.043 126.816 119.39 127.04C120.22 138.04 102.74 142.04 93.32 139.42L96.82 151.66L87.82 151.98L72.07 129.43C66.76 130.93 60.49 131.65 56.44 125.15C56.0721 124.553 55.7382 123.935 55.44 123.3C54.4098 123.51 53.3614 123.617 52.31 123.62C49.31 123.62 44.31 122.72 41.77 120.96C39.7563 119.625 38.1588 117.75 37.16 115.55C31.75 116.29 27.16 115.02 24.16 111.88C20.36 107.97 19.28 101.51 21.26 94.58C23.87 85.33 31.81 74.91 47.59 71C48.9589 69.2982 50.5972 67.8322 52.44 66.66C62.35 60.31 78.44 59.76 90.65 65.79C95.3836 64.9082 100.27 65.376 104.75 67.14C113.53 70.43 119.91 77.31 121.11 84.3C123.487 85.5317 125.433 87.4568 126.69 89.82C129.32 94.76 129.69 99.71 127.92 103.71C129.587 107.049 130.138 110.835 129.49 114.51ZM123.01 109.31C121.612 110.048 120.056 110.434 118.475 110.434C116.894 110.434 115.338 110.048 113.94 109.31L114.67 104.46C117.75 104.76 120.26 103.8 121.57 101.83C123.04 99.64 122.81 96.39 120.95 92.9C118.87 88.99 114.38 88.37 111.89 88.34H111.73C105.49 88.34 99.13 91.89 96.56 96.52L92.82 94.73C93.5553 92.3449 94.8046 90.15 96.48 88.3C95.0376 87.0754 93.9474 85.4887 93.3217 83.703C92.696 81.9173 92.5574 79.9971 92.92 78.14L96.61 77.8C96.7789 79.302 97.4 80.7172 98.3911 81.8583C99.3822 82.9994 100.697 83.8125 102.16 84.19C105.238 82.8161 108.58 82.1335 111.95 82.19C112.43 82.19 112.89 82.24 113.36 82.27C110.969 78.0312 107.18 74.7545 102.64 73C91.56 68.7 84.09 75.37 82.38 77.67C78.26 83.19 80.9 88.41 82.91 91.8L79.61 94.8C76.736 92.314 74.8075 88.9127 74.15 85.17C69.92 86.44 64.24 86.17 61.06 80.74L64.06 78.68C67.43 81.2 72.78 80.98 75.32 77.87C75.9252 76.4949 76.6905 75.1959 77.6 74C79.044 72.093 80.7864 70.4316 82.76 69.08C74.47 66.82 62.76 67.19 55.68 71.73C53.7668 72.841 52.192 74.4517 51.1244 76.3895C50.0569 78.3274 49.5368 80.5192 49.62 82.73C49.62 86.3 52.42 91.94 56.19 92.82L54 97.07C51.5946 96.5129 49.4109 95.2487 47.73 93.44L44.48 97.58L41.23 96L44.41 87.68C43.8904 86.064 43.624 84.3774 43.62 82.68C43.628 81.3361 43.7687 79.9963 44.04 78.68C34.04 82.81 29.1 89.68 27.29 95.96C25.9 100.79 26.44 105.15 28.72 107.49C30.53 109.35 33.3 109.79 35.91 109.62L42.91 104.17L45.21 106.11L43.13 112.93C44.22 116.4 47.79 118.19 54.3 116.93C54.6375 114.169 55.7272 111.554 57.45 109.37C58.7133 107.552 60.3846 106.056 62.33 105L65.75 95.79L69.17 95.64L68.8 103.19C74.55 102.6 80.98 103.77 86.97 102.87L88.07 106.87C79.29 110.93 70.3 104.31 62.15 113.04C59.22 116.18 60.34 118.91 62.15 121.66C64.76 125.59 69.66 123.23 74.67 121.66C82.26 119.34 87.77 117.66 98.16 118.51C95.68 113.8 95.92 108.11 99.24 101.85L104.13 103.78C100.7 111.69 103.91 116.27 106.13 118.29C109.56 121.41 114.72 122.35 118.13 120.47C119.436 119.749 120.559 118.737 121.412 117.513C122.265 116.289 122.825 114.885 123.05 113.41C123.275 112.051 123.258 110.663 123 109.31H123.01Z"/>
<path fill="#FFF" fill-rule="evenodd" clip-rule="evenodd" transform="translate(10 3) scale(0.3)" d="M129.49 114.51C129.121 116.961 128.187 119.293 126.762 121.322C125.337 123.351 123.461 125.021 121.28 126.2C120.676 126.535 120.043 126.816 119.39 127.04C120.22 138.04 102.74 142.04 93.32 139.42L96.82 151.66L87.82 151.98L72.07 129.43C66.76 130.93 60.49 131.65 56.44 125.15C56.0721 124.553 55.7382 123.935 55.44 123.3C54.4098 123.51 53.3614 123.617 52.31 123.62C49.31 123.62 44.31 122.72 41.77 120.96C39.7563 119.625 38.1588 117.75 37.16 115.55C31.75 116.29 27.16 115.02 24.16 111.88C20.36 107.97 19.28 101.51 21.26 94.58C23.87 85.33 31.81 74.91 47.59 71C48.9589 69.2982 50.5972 67.8322 52.44 66.66C62.35 60.31 78.44 59.76 90.65 65.79C95.3836 64.9082 100.27 65.376 104.75 67.14C113.53 70.43 119.91 77.31 121.11 84.3C123.487 85.5317 125.433 87.4568 126.69 89.82C129.32 94.76 129.69 99.71 127.92 103.71C129.587 107.049 130.138 110.835 129.49 114.51ZM123.01 109.31C121.612 110.048 120.056 110.434 118.475 110.434C116.894 110.434 115.338 110.048 113.94 109.31L114.67 104.46C117.75 104.76 120.26 103.8 121.57 101.83C123.04 99.64 122.81 96.39 120.95 92.9C118.87 88.99 114.38 88.37 111.89 88.34H111.73C105.49 88.34 99.13 91.89 96.56 96.52L92.82 94.73C93.5553 92.3449 94.8046 90.15 96.48 88.3C95.0376 87.0754 93.9474 85.4887 93.3217 83.703C92.696 81.9173 92.5574 79.9971 92.92 78.14L96.61 77.8C96.7789 79.302 97.4 80.7172 98.3911 81.8583C99.3822 82.9994 100.697 83.8125 102.16 84.19C105.238 82.8161 108.58 82.1335 111.95 82.19C112.43 82.19 112.89 82.24 113.36 82.27C110.969 78.0312 107.18 74.7545 102.64 73C91.56 68.7 84.09 75.37 82.38 77.67C78.26 83.19 80.9 88.41 82.91 91.8L79.61 94.8C76.736 92.314 74.8075 88.9127 74.15 85.17C69.92 86.44 64.24 86.17 61.06 80.74L64.06 78.68C67.43 81.2 72.78 80.98 75.32 77.87C75.9252 76.4949 76.6905 75.1959 77.6 74C79.044 72.093 80.7864 70.4316 82.76 69.08C74.47 66.82 62.76 67.19 55.68 71.73C53.7668 72.841 52.192 74.4517 51.1244 76.3895C50.0569 78.3274 49.5368 80.5192 49.62 82.73C49.62 86.3 52.42 91.94 56.19 92.82L54 97.07C51.5946 96.5129 49.4109 95.2487 47.73 93.44L44.48 97.58L41.23 96L44.41 87.68C43.8904 86.064 43.624 84.3774 43.62 82.68C43.628 81.3361 43.7687 79.9963 44.04 78.68C34.04 82.81 29.1 89.68 27.29 95.96C25.9 100.79 26.44 105.15 28.72 107.49C30.53 109.35 33.3 109.79 35.91 109.62L42.91 104.17L45.21 106.11L43.13 112.93C44"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

@@ -0,0 +1,380 @@
<!-- Hero Section -->
<section style="padding:4rem 0 3rem">
<div class="container">
<div class="text-center" style="max-width:800px; margin:0 auto">
<div style="display:inline-flex; padding:0.5rem 1.25rem; border-radius:50px; margin-bottom:2rem">
<i class="fa fa-user-circle" style="color:#71639e; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#71639e; font-weight:600; font-size:0.9rem">AI Assistants</span>
</div>
<h1 style="font-size:3rem; font-weight:800; color:#212529; margin-bottom:1.5rem; line-height:1.2">
LLM Assistant Module
</h1>
<p style="font-size:1.25rem; color:#6c757d; margin-bottom:1rem; line-height:1.6">
Create and manage specialized AI assistants with dedicated tools and configurations.
</p>
<p style="font-size:1rem; color:#868e96; margin-bottom:2rem">
Build <strong style="color:#495057">purpose-built AI helpers</strong> for different use cases in Odoo
</p>
<div class="d-flex justify-content-center flex-wrap">
<div class="d-flex align-items-center" style="padding:0.75rem 1.5rem; border-radius:12px; margin:0.5rem">
<i class="fa fa-cog" style="color:#71639e; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#5b4c7d; font-weight:600">Configurable</span>
</div>
<div class="d-flex align-items-center" style="padding:0.75rem 1.5rem; border-radius:12px; margin:0.5rem">
<i class="fa fa-wrench" style="color:#17a2b8; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#0c5460; font-weight:600">Tool Support</span>
</div>
<div class="d-flex align-items-center" style="padding:0.75rem 1.5rem; border-radius:12px; margin:0.5rem">
<i class="fa fa-github" style="color:#28a745; font-size:1.25rem; margin-right:0.5rem"></i>
<span style="color:#155724; font-weight:600">Open Source</span>
</div>
</div>
</div>
</div>
</section>
<div class="container my-5">
<!-- Overview Section -->
<section class="mb-5">
<h2 class="text-center mb-3" style="font-size:2.5rem; font-weight:700; color:#71639e">What is LLM Assistant?</h2>
<p class="text-center mb-5" style="font-size:1.2rem; color:#6c757d; font-weight:500">Specialized AI assistants for your Odoo workflows</p>
<div class="row justify-content-center mb-5">
<div class="col-lg-10">
<div class="card border-0 shadow-sm" style="border-radius:16px; padding:2.5rem">
<p style="font-size:1.15rem; color:#495057; line-height:1.9; margin-bottom:0; text-align:center">
The LLM Assistant module extends the core LLM integration by providing a framework for creating and managing specialized AI assistants. Each assistant can have its own configuration, tools, and system prompts, enabling more targeted AI assistance for different use cases.
</p>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Key Features Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Key Features</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Everything you need to create specialized AI assistants</p>
<div class="row g-4">
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-bottom:1.5rem">
<i class="fa fa-plus-circle" style="font-size:28px; color:#71639e"></i>
</div>
<h3 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Create Specialized Assistants</h3>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Build AI assistants tailored for specific tasks like customer support, sales, HR, or technical documentation.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-bottom:1.5rem">
<i class="fa fa-sliders" style="font-size:28px; color:#71639e"></i>
</div>
<h3 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Assign Models &amp; Providers</h3>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Configure specific LLM providers and models for each assistant to optimize performance and costs.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-bottom:1.5rem">
<i class="fa fa-wrench" style="font-size:28px; color:#71639e"></i>
</div>
<h3 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Configure Tools &amp; Capabilities</h3>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Attach specific tools to each assistant, giving them the ability to perform actions in Odoo.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-bottom:1.5rem">
<i class="fa fa-file-text-o" style="font-size:28px; color:#71639e"></i>
</div>
<h3 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Custom System Prompts</h3>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Define unique personalities and behaviors with customizable system prompts using template variables.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-bottom:1.5rem">
<i class="fa fa-exchange" style="font-size:28px; color:#71639e"></i>
</div>
<h3 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Easy Assistant Switching</h3>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Switch between assistants during conversations from the chat interface dropdown.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:56px; height:56px; border-radius:12px; margin-bottom:1.5rem">
<i class="fa fa-shield" style="font-size:28px; color:#71639e"></i>
</div>
<h3 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1rem">Role-Based Access Control</h3>
<p style="color:#6c757d; font-size:0.95rem; line-height:1.7; margin-bottom:0">Regular users can use assistants while only managers can create and edit them.</p>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Use Cases Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Use Cases</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Create specialized assistants for different departments</p>
<div class="row g-4">
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 shadow-sm bg-light" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:50%; margin:0 auto 1.5rem">
<i class="fa fa-headphones" style="font-size:32px; color:#ffffff"></i>
</div>
<h3 style="font-size:1.1rem; font-weight:700; color:#5b4c7d">Customer Support</h3>
<p style="color:#5b4c7d; margin-bottom:0; font-size:0.9rem">Answer product questions and troubleshoot issues.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 shadow-sm bg-light" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:50%; margin:0 auto 1.5rem">
<i class="fa fa-line-chart" style="font-size:32px; color:#ffffff"></i>
</div>
<h3 style="font-size:1.1rem; font-weight:700; color:#5b4c7d">Sales Assistant</h3>
<p style="color:#5b4c7d; margin-bottom:0; font-size:0.9rem">Product recommendations and pricing information.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 shadow-sm bg-light" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:50%; margin:0 auto 1.5rem">
<i class="fa fa-users" style="font-size:32px; color:#ffffff"></i>
</div>
<h3 style="font-size:1.1rem; font-weight:700; color:#5b4c7d">HR Assistant</h3>
<p style="color:#5b4c7d; margin-bottom:0; font-size:0.9rem">Policy questions and onboarding information.</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 shadow-sm bg-light" style="border-radius:16px">
<div class="card-body p-4 text-center">
<div class="bg-primary d-flex align-items-center justify-content-center" style="width:64px; height:64px; border-radius:50%; margin:0 auto 1.5rem">
<i class="fa fa-code" style="font-size:32px; color:#ffffff"></i>
</div>
<h3 style="font-size:1.1rem; font-weight:700; color:#5b4c7d">Technical Expert</h3>
<p style="color:#5b4c7d; margin-bottom:0; font-size:0.9rem">Documentation and code examples.</p>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Technical Details -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Technical Details</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Requirements and dependencies</p>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<h4 style="font-size:1.25rem; font-weight:700; color:#212529; margin-bottom:1.5rem">Module Information</h4>
<div class="row g-3">
<div class="col-sm-6">
<div class="bg-light" style="padding:1rem; border-radius:8px">
<strong style="color:#495057">Dependencies:</strong>
<p style="margin:0.5rem 0 0 0; color:#6c757d">llm_thread, llm_tool</p>
</div>
</div>
<div class="col-sm-6">
<div class="bg-light" style="padding:1rem; border-radius:8px">
<strong style="color:#495057">Category:</strong>
<p style="margin:0.5rem 0 0 0; color:#6c757d">Productivity/LLM</p>
</div>
</div>
<div class="col-sm-6">
<div class="bg-light" style="padding:1rem; border-radius:8px">
<strong style="color:#495057">Version:</strong>
<p style="margin:0.5rem 0 0 0; color:#6c757d">18.0.1.0.0</p>
</div>
</div>
<div class="col-sm-6">
<div class="bg-light" style="padding:1rem; border-radius:8px">
<strong style="color:#495057">License:</strong>
<p style="margin:0.5rem 0 0 0; color:#6c757d">LGPL-3</p>
</div>
</div>
</div>
<div class="bg-light mt-4" style="padding:1.5rem; border-radius:12px">
<h5 style="font-weight:700; color:#212529; margin-bottom:0.75rem">Configuration</h5>
<p style="color:#6c757d; margin-bottom:0">Navigate to <strong>LLM &rarr; Configuration &rarr; Assistants</strong> to create and manage your AI assistants.</p>
</div>
</div>
</div>
</div>
</div>
</section>
<hr class="my-5 bg-secondary" style="height:2px; border:none; opacity:0.5">
<!-- Related Modules Section -->
<section class="mb-5">
<h2 class="text-center mb-4" style="font-size:2.5rem; font-weight:700; color:#71639e">Related Modules</h2>
<p class="text-center mb-5" style="font-size:1.1rem; color:#6c757d">Build your complete AI ecosystem</p>
<div class="row g-4">
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-puzzle-piece" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Base</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Core framework for AI integration. Required dependency for all LLM modules.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_thread" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-comments" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Thread</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Persistent chat conversations with streaming responses and message history.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_tool" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-wrench" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Tool</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Enable AI to execute Odoo functions with the @llm_tool decorator.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_openai" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-circle-o" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">OpenAI Provider</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Connect to OpenAI, Gemini, Grok, DeepSeek, and any OpenAI-compatible API.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_knowledge" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-book" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">LLM Knowledge</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">RAG-powered knowledge base with semantic search and document indexing.</p>
</div>
</div>
</a>
</div>
<div class="col-md-6 col-lg-4">
<a href="https://apps.odoo.com/apps/modules/18.0/llm_ollama" style="text-decoration:none">
<div class="card h-100 border-0 shadow-sm" style="border-radius:16px">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-light d-flex align-items-center justify-content-center" style="width:48px; height:48px; border-radius:10px; margin-right:1rem">
<i class="fa fa-server" style="font-size:24px; color:#71639e"></i>
</div>
<h4 style="font-size:1.1rem; font-weight:700; color:#212529; margin-bottom:0">Ollama Provider</h4>
</div>
<p style="color:#6c757d; font-size:0.9rem; line-height:1.6; margin-bottom:0">Run open-source models locally with full privacy and no API costs.</p>
</div>
</div>
</a>
</div>
</div>
</section>
</div>
<!-- Footer -->
<footer class="bg-primary" style="padding:3rem 2rem; border-radius:16px; margin:2rem 1rem">
<div class="container">
<div style="border-radius:12px; padding:2.5rem">
<div class="row align-items-center">
<div class="col-md-8">
<h4 style="font-weight:700; color:#ffffff; font-size:1.5rem; margin-bottom:0.75rem">LLM Assistant Module</h4>
<p style="color:#f8f9fa; font-size:1rem; margin-bottom:1rem">Specialized AI assistants for Odoo</p>
<p style="color:#e9d5ff; margin-bottom:0.25rem">Developed by <strong style="color:#ffffff">Apexive Solutions LLC</strong></p>
<p style="color:#e0d4ec; font-size:0.9rem; margin-bottom:0">Licensed under LGPL-3 &bull; &copy; 2025 All rights reserved.</p>
</div>
<div class="col-md-4 mt-3 mt-md-0">
<div class="d-flex flex-column">
<div class="bg-white d-flex align-items-center justify-content-center" style="padding:0.875rem 1.5rem; border-radius:8px; color:#71639e; font-weight:600; margin-bottom:0.75rem">
<i class="fa fa-github" style="font-size:18px; margin-right:0.5rem"></i>
github.com/apexive/odoo-llm
</div>
<a href="mailto:hello@apexive.com" class="bg-white d-flex align-items-center justify-content-center" style="padding:0.875rem 1.5rem; border-radius:8px; text-decoration:none; color:#71639e; font-weight:600; margin-bottom:0">
<i class="fa fa-envelope" style="font-size:18px; margin-right:0.5rem"></i>
hello@apexive.com
</a>
</div>
</div>
</div>
</div>
</div>
</footer>
Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

@@ -0,0 +1,6 @@
import {patch} from "@web/core/utils/patch";
import {CodeEditor} from "@web/core/code_editor/code_editor";
patch(CodeEditor, {
MODES: ["javascript", "xml", "qweb", "scss", "python", "text"]
});
@@ -0,0 +1,38 @@
/** @odoo-module **/
import { LLMChatThreadHeader } from "@llm_thread/components/llm_chat_thread_header/llm_chat_thread_header";
import { patch } from "@web/core/utils/patch";
patch(
LLMChatThreadHeader.prototype,
"llm_assistant.llm_assistant_dropdown_patch",
{
/**
* Get all available assistants
*/
get llmAssistants() {
// Make sure we have a valid llmChat reference
if (!this.llmChat) {
return [];
}
// Return the assistants array
return this.llmChat.llmAssistants || [];
},
/**
* Handle assistant selection
* @param {Object} assistant - The selected assistant
*/
onSelectAssistant(assistant) {
this.llmChatThreadHeaderView.saveSelectedAssistant(assistant.id);
},
/**
* Clear the selected assistant
*/
onClearAssistant() {
this.llmChatThreadHeaderView.saveSelectedAssistant(false);
},
}
);
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t
t-name="llm_assistant.LLMChatThreadHeader"
t-inherit="llm_thread.LLMChatThreadHeader"
t-inherit-mode="extension"
>
<xpath
expr="//div[hasclass('o_ThreadDropdowns')]/div[hasclass('dropdown')][1]"
position="before"
>
<!-- LLM Assistant Dropdown -->
<div class="dropdown me-2">
<button
class="btn btn-light dropdown-toggle d-flex align-items-center gap-2"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
style="max-width: 200px;"
>
<div class="text-truncate">
<t t-if="llmChatThreadHeaderView.selectedAssistant">
<t
t-esc="llmChatThreadHeaderView.selectedAssistant.name"
/>
</t>
<t t-else="">
Select Assistant
</t>
</div>
</button>
<ul class="dropdown-menu">
<li>
<a
class="dropdown-item text-truncate"
href="#"
t-on-click="onClearAssistant"
t-attf-class="{{ !llmChatThreadHeaderView.selectedAssistant ? 'active' : '' }}"
>
<i class="fa fa-times me-1" /> No Assistant
</a>
</li>
<li><hr class="dropdown-divider" /></li>
<t
t-foreach="llmAssistants"
t-as="assistant"
t-key="assistant.id"
>
<li>
<a
class="dropdown-item text-truncate"
href="#"
t-on-click="() => this.onSelectAssistant(assistant)"
t-att-title="assistant.name"
t-attf-class="{{ llmChatThreadHeaderView.selectedAssistant and llmChatThreadHeaderView.selectedAssistant.id === assistant.id ? 'active' : '' }}"
>
<t t-esc="assistant.name" />
</a>
</li>
</t>
</ul>
</div>
</xpath>
</t>
</templates>
@@ -0,0 +1,35 @@
/** @odoo-module **/
import { attr, many, one } from "@mail/model/model_field";
import { registerModel } from "@mail/model/model_core";
/**
* Model for LLM Assistant
*/
registerModel({
name: "LLMAssistant",
fields: {
id: attr({
identifying: true,
}),
name: attr(),
/**
* Threads associated with this assistant
*/
threads: many("Thread", {
inverse: "llmAssistant",
}),
/**
* The prompt associated with this assistant
*/
llmPrompt: one("LLMPrompt", {
inverse: "assistants",
}),
/**
* Prompt ID (used for loading from server)
*/
promptId: attr(),
defaultValues: attr(),
evaluatedDefaultValues: attr(),
},
});
@@ -0,0 +1,281 @@
/** @odoo-module **/
import { many } from "@mail/model/model_field";
import { clear } from "@mail/model/model_field_command";
import { registerPatch } from "@mail/model/model_core";
// Define assistant-related fields to fetch from server
const ASSISTANT_THREAD_FIELDS = ["assistant_id"];
/**
* Patch the LLMChat model to add assistants
*/
registerPatch({
name: "LLMChat",
fields: {
// Use attr instead of many for direct array access
llmAssistants: many("LLMAssistant"),
},
onChanges: [
{
dependencies: ["activeId"],
methodName: "onActiveIdChanged",
},
],
recordMethods: {
/**
* Load assistants from the server
*/
async loadAssistants() {
// Load assistants with their basic data and prompt_id (only actual model fields)
const assistantResult = await this.messaging.rpc({
model: "llm.assistant",
method: "search_read",
kwargs: {
domain: [["active", "=", true]],
fields: ["name", "default_values", "prompt_id"],
},
});
// Extract all prompt IDs to fetch their details
const promptIds = assistantResult
.map((assistant) => assistant.prompt_id && assistant.prompt_id[0])
.filter((id) => id); // Filter out falsy values
// If we have prompt IDs, fetch their details
let promptsById = {};
if (promptIds.length > 0) {
const promptResult = await this.messaging.rpc({
model: "llm.prompt",
method: "search_read",
kwargs: {
domain: [["id", "in", promptIds]],
fields: ["name", "input_schema_json"],
},
});
// Create a map of prompts by ID for easy lookup
promptsById = promptResult.reduce((acc, prompt) => {
acc[prompt.id] = {
id: prompt.id,
name: prompt.name,
inputSchemaJson: prompt.input_schema_json,
};
return acc;
}, {});
}
// Map assistant data and include prompt details if available
const assistantData = assistantResult.map((assistant) => {
const data = {
id: assistant.id,
name: assistant.name,
defaultValues: assistant.default_values,
// Don't set evaluatedDefaultValues here - it will be fetched dynamically when needed
};
// If this assistant has a prompt, include its ID and create the relationship
if (assistant.prompt_id && assistant.prompt_id[0]) {
const promptId = assistant.prompt_id[0];
data.promptId = promptId;
// If we have the prompt details, include them
if (promptsById[promptId]) {
data.llmPrompt = promptsById[promptId];
}
}
return data;
});
this.update({ llmAssistants: assistantData });
},
/**
* Override ensureThread to load assistants as well
* @override
*/
async ensureThread(options) {
// Load assistants if not already loaded
if (!this.llmAssistants || this.llmAssistants.length === 0) {
await this.loadAssistants();
}
// Call the original method
return this._super(options);
},
/**
* Override initializeLLMChat to include assistant loading
* @override
*/
async initializeLLMChat(
action,
initActiveId,
postInitializationPromises = []
) {
// Pass our loadAssistants promise to the original method
return this._super(action, initActiveId, [
...postInitializationPromises,
this.loadAssistants(),
]);
},
/**
* Override loadThreads to include assistant_id field
* @override
*/
async loadThreads(additionalFields = []) {
// Call the super method with our additional fields
return this._super([...additionalFields, ...ASSISTANT_THREAD_FIELDS]);
},
/**
* Override refreshThread to include assistant_id field
* @override
*/
async refreshThread(threadId, additionalFields = []) {
// Call the super method with our additional fields
return this._super(threadId, [
...additionalFields,
...ASSISTANT_THREAD_FIELDS,
]);
},
/**
* Override _mapThreadDataFromServer to add assistant information
* @override
*/
_mapThreadDataFromServer(threadData) {
// Get the base mapped data from super
const mappedData = this._super(threadData);
// Add assistant information if present
if (threadData.assistant_id) {
const assistantId = threadData.assistant_id[0];
mappedData.llmAssistant = {
id: assistantId,
name: threadData.assistant_id[1],
};
// Only fetch thread-specific evaluated default values for the active thread
if (this.activeId === threadData.id) {
this._fetchAssistantValuesForThread(threadData.id, assistantId);
}
} else {
// IMPORTANT: Clear the llmAssistant field when assistant_id is not present
mappedData.llmAssistant = clear();
}
return mappedData;
},
/**
* Handle active thread changes
*/
onActiveIdChanged() {
if (!this.activeId) {
return;
}
const [model, id] =
typeof this.activeId === "number"
? ["llm.thread", this.activeId]
: this.activeId.split("_");
// Get the active thread
const activeThread = this.messaging.models.Thread.findFromIdentifyingData(
{ id: Number(id), model }
);
if (!activeThread || !activeThread.llmAssistant) {
return;
}
// Fetch thread-specific evaluated default values for the active thread's assistant
this._fetchAssistantValuesForThread(
activeThread.id,
activeThread.llmAssistant.id
);
},
/**
* Fetch thread-specific evaluated default values for an assistant
* @param {Number} threadId - ID of the thread
* @param {Number} assistantId - ID of the assistant
* @private
*/
async _fetchAssistantValuesForThread(threadId, assistantId) {
try {
const result = await this.messaging.rpc({
route: "/llm/thread/get_assistant_values",
params: {
thread_id: threadId,
assistant_id: assistantId,
},
});
if (result.success) {
// Find the thread and update its assistant with the evaluated values
const thread = this.messaging.models.Thread.findFromIdentifyingData({
id: threadId,
model: "llm.thread",
});
if (thread) {
// Find the assistant in our registry
const assistant = this.llmAssistants.find(
(a) => a.id === assistantId
);
if (assistant) {
// Update the assistant with thread-specific evaluated values
if (result.evaluated_default_values) {
assistant.update({
defaultValues: result.default_values,
evaluatedDefaultValues: result.evaluated_default_values,
});
} else {
console.log("cleaning default values");
// Clean up default values when there are no evaluated default values
assistant.update({
defaultValues: clear(),
evaluatedDefaultValues: clear(),
});
}
// If we have prompt data, update or create the prompt relationship
if (result.prompt) {
const promptData = result.prompt;
const prompt =
this.messaging.models.LLMPrompt.findFromIdentifyingData({
id: promptData.id,
});
if (prompt) {
// Update existing prompt
prompt.update({
name: promptData.name,
inputSchemaJson: promptData.input_schema_json,
});
} else {
// Create new prompt record
this.messaging.models.LLMPrompt.insert({
id: promptData.id,
name: promptData.name,
inputSchemaJson: promptData.input_schema_json,
});
}
// Update assistant with prompt relationship
assistant.update({
promptId: promptData.id,
llmPrompt: { id: promptData.id },
});
}
}
}
} else {
console.error("Error fetching assistant values:", result.error);
}
} catch (error) {
console.error("Error in _fetchAssistantValuesForThread:", error);
}
},
},
});
@@ -0,0 +1,136 @@
/** @odoo-module **/
import { attr, one } from "@mail/model/model_field";
import { clear } from "@mail/model/model_field_command";
import { registerPatch } from "@mail/model/model_core";
registerPatch({
name: "LLMChatThreadHeaderView",
fields: {
/**
* Selected assistant ID
*/
selectedAssistantId: attr(),
/**
* Selected assistant record
*/
selectedAssistant: one("LLMAssistant", {
compute() {
if (!this.selectedAssistantId) {
return clear();
}
// This now searches within a collection of LLMAssistant records
// and returns a record instance, which is correct.
const assistants = this.threadView?.thread?.llmChat?.llmAssistants;
if (!assistants || !Array.isArray(assistants)) {
return clear();
}
return (
assistants.find(
(assistantRecord) =>
assistantRecord && assistantRecord.id === this.selectedAssistantId
) || clear()
);
},
}),
},
recordMethods: {
/**
* Initialize or reset state based on current thread
* @override
* @private
*/
_initializeState() {
this._super();
const currentThread = this.threadView?.thread;
if (!currentThread) {
this.update({
selectedAssistantId: clear(),
});
return;
}
this.update({
selectedAssistantId: currentThread.llmAssistant?.id || clear(),
});
},
/**
* Save selected assistant to the thread using the dedicated endpoint
* @param {Number|false} assistantId - ID of the selected assistant or false to clear
*/
async saveSelectedAssistant(assistantId) {
if (assistantId === this.selectedAssistantId) {
return;
}
// Update the local state immediately for responsive UI
this.update({
selectedAssistantId: assistantId || clear(),
});
const thread = this.threadView.thread;
const result = await this.messaging.rpc({
route: "/llm/thread/set_assistant",
params: {
thread_id: thread.id,
assistant_id: assistantId,
},
});
if (result.success) {
// Find the assistant in the list
const assistants = this.threadView?.thread?.llmChat?.llmAssistants;
if (assistants && assistantId) {
const assistant = assistants.find((a) => a.id === assistantId);
if (assistant) {
if (result.evaluated_default_values) {
// Update the individual assistant properties with new values
assistant.update({
defaultValues: result.default_values,
evaluatedDefaultValues: result.evaluated_default_values,
});
} else {
// Clean up default values when there are no evaluated default values
assistant.update({
defaultValues: clear(),
evaluatedDefaultValues: clear(),
});
}
}
}
// Refresh the thread to get updated data
await this.threadView.thread.llmChat.refreshThread(
this.threadView.thread.id
);
if (assistantId === false) {
this.update({
selectedAssistantId: clear(),
});
} else {
this.update({
selectedModelId: this.threadView.thread.llmModel?.id,
selectedProviderId:
this.threadView.thread.llmModel?.llmProvider?.id,
});
}
} else {
// Revert the local state if the server call failed
this.update({
selectedAssistantId:
this.threadView.thread.llmAssistant?.id || clear(),
});
// Show error message
this.messaging.notify({
type: "warning",
message: "Failed to update assistant",
});
}
},
},
});
@@ -0,0 +1,23 @@
/** @odoo-module **/
import { attr, many } from "@mail/model/model_field";
import { registerModel } from "@mail/model/model_core";
registerModel({
name: "LLMPrompt",
fields: {
id: attr({
identifying: true,
}),
name: attr(),
inputSchemaJson: attr({
default: "{}",
}),
/**
* Assistants using this prompt
*/
assistants: many("LLMAssistant", {
inverse: "llmPrompt",
}),
},
});
@@ -0,0 +1,7 @@
/** @odoo-module **/
// Import all models to ensure they are registered
import "@llm_assistant/models/llm_chat";
import "@llm_assistant/models/llm_assistant";
import "@llm_assistant/models/thread";
import "@llm_assistant/models/llm_chat_thread_header_view";
@@ -0,0 +1,48 @@
/** @odoo-module **/
import { attr, one } from "@mail/model/model_field";
import { registerPatch } from "@mail/model/model_core";
/**
* Patch the Thread model to add llmAssistant field
*/
registerPatch({
name: "Thread",
fields: {
/**
* The LLM assistant associated with this thread
*/
llmAssistant: one("LLMAssistant", {
inverse: "threads",
}),
/**
* The prompt ID associated with this thread (legacy support)
*/
promptId: attr(),
},
recordMethods: {
/**
* Override updateLLMChatThreadSettings to handle assistant
* @override
* @param {Object} settings - Settings object
* @param {Number|false} [settings.assistantId] - Assistant ID to set, or false to clear
*/
async updateLLMChatThreadSettings(settings = {}) {
const { assistantId, ...otherSettings } = settings;
// Prepare additional values for the assistant_id field
const additionalValues = {};
// Handle assistant_id if provided
if (assistantId !== undefined) {
additionalValues.assistant_id = assistantId || false;
}
// Call super with our additional values
return this._super({
...otherSettings,
additionalValues,
});
},
},
});
@@ -0,0 +1,62 @@
/** @odoo-module **/
import { LLMThreadHeader } from "@llm_thread/components/llm_thread_header/llm_thread_header";
import { patch } from "@web/core/utils/patch";
/**
* Minimal patch to add assistant functionality to existing thread header
* Reuses all existing patterns and follows DRY principles
*/
patch(LLMThreadHeader.prototype, {
setup() {
super.setup();
// Reuse existing store - it's already the patched version via useService
this.assistantStore = this.llmStore;
},
/**
* Get current assistant following existing pattern
*/
get currentAssistant() {
if (!this.assistantStore?.currentAssistant) return null;
return this.assistantStore.currentAssistant;
},
/**
* Get available assistants following existing pattern
*/
get availableAssistants() {
if (!this.assistantStore?._assistantsLoaded) return [];
return Array.from(this.assistantStore.llmAssistants.values());
},
/**
* Select assistant following existing update pattern
* @param {Object} assistant - Assistant object to select
*/
async selectAssistant(assistant) {
if (!this.assistantStore) return;
const assistantId = assistant ? assistant.id : null;
if (assistantId === this.currentAssistant?.id) return;
try {
this.state.isLoadingUpdate = true;
await this.assistantStore.selectAssistant(assistantId);
} catch (error) {
this.notification.add("Failed to update assistant", {
type: "danger",
});
console.error("Error updating assistant:", error);
} finally {
this.state.isLoadingUpdate = false;
}
},
/**
* Clear assistant selection
*/
async clearAssistant() {
await this.selectAssistant(null);
},
});
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<!-- Extend existing thread header to add assistant dropdown -->
<t
t-name="llm_thread.LLMThreadHeader"
t-inherit="llm_thread.LLMThreadHeader"
t-inherit-mode="extension"
>
<!-- Add assistant dropdown to DESKTOP header (after tools dropdown) -->
<xpath
expr="//div[hasclass('o-llm-thread-header')]//Dropdown[contains(@t-if, 'availableTools')]"
position="after"
>
<!-- Assistant Dropdown -->
<Dropdown t-if="availableAssistants.length > 0 or currentAssistant">
<button
class="btn btn-sm btn-outline-secondary dropdown-toggle"
>
<i class="fa fa-user me-1" />
<span
t-if="currentAssistant"
t-esc="currentAssistant.name"
/>
<span t-else="">No Assistant</span>
</button>
<t t-set-slot="content">
<!-- Clear Assistant Option -->
<t t-if="currentAssistant">
<DropdownItem
onSelected="() => this.clearAssistant()"
class="'text-danger'"
>
<i class="fa fa-times me-2" />
Clear Assistant
</DropdownItem>
<div class="dropdown-divider" />
</t>
<!-- Available Assistants -->
<t
t-foreach="availableAssistants"
t-as="assistant"
t-key="assistant.id"
>
<DropdownItem
onSelected="() => this.selectAssistant(assistant)"
>
<div class="d-flex align-items-center">
<i
t-if="currentAssistant?.id === assistant.id"
class="fa fa-check me-2 text-success"
/>
<span t-esc="assistant.name" />
</div>
</DropdownItem>
</t>
<!-- No Assistants Available -->
<div
t-if="availableAssistants.length === 0"
class="dropdown-item-text text-muted"
>
No assistants available
</div>
</t>
</Dropdown>
</xpath>
<!-- Mobile: No assistant dropdown in header - use form view via cog button instead -->
</t>
</templates>
@@ -0,0 +1,103 @@
/** @odoo-module **/
import { llmStoreService } from "@llm_thread/services/llm_store_service";
import { patch } from "@web/core/utils/patch";
import { rpc } from "@web/core/network/rpc";
/**
* Minimal patch to add assistant functionality to existing LLM store
* Reuses all existing patterns and infrastructure
*/
patch(llmStoreService, {
start(env, services) {
const llmStore = super.start(env, services);
const { orm, notification } = services;
// Store the original getDataLoaders method
const originalGetDataLoaders = llmStore.getDataLoaders.bind(llmStore);
// Add assistant-specific properties directly
llmStore.llmAssistants = new Map();
llmStore._assistantsLoaded = false;
// Define currentAssistant getter with proper context binding
Object.defineProperty(llmStore, "currentAssistant", {
get: function () {
const activeThread = this.activeLLMThread;
if (!activeThread?.assistant_id) return null;
const assistantId =
activeThread.assistant_id?.id || activeThread.assistant_id;
const assistant = this.llmAssistants.get(assistantId);
return assistant || activeThread.assistant_id;
},
enumerable: true,
configurable: true,
});
// Add other methods using Object.assign
Object.assign(llmStore, {
async loadLLMAssistants() {
try {
const assistants = await orm.searchRead(
"llm.assistant",
[["active", "=", true]],
["id", "name", "is_public", "provider_id", "model_id", "tool_ids"]
);
assistants.forEach((assistant) => {
this.llmAssistants.set(assistant.id, assistant);
});
this._assistantsLoaded = true;
} catch (error) {
console.warn(
"LLM assistants not available - llm_assistant module may not be installed:",
error.message
);
}
},
async selectAssistant(assistantId) {
const activeThread = this.activeLLMThread;
if (!activeThread) {
notification.add("No active thread to update", { type: "warning" });
return;
}
try {
// Use RPC endpoint instead of direct ORM call for better separation of concerns
const result = await rpc("/llm/thread/set_assistant", {
thread_id: activeThread.id,
assistant_id: assistantId,
});
if (!result.success && result.success !== undefined) {
notification.add("Failed to update assistant", { type: "danger" });
return;
}
// Reuse existing fetchData pattern to refresh thread data
await activeThread.fetchData([
"assistant_id",
"provider_id",
"model_id",
"tool_ids",
"prompt_id",
]);
} catch (error) {
console.error("Error selecting assistant:", error);
notification.add("Failed to update assistant", { type: "danger" });
}
},
// Extend existing getDataLoaders method instead of overriding initialize
getDataLoaders() {
const baseLoaders = originalGetDataLoaders();
return [...baseLoaders, this.loadLLMAssistants];
},
});
return llmStore;
},
});
@@ -0,0 +1 @@
# Test module for llm_assistant
@@ -0,0 +1,176 @@
import json
from odoo.tests.common import TransactionCase
class TestPromptArguments(TransactionCase):
"""Test prompt argument auto-detection and schema synchronization"""
def setUp(self):
super().setUp()
self.prompt_model = self.env["llm.prompt"]
def test_auto_detect_arguments_on_create(self):
"""Test that arguments are auto-detected when creating a prompt"""
prompt = self.prompt_model.create(
{
"name": "Test Prompt",
"template": "Hello {{name}}, your age is {{age}} and you live in {{city}}.",
"format": "text",
}
)
# Check that arguments were auto-detected
arguments = json.loads(prompt.arguments_json)
self.assertIn("name", arguments)
self.assertIn("age", arguments)
self.assertIn("city", arguments)
# Check that they are marked as required by default
self.assertTrue(arguments["name"]["required"])
self.assertTrue(arguments["age"]["required"])
self.assertTrue(arguments["city"]["required"])
def test_auto_detect_arguments_on_template_update(self):
"""Test that arguments are auto-detected when template is updated"""
prompt = self.prompt_model.create(
{"name": "Test Prompt", "template": "Hello {{name}}.", "format": "text"}
)
# Initially only has 'name' argument
arguments = json.loads(prompt.arguments_json)
self.assertEqual(len(arguments), 1)
self.assertIn("name", arguments)
# Update template to include more arguments
prompt.write(
{
"template": "Hello {{name}}, your email is {{email}} and phone is {{phone}}."
}
)
# Check that new arguments were auto-detected
arguments = json.loads(prompt.arguments_json)
self.assertEqual(len(arguments), 3)
self.assertIn("name", arguments)
self.assertIn("email", arguments)
self.assertIn("phone", arguments)
def test_ensure_arguments_sync(self):
"""Test the _ensure_arguments_sync method"""
prompt = self.prompt_model.create(
{
"name": "Test Prompt",
"template": "Hello {{name}}.",
"format": "text",
"arguments_json": json.dumps(
{
"name": {
"type": "string",
"description": "User's name",
"required": True,
}
}
),
}
)
# Update template to include new argument without updating schema
prompt.template = "Hello {{name}}, welcome to {{platform}}."
# Call _ensure_arguments_sync directly
prompt._ensure_arguments_sync()
# Check that new argument was added
arguments = json.loads(prompt.arguments_json)
self.assertIn("platform", arguments)
self.assertEqual(arguments["platform"]["type"], "string")
self.assertTrue(arguments["platform"]["required"])
def test_input_schema_json_computation(self):
"""Test that input_schema_json is computed correctly"""
prompt = self.prompt_model.create(
{
"name": "Test Prompt",
"template": "Hello {{name}} and {{email}}.",
"format": "text",
}
)
# Check that input_schema_json was computed
self.assertIsInstance(prompt.input_schema_json, dict)
self.assertEqual(prompt.input_schema_json["type"], "object")
self.assertIn("properties", prompt.input_schema_json)
self.assertIn("required", prompt.input_schema_json)
# Check that properties match auto-detected arguments
properties = prompt.input_schema_json["properties"]
self.assertIn("name", properties)
self.assertIn("email", properties)
# Check that required fields are correctly set
required = prompt.input_schema_json["required"]
self.assertIn("name", required)
self.assertIn("email", required)
def test_undefined_arguments_detection(self):
"""Test that undefined arguments are detected correctly"""
prompt = self.prompt_model.create(
{
"name": "Test Prompt",
"template": "Hello {{name}}, your age is {{age}}.",
"format": "text",
"arguments_json": json.dumps(
{
"name": {
"type": "string",
"description": "User's name",
"required": True,
}
# Note: 'age' is missing from schema but used in template
}
),
}
)
# Check that undefined arguments are detected
self.assertEqual(prompt.undefined_arguments, "age")
def test_argument_count_computation(self):
"""Test that argument count is computed correctly"""
prompt = self.prompt_model.create(
{
"name": "Test Prompt",
"template": "Hello {{name}}, {{greeting}} and {{farewell}}.",
"format": "text",
}
)
# Should have 3 arguments
self.assertEqual(prompt.argument_count, 3)
def test_extract_arguments_from_template(self):
"""Test the argument extraction method"""
# Test simple variable extraction
args = self.prompt_model._extract_arguments_from_template(
"Hello {{name}}, your age is {{age}}."
)
self.assertEqual(args, {"name", "age"})
# Test with spaces
args = self.prompt_model._extract_arguments_from_template(
"Hello {{ name }}, your age is {{ age }}."
)
self.assertEqual(args, {"name", "age"})
# Test with no arguments
args = self.prompt_model._extract_arguments_from_template(
"Hello world, no variables here."
)
self.assertEqual(args, set())
# Test with duplicate arguments
args = self.prompt_model._extract_arguments_from_template(
"Hello {{name}}, again {{name}}."
)
self.assertEqual(args, {"name"})
+56
View File
@@ -0,0 +1,56 @@
import logging
from jinja2 import Environment, Undefined
from odoo import _
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
def render_template(template=None, context=None):
"""
Replace argument placeholders in content with their values using Jinja2.
Args:
context (dict): Dictionary of argument values
Returns:
str: Content with placeholders replaced by values
:param context:
:param template:
"""
# Make a copy of context to avoid modifying the original
context_copy = dict(context)
# Process boolean values for JSON compatibility
processed_args = {}
for arg_name, arg_value in context_copy.items():
if isinstance(arg_value, bool):
# Convert Python True/False to JSON true/false
processed_args[arg_name] = "true" if arg_value else "false"
else:
processed_args[arg_name] = arg_value
# Create Jinja2 environment
env = Environment(
variable_start_string="{{",
variable_end_string="}}",
trim_blocks=True,
lstrip_blocks=True,
undefined=Undefined, # Handle missing variables gracefully
)
# Create and render the template
try:
template = env.from_string(template)
return template.render(**processed_args)
except Exception as e:
raise ValidationError(
_(
"Could not process the prompt template. Check that all placeholder "
"variables ({{variable}}) are correctly named and the template syntax is valid.\n\n"
"Details: %s"
)
% str(e)
) from e
@@ -0,0 +1,356 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Tree View -->
<record id="view_llm_assistant_tree" model="ir.ui.view">
<field name="name">llm.assistant.tree</field>
<field name="model">llm.assistant</field>
<field name="arch" type="xml">
<list>
<field name="name" />
<field name="code" />
<field name="res_model" />
<field name="category_id" />
<field name="provider_id" />
<field name="model_id" />
<field name="prompt_id" />
<field name="template_format" />
<field name="tag_ids" widget="many2many_tags" />
<field name="is_public" />
<field name="is_default" widget="boolean_toggle" />
<field name="thread_count" />
<field name="active" />
</list>
</field>
</record>
<!-- Form View -->
<record id="view_llm_assistant_form" model="ir.ui.view">
<field name="name">llm.assistant.form</field>
<field name="model">llm.assistant</field>
<field name="arch" type="xml">
<form string="Assistant">
<sheet>
<div class="oe_button_box" name="button_box">
<button
name="action_view_threads"
type="object"
class="oe_stat_button"
icon="fa-comments"
>
<field
name="thread_count"
widget="statinfo"
string="Threads"
/>
</button>
<button
name="action_view_prompt"
type="object"
class="oe_stat_button"
icon="fa-file-text-o"
invisible="not prompt_id"
>
<div class="o_field_widget o_stat_info">
<span class="o_stat_value">
<field
name="template_format"
readonly="1"
/>
</span>
<span class="o_stat_text">Template</span>
</div>
</button>
<button
name="toggle_active"
type="object"
class="oe_stat_button"
icon="fa-archive"
>
<field name="active" widget="boolean_toggle" />
</button>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only" />
<h1><field
name="name"
placeholder="Assistant Name"
/></h1>
</div>
<group>
<group name="configuration">
<field
name="code"
placeholder="e.g., roleplay, avatar_generation"
/>
<field
name="res_model"
placeholder="e.g., fleek.character"
/>
<field name="provider_id" />
<field name="model_id" />
<field
name="prompt_id"
options="{'no_create': False, 'no_open': False}"
/>
<field name="category_id" readonly="1" />
<field
name="tag_ids"
widget="many2many_tags"
readonly="1"
/>
</group>
<group name="access_control">
<field name="is_public" />
<field name="is_default" />
<field
name="allowed_group_ids"
widget="many2many_tags"
invisible="is_public"
options="{'no_create': True}"
/>
</group>
</group>
<notebook>
<page string="Configuration" name="configuration">
<group>
<field
name="default_values"
widget="json_editor"
/>
<field
name="has_dynamic_defaults"
help="Enable if your default values contain template expressions that should be evaluated"
/>
</group>
<!-- Reset Defaults Button -->
<div class="mt-2">
<button
name="action_reset_defaults"
string="Reset Defaults"
type="object"
class="btn-secondary"
invisible="not prompt_id"
help="Reset default values from the prompt template's argument schema defaults"
/>
</div>
<!-- Help Text -->
<div
class="alert alert-info w-100 mt-3 mb-3"
role="alert"
>
<p class="mb-2">
<strong
>Default Values:</strong> Define default values for prompt variables in JSON format.
These will be used as inputs to the prompt template.
</p>
<p class="mb-2">
<strong
>Dynamic Defaults:</strong> When enabled, you can use template syntax like
<code>{{ user.name }}</code> or <code
>{{ related_record.get_field('name') }}</code> in your default values.
</p>
<p class="mb-0">
<strong
>Reset Defaults:</strong> Click "Reset Defaults" to automatically populate this field
with default values from the selected prompt template's argument schema.
</p>
</div>
</page>
<page string="Tools" name="tools">
<group>
<field
name="tool_ids"
widget="many2many_tags"
options="{'no_create': True}"
placeholder="Select tools for this assistant..."
/>
<field
name="tool_calls_max"
help="Maximum number of consecutive tool calls allowed before breaking the loop to prevent infinite tool calling"
/>
</group>
<div class="text-muted px-3 py-2">
Select the tools this assistant may need. These tools will be auto selected when you assign this assistant on LLM Chat Thread.
<br />
<strong
>Max Tool Calls:</strong> Prevents infinite loops by limiting consecutive tool executions (default: 5).
</div>
</page>
<page string="Template" name="template">
<div
class="alert alert-info w-100 mb-3"
role="alert"
>
<p class="mb-0">
<strong>Template Management:</strong>
The template is managed through the associated prompt record.
Format: <strong><field
name="template_format"
readonly="1"
class="oe_inline"
/></strong>
</p>
</div>
<group>
<field
name="template"
widget="ace"
readonly="1"
options="{'mode': 'text'}"
/>
</group>
<div class="mt-3">
<button
name="action_view_prompt"
type="object"
string="Edit Template in Prompt"
class="btn-primary"
invisible="not prompt_id"
/>
<p class="text-muted mt-2">
<i class="fa fa-info-circle" />
The template is read-only here. Click the button above to edit it in the prompt record.
</p>
</div>
</page>
<page string="Preview" name="preview">
<field
name="system_prompt_preview"
widget="ace"
options="{'mode': 'markdown'}"
readonly="1"
/>
</page>
<page string="Related Threads" name="related_threads">
<field name="thread_ids" readonly="1">
<list>
<field name="name" />
<field name="user_id" />
<field name="write_date" />
<button
name="action_open_thread"
string="Open"
type="object"
class="btn btn-primary btn-sm"
/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter />
</form>
</field>
</record>
<!-- Search View -->
<record id="view_llm_assistant_search" model="ir.ui.view">
<field name="name">llm.assistant.search</field>
<field name="model">llm.assistant</field>
<field name="arch" type="xml">
<search string="Search Assistants">
<field name="name" />
<field name="code" />
<field name="res_model" />
<field name="category_id" />
<field name="tag_ids" />
<field name="provider_id" />
<field name="model_id" />
<field name="prompt_id" />
<field name="allowed_group_ids" />
<separator />
<filter
string="Public"
name="public"
domain="[('is_public', '=', True)]"
/>
<filter
string="Private"
name="private"
domain="[('is_public', '=', False)]"
/>
<filter
string="Default"
name="default"
domain="[('is_default', '=', True)]"
/>
<filter
string="Text Templates"
name="format_text"
domain="[('template_format', '=', 'text')]"
/>
<filter
string="YAML Templates"
name="format_yaml"
domain="[('template_format', '=', 'yaml')]"
/>
<filter
string="JSON Templates"
name="format_json"
domain="[('template_format', '=', 'json')]"
/>
<filter
string="Archived"
name="inactive"
domain="[('active', '=', False)]"
/>
<group expand="0" string="Group By">
<filter
string="Category"
name="category"
domain="[]"
context="{'group_by': 'category_id'}"
/>
<filter
string="Provider"
name="provider"
domain="[]"
context="{'group_by': 'provider_id'}"
/>
<filter
string="Model"
name="model"
domain="[]"
context="{'group_by': 'model_id'}"
/>
<filter
string="Prompt Template"
name="prompt"
domain="[]"
context="{'group_by': 'prompt_id'}"
/>
<filter
string="Template Format"
name="template_format"
domain="[]"
context="{'group_by': 'template_format'}"
/>
<filter
string="Access Level"
name="access_level"
domain="[]"
context="{'group_by': 'is_public'}"
/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_llm_assistant" model="ir.actions.act_window">
<field name="name">Assistants</field>
<field name="res_model">llm.assistant</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_llm_assistant_search" />
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first AI Assistant!
</p>
<p>
Assistants can be configured with specific roles, goals, and tools to enhance your AI interactions.
</p>
</field>
</record>
</odoo>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Add Assistant menu under LLM Configuration -->
<menuitem
id="menu_llm_assistant"
name="Assistants"
parent="llm.menu_llm_config"
action="action_llm_assistant"
sequence="26"
/>
<!-- Root Menu for Prompts -->
<menuitem
id="llm_prompt_menu"
name="Prompts"
parent="llm.menu_llm_root"
action="llm_prompt_action"
sequence="20"
/>
<!-- Configuration Menu -->
<menuitem
id="llm_prompt_configuration_menu"
name="Prompts Configuration"
parent="llm.menu_llm_config"
sequence="30"
/>
<!-- Configuration Sub-menus -->
<menuitem
id="llm_prompt_category_menu"
name="Categories"
parent="llm_prompt_configuration_menu"
action="llm_prompt_category_action"
sequence="10"
/>
<menuitem
id="llm_prompt_tag_menu"
name="Tags"
parent="llm_prompt_configuration_menu"
action="llm_prompt_tag_action"
sequence="20"
/>
</odoo>
@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Category Tree View -->
<record id="llm_prompt_category_view_tree" model="ir.ui.view">
<field name="name">llm.prompt.category.tree</field>
<field name="model">llm.prompt.category</field>
<field name="arch" type="xml">
<list>
<field name="sequence" widget="handle" />
<field name="complete_name" />
<field name="code" />
<field name="prompt_count" />
</list>
</field>
</record>
<!-- Category Form View -->
<record id="llm_prompt_category_view_form" model="ir.ui.view">
<field name="name">llm.prompt.category.form</field>
<field name="model">llm.prompt.category</field>
<field name="arch" type="xml">
<form string="Prompt Category">
<sheet>
<div class="oe_button_box" name="button_box">
<button
name="toggle_active"
type="object"
class="oe_stat_button"
icon="fa-archive"
>
<field
name="active"
widget="boolean_toggle"
options="{'terminology': 'archive'}"
/>
</button>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only" />
<h1>
<field
name="name"
placeholder="e.g. Data Analysis"
/>
</h1>
</div>
<group>
<group>
<field name="parent_id" />
<field name="code" />
<field name="sequence" />
</group>
<group>
<field name="complete_name" readonly="1" />
<field name="prompt_count" readonly="1" />
</group>
</group>
<notebook>
<page string="Description">
<field
name="description"
placeholder="Describe the purpose of this category..."
/>
</page>
<page string="Child Categories">
<field name="child_ids">
<list editable="bottom">
<field name="sequence" widget="handle" />
<field name="name" />
<field name="code" />
<field name="prompt_count" />
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Category Search View -->
<record id="llm_prompt_category_view_search" model="ir.ui.view">
<field name="name">llm.prompt.category.search</field>
<field name="model">llm.prompt.category</field>
<field name="arch" type="xml">
<search string="Search Categories">
<field name="name" />
<field name="code" />
<filter
string="Archived"
name="inactive"
domain="[('active', '=', False)]"
/>
<group expand="0" string="Group By">
<filter
string="Parent Category"
name="group_by_parent"
domain="[]"
context="{'group_by': 'parent_id'}"
/>
</group>
</search>
</field>
</record>
<!-- Action for Categories -->
<record id="llm_prompt_category_action" model="ir.actions.act_window">
<field name="name">Prompt Categories</field>
<field name="res_model">llm.prompt.category</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_group_by_parent': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new prompt category
</p>
<p>
Categories help organize and classify your prompt templates.
</p>
</field>
</record>
</odoo>

Some files were not shown because too many files have changed in this diff Show More