diff --git a/custom_addons/README.md b/custom_addons/README.md new file mode 100644 index 0000000..9bba804 --- /dev/null +++ b/custom_addons/README.md @@ -0,0 +1,30 @@ +# Odoo addons + +This module contains the custom Odoo addons designed to bridge the Odoo ERP with the **Perry AI ecosystem**. + +These addons ensure that Perry can "listen" to communications, process intents asynchronously, and stage sensitive operations for human verification. Every addon follows a non-blocking architecture and is fully instrumented for real-time observability. + +## Structure + +- `perry_webhook`: Intercepts Odoo messages via the `mail.message` model and dispatches them to the Perry Ingest API using background threads. +- `perry_hitl`: Implements the "Human-in-the-Loop" safety layer, managing the `perry.pending.action` model for staging AI intents. + +## Core features + +### Human-in-the-Loop (HITL) +To maintain data integrity, sensitive operations (Payments, Leads, Calendar) are never created directly by the AI. They are staged as "Pending Actions" where a human must review the extracted data and click "Confirm" to execute the final Odoo record creation. + +### Non-blocking architecture +All external API calls are executed using Python's `threading` library. This ensures that the Odoo UI remains responsive and the message input bar clears instantly, while Perry processes the request in a background thread with its own database cursor. + +### Full observability +Every hook, API call, and human confirmation is wrapped in **Logfire spans**. This provides a complete audit trail from the moment a user types a message to the final execution of an administrative task. + +## Documentation + +### Addon-specific Documentation +- [Perry Webhook Addon](docs/addons/perry-webhook.md) — How messages are intercepted and dispatched. +- [Perry Human-in-the-Loop](docs/addons/perry-human-in-the-loop.md) — Technical details on the staging and confirmation logic. + +### Integration Standards +- [Logfire Configuration](docs/logfire.md) — Setting up traceability across the Odoo instance. diff --git a/custom_addons/docs/README.md b/custom_addons/docs/README.md deleted file mode 100644 index 78215cc..0000000 --- a/custom_addons/docs/README.md +++ /dev/null @@ -1,224 +0,0 @@ -# Traceability & Observability - -
- -
- - Logfire Logo - -
- -
- -This project implements robust observability using **[Pydantic Logfire](https://logfire.pydantic.dev/docs/)**. - -Logfire allows us to trace the complete lifecycle of our webhook events, internal Odoo model logic and background threads, providing a clear **live dashboard** for debugging and performance monitoring across different addons. - -
- Logfire General Dashboard -

Overview of the Logfire tracing dashboard

-
- ---- - -## Configuration Setup - -Logfire is configured individually inside each Odoo addon module (e.g., `perry_webhook/` and `perry_human_loop/`) via their respective `logfire_config.py` files. - -It ensures telemetry is initialized only once per module and gracefully disables itself if no token is found, preventing crashes in environments without Logfire configured. - -```python -import os -import logfire - -_configured = False - -def configure_logfire() -> None: - global _configured - - if _configured: - return - - token = os.getenv("LOGFIRE_TOKEN") - - # Disable if no token is present - if not token: - logfire.configure(send_to_logfire=False) - _configured = True - return - - # Configure specific service names per addon - logfire.configure( - token=token, - # For perry_webhook - service_name=os.getenv("LOGFIRE_SERVICE_NAME_WEBHOOK", "perry-odoo-webhook"), - # For perry_human_loop - service_name=os.geten("LOGFIRE_SERVICE_NAME_HUMAN_LOOP", "perry-odoo-human-loop"), - environment=os.getenv("LOGFIRE_ENVIRONMENT", "development"), - ) - - _configured = True -``` - -### Environment variables -To enable Logfire across the Odoo environment, the following variables must be present in the `.env` file: - -* `LOGFIRE_TOKEN`: This is the authentication token generated from the Logfire dashboard. To generate one: - 1. Log into your Pydantic Logfire account and select your project. - 2. Navigate to **Project settings** > **Write tokens** > **New**. - 3. Enter a **Description** to identify your token. - 4. Set the **Expiration** (e.g., "No expiration"). - 5. Click the **Create token** button and copy the generated value. - -* `LOGFIRE_ENVIRONMENT`: Identifies the deployment context (`development`, `staging`, `production`). - -* `LOGFIRE_SERVICE_NAME_WEBHOOK`: Identifies the source of the logs for the Webhook addon (defaults to `perry-odoo-webhook`). - -* `LOGFIRE_SERVICE_NAME_HUMAN_LOOP`: Identifies the source of the logs for the Human Loop addon (defaults to `perry-odoo-human-loop`). - ---- - -## How to use Logfire in the code - -We use Logfire to explicitly track different flows, like Odoo record creations, action confirmations and background API requests. - -Here is how the different logging methods should be used when developing new features or interacting with Odoo models: - -### 1. Contextual spans (`logfire.span`) -Use spans to measure the duration and track the internal steps of a specific block of code (like processing an Odoo action or a threaded FastAPI call). -```python -# The span automatically measures how long the block takes to execute -with logfire.span( - "Send Odoo webhook to FastAPI", - dbname=dbname, - channel_id=channel_id, - session_id=payload.get("session_id") -): - # Code executed inside this context will be grouped in the Logfire dashboard - db_registry = registry(dbname) - with db_registry.cursor() as cr: - # ... Odoo environment logic ... -``` -
- Logfire Open Span -

Example of an open span showing execution details and metadata

-
- -### 2. Informational events (`logfire.info`) -Use this for standard events that indicate the normal flow of the application. Always include relevant Odoo metadata like Record IDs. -```python -logfire.info( - "Pending lead created in Odoo", - action_id=getattr(rec, "id", None), - has_name=bool(data.get("name")), - has_email_from=bool(data.get("email_from")) -) -``` - -### 3. Warnings (`logfire.warning`) -Use this for non-fatal errors or unexpected states that do not stop the execution but require attention (e.g., ignoring a state or skipping an action). -```python -logfire.warning( - "Pending action ignored because state is not executable", - action_id=getattr(rec, "id", None), - action_type=getattr(rec, "action_type", None), - state=getattr(rec, "state", None) -) -``` - -### 4. Errors without Exceptions (`logfire.error`) -Use this when a business logic failure occurs (e.g., a related Odoo record is missing or an external API returns a bad status code) but Python itself has not automatically raised an Exception. -```python -logfire.error( - "Partner or Journal not found for pending payment", - action_id=getattr(rec, "id", None), - partner_found=bool(partner), - journal_found=bool(journal) -) -``` - -### 5. Caught Exceptions (`logfire.exception`) -Use this specifically inside `except` blocks. Logfire will automatically capture the full Python stack trace and send it to the dashboard. -```python -try: - # Triggering the webhook - response = re.post(...) -except Exception as e: - logfire.exception( - "Failed to trigger webhook", - error=str(e), - session_id=payload.get("session_id"), - message_id=payload.get("message", {}).get("id") - ) -``` - ---- - -## Maintaining observability in new features - -When adding new functionalities (like a new Odoo Model, Controller or background thread), follow these strict guidelines to ensure end-to-end traceability: - -1. **Instrument Odoo hooks:** Add a `logfire.span` or `logfire.info` inside `@api.model_create_multi` blocks or custom methods (like `action_confirm`) to track record lifecycles. - -2. **Wrap main logic blocks in spans:** Any method that involves network requests (`requests.post`), thread creation or heavy data processing (like `json.loads` over loops) should be enclosed in a `with logfire.span("Context Name"):` block. - -3. **Inject context metrics:** Do not just log strings. Always pass relevant key-value pairs to the logfire functions (e.g., `action_id`, `session_id`, `partner_found`, `status_code`). This is crucial for filtering and searching logs in the dashboard. - -4. **Handle exceptions gracefully:** Ensure that every major `try/except` block utilizes `logfire.exception()` before raising an `odoo.exceptions.ValidationError` or logging it locally. This guarantees that no silent errors slip past Logfire. - ---- - -## Scaling: Adding new Addons - -If you develop a new Odoo addon (e.g., `perry_accounting`) and want to include it in the observability stack, you must follow these steps to ensure its logs are correctly segmented in the dashboard: - -### 1. Create a local configuration -Copy the `logfire_config.py` template into the root of the new addon. This ensures that the addon can initialize its own connection to Logfire. - -### 2. Define a unique Service Name -Add a new variable to the `.env` file to identify the new addon. This prevents logs from different modules from mixing together. - -**Example for a new addon:** -```bash -# .env -LOGFIRE_SERVICE_NAME_NEW_ADDON=perry-odoo-new-feature -``` - -### 3. Update the addon's config logic -In the new `logfire_config.py`, make sure to reference that specific environment variable: - -```python -# logfire_config.py inside the new addon -logfire.configure( - token=token, - service_name=os.getenv("LOGFIRE_SERVICE_NAME_NEW_ADDON", "perry-odoo-new-feature"), - environment=os.getenv("LOGFIRE_ENVIRONMENT", "development"), -) -``` - -### 4. Initialize in the Addon -Import and call `configure_logfire()` in the `__init__.py` of the models or at the top of the main model files. - -> [!IMPORTANT] -> Since Odoo loads modules dynamically, failing to call the configuration inside the new addon will result in Logfire being disabled for that specific module's logic, even if it is enabled elsewhere. - ---- - -## Navigating the Logfire Dashboard - -Once the local application is running and generating traffic, you can view the traces in real-time: - -
- Logfire Live Dashboard -

The 'Live' view in Logfire showing a timeline and captured spans

-
- -To inspect the logs effectively: - -1. Log in to your **[Pydantic Logfire Dashboard](https://logfire.pydantic.dev/)** and select the active project. - -2. In the left sidebar, under the **OBSERVE** section, click on **Live** (to see logs coming in right now) or **Explore** (to search through past logs). - -3. **Locate Spans:** Look for entries in the list that have a blue `+ [number]` badge next to them (e.g., `+ 95`). This indicates a **Span** containing multiple nested steps. - -4. **Inspect details:** Click anywhere on that row to expand it. A side panel will open showing the complete execution tree, background thread events, API responses, and all the contextual Odoo variables (like `action_id` or `session_id`) that are injected in the code.