Files
ai_logistic_outlook_integra…/README.md
2025-11-12 08:53:59 -03:00

354 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Email Export Process Monitor with OCI Generative AI, Outlook Emails and Oracle Database
This project automates the monitoring of export and logistics communications by reading Outlook `.msg` emails from OCI Object Storage, analyzing them with OCI Generative AI to extract status and alerts, summarizing content, and displaying results in a Flask web application backed by Oracle Database.
---
# Email Export Process Monitor with OCI Generative AI, Outlook e-mails and Oracle Database
> A productiongrade pipeline that **reads Outlook `.msg` emails** from an **OCI Object Storage** inbox, uses **Generative AI** to understand the **status of an export/logistics process**, **summarizes** each message, flags **alerts**, and serves a **timeline report** via a **Flask web app** backed by **Oracle Database**.
---
## 1) What problem this project solves (Use Case)
Export and logistics operations generate long email threads (forwarded/quoted chains, multiple senders, mixed languages). Manually reading and updating spreadsheets or systems is slow and errorprone.
This project **automates** that routine:
1. **Ingest** Outlook emails (`.msg`) placed in an **OCI Object Storage** bucket (e.g., `inbox/`).
2. **Parse** each email (including forwarded/replied headers) and **split** the thread into individual communications.
3. **Summarize** and **classify** each communication with **OCI Generative AI** (via LangChain), extracting **structured fields** like:
- `date_sent` (ISO8601 when possible)
- `booking`
- `email_from`, `email_to`, `subject`
- `status` (e.g., “Send Note to Forwarder”, “Approve Invoice”, “Perform Weighing”, etc.)
- `brief_description` (12 lines)
- `alert` (“YES”/“NO” based on urgency, problems, missing docs, delays, etc.)
4. **Persist** the result to **Oracle Database** (`PROCESSED_EMAILS`), one row per communication (thread index).
5. **Move** processed files to a `processed/` prefix to avoid reprocessing.
6. **Visualize** a **timeline report** and filters in a **Flask UI** (date range, booking, status, alerts). You can also open each original email to inspect content.
**Outcome:** A nearrealtime **control panel** of export process communications with **actionable alerts** and **structured status**—no manual triage of huge mail chains.
![img.png](images/img.png)
---
## 2) Other use cases you can support with the same architecture
- **Accounts Payable/Receivable**: triage invoice/dispute emails, extract amounts, due dates, and blockers.
- **Customer Support**: summarize tickets by email, detect intent/severity, and push alerts to a dashboard.
- **Procurement**: track RFQs/quotes/PO confirmations arriving by email and update ERP status fields.
- **Compliance**: autodetect phrases indicating risk/backlogs or missing mandatory documents.
- **Sales Ops**: parse quote/booking confirmations and flag risks (price mismatch, dates, missing attachments).
---
## 3) Architecture (high level)
```mermaid
flowchart LR
A[Outlook emails .msg\nOCI Object Storage bucket: inbox/] --> B[Processor\nprocess_emails_v2.py]
B -->|Split thread + chunk| C[LLM OCI Generative AI\nmodel_id: cohere.command-a-03-2025]
C -->|JSON results| D[Oracle DB 23ai\nEMAILS_PROCESSADOS]
B -->|Move done| A2[Object Storage: processed/]
D --> E[Flask App app_emails.py\nTimeline + Filters + View .msg]
```
Key behaviors:
- **Thread splitter** detects “From/De, Sent/Sent, To/Para, Subject/Subject…” headers and separators (“Original Message / Original Message / Forwarded message”).
- **Chunking** limits each LLM call to ~10k chars (≈2.5k tokens) to keep cost/latency predictable.
- **Prompting** uses a **deterministic, JSONonly** instruction in Portuguese tailored to **international logistics**.
- **DB write** normalizes dates and strings, stores `thread_index` per communication.
- **UI** renders a chronological **timeline** with filters and **redacted emails** if desired.
---
## 4) Technologies
| Layer | Technology | Why |
|---|---|---|
| Email parsing | `extract_msg` | Robust reader for Outlook `.msg` files (headers, body) |
| Object storage | **OCI Object Storage** | Durable inbox/processed area, cheap and scalable |
| AI runtime | **OCI Generative AI** via `langchain_community.chat_models.oci_generative_ai.ChatOCIGenAI` | Secure, lowlatency, enterprise LLM serving |
| Orchestration | **LangChain** (`ChatPromptTemplate`) | Structured prompting & message assembly |
| Database | **Oracle Database 23ai** (`oracledb`) | Reliable transactional store, SQL analytics, wallet auth |
| API/UI | **Flask** | Simple serverside UI for timeline + filters |
| Config/Auth | `oci` SDK + `~/.oci/config` profile | Standardized credential management |
---
## 5) Project structure (key files)
- **`process_emails_v2.py`** batch processor:
- Lists `.msg` files in `BUCKET_NAME` inbox
- Downloads to temp, **extracts top headers + body**
- **Splits** thread into messages and **chunks** text to fit LLM limits
- **Builds prompt** and calls **OCI GenAI** for each chunk
- **Parses JSON**, **normalizes** fields, and **inserts** into `PROCESSED_EMAILS`
- On success, **moves** the `.msg` object to `processed/`
- Propagates **last detected `booking`** to blank items of the same file
- **`app_emails.py`** Flask web app:
- Connects to Oracle using **wallet** (`WALLET_PATH`) and `DB_ALIAS`
- Reads from **`PROCESSED_EMAILS_REDACTED` view** (recommended) or the base table
- Provides **filters**: `booking`, `alert (YES/NO)`, `status`, `date range`, and option to **include forwarded-only** messages
- Shows **timeline** sorted by `DATE_SENT` ASC
- Route **`/view_email/<file_name>`** renders the original `.msg` (downloaded from OCI) for inspection
---
## 6) Data model
### 6.1 Table
`PROCESSED_EMAILS` (from the inline DDL in `process_emails_v2.py`):
```sql
CREATE TABLE PROCESSED_EMAILS (
ID NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
DATE_SENT TIMESTAMP,
BOOKING VARCHAR2(100),
EMAIL_FROM VARCHAR2(500),
EMAIL_TO VARCHAR2(1000),
SUBJECT VARCHAR2(1000),
STATUS VARCHAR2(100),
BRIEF_DESCRIPTION VARCHAR2(1000),
ALERT VARCHAR2(10),
THREAD_INDEX NUMBER(10),
FILE_NAME VARCHAR2(400),
INSERT_DATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
> **Tip:** For the UI, create a sanitized view that redacts email addresses:
```sql
CREATE OR REPLACE VIEW PROCESSED_EMAILS_REDACTED AS
SELECT
REGEXP_REPLACE(
REGEXP_REPLACE(
REGEXP_REPLACE(
EMAIL_TO,
'([[:alpha:]][[:alpha:] .''()_-]*)<([^>]+)>',
'<\2>'
),
'([A-Za-z])([A-Za-z0-9._%+-]*)@([A-Za-z])([A-Za-z0-9.-]*)(\.[A-Za-z]{2,}(?:\.[A-Za-z]{2,})*)',
'\1******@\3******\5',
1, 0, 'i'
),
'\s*;\s*', '; '
) AS EMAIL_TO,
REGEXP_REPLACE(
REGEXP_REPLACE(
REGEXP_REPLACE(
EMAIL_FROM,
'([[:alpha:]][[:alpha:] .''()_-]*)<([^>]+)>',
'<\2>'
),
'([A-Za-z])([A-Za-z0-9._%+-]*)@([A-Za-z])([A-Za-z0-9.-]*)(\.[A-Za-z]{2,}(?:\.[A-Za-z]{2,})*)',
'\1******@\3******\5',
1, 0, 'i'
),
'\s*;\s*', '; '
) AS EMAIL_FROM,
SUBJECT,
REGEXP_REPLACE(
BOOKING,
'([A-Za-z0-9])([A-Za-z0-9 _.-]*)',
'***'
) AS BOOKING,
DATE_SENT,
STATUS,
FILE_NAME,
THREAD_INDEX, INSERT_DATE, BRIEF_DESCRIPTION, ALERT
FROM PROCESSED_EMAILS;
/
```
### 6.2 Key normalization logic
- `normalize_datetime()` parses ISO8601 and common patterns, removes TZ for DB TIMESTAMP.
- `sanitize_value()` stringifies lists/dicts and trims empty values.
---
## 7) Configuration
Create a JSON file named **`./config`** used by **both** scripts.
Minimum keys (adapt to your tenancy and DB):
```json
{
"oci_profile": "DEFAULT",
"llm_endpoint": "https://inference.generativeai.us-phoenix-1.oci.oraclecloud.com",
"namespace": "xxxxxxxxxxxxxxxx",
"compartment_id": "ocid1.compartment.oc1..example",
"bucket-profile": "DEFAULT",
"bucket": "emails",
"WALLET_PATH": "/path/to/db/wallet",
"DB_ALIAS": "adb_high",
"USERNAME": "APPUSER",
"PASSWORD": "****"
}
```
> In `process_emails_v2.py` the following constants are read/used:
> - `MODEL_ID="cohere.command-a-03-2025"` (you can change to another model available in your tenancy)
> - `SERVICE_ENDPOINT`, `COMPARTMENT_ID`, `AUTH_PROFILE`
> - `BUCKET_NAME`, `PROCESSED_PREFIX="processed/"`
> - `DB_ALIAS`, `USERNAME`, `PASSWORD`, `WALLET_PATH`
> In `app_emails.py` youll also see:
> - `bucket-profile`, `bucket`, `compartment_id` for Object Storage access
> - The Flask app runs by default on **`0.0.0.0:5015`**
OCI credentials come from `~/.oci/config` under the chosen profile(s).
---
## 8) How the processing works (deep dive)
### 8.1 Extract the latest message from a `.msg`
- `extract_msg_text(path)` uses `extract_msg.Message` to read `sender`, `to`, `subject`, `date`, and `body`.
- Returns a dict with `top` (headers) and `body`.
### 8.2 Split threads reliably
- `split_refined_email_thread(text)` detects boundaries using a robust regex that looks for header groups (From/De, Sent/Sent, To/Para, Subject/Subject) and common separators like “Original Message”, “Original Message”, “Forwarded message”.
- Fallback to `split_thread_by_headers()` if needed.
### 8.3 Chunking & prompts
- `chunk_by_size(text, MAX_CHARS_PER_CALL=10000)` ensures each LLM call is bounded.
- `prepare_blocks_for_llm(headers_topo, body)` packs the first chunk with top headers if needed.
- `build_prompt(bloco_texto, headers_topo)` (Portuguese) directs the model to **return JSON ONLY** with fields:
`data_envio, booking, email_from, email_to, subject, status, brief_description, alert`
and maps domainspecific phrases to **standardized `status`** values (e.g., *“Send Note to Forwarder”, “Realizar Estufagem”, “Validar Peso”, “Approve Invoice”*, etc.).
### 8.4 LLM call
- `call_llm_extract()` instantiates `ChatOCIGenAI` with:
- `model_id=MODEL_ID` (default: `cohere.command-a-03-2025`)
- `temperature=0.0` for determinism, `max_tokens≈1800`, `top_p=0.9`
- Cleans fenced code blocks and parses JSON; takes first dict if a list is returned.
### 8.5 Persist results
- `insert_into_database(item, file_name, thread_index)` writes each communication to `PROCESSED_EMAILS`.
- After processing a file, the **last detected `booking`** is propagated to rows where its empty (same `FILE_NAME`).
### 8.6 Idempotence
- Objects are **moved to `processed/`** after success to prevent reprocessing.
---
## 9) Running locally (endtoend)
### 9.1 Prerequisites
- **Python 3.10+**
- **Oracle Instant Client** (for thick mode) and **DB wallet** (place path in `WALLET_PATH`)
- Access to **OCI Tenancy** with permissions to read the chosen **Object Storage** bucket
- A table `PROCESSED_EMAILS` created in your schema
Install dependencies (example):
```bash
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install flask oracledb oci extract_msg langchain langchain-core langchain-community
```
### 9.2 Prepare Object Storage
- Create bucket (e.g., `emails`).
- Upload Outlook `.msg` files into `emails/inbox/` (or your chosen prefix).
Ensure the paths in `process_emails_v2.py` match your structure.
### 9.3 Configure
- Create `./config` (see section **7**).
- Ensure `~/.oci/config` has the profile named in `"oci_profile"` / `"bucket-profile"`.
### 9.4 Process emails
Run the batch once (or schedule via cron):
```bash
python process_emails_v2.py
```
Expected logs:
- Files listed and downloaded
- Thread chunks sent to LLM
- Rows inserted in `PROCESSED_EMAILS`
- File moved to `processed/`
### 9.5 Start the web app
```bash
python app_emails.py
```
Open: **http://localhost:5015/**
Use filters:
- **Booking**: caseinsensitive contains
- **Alert**: `YES` or `NO`
- **Status**: dropdown from `SELECT DISTINCT STATUS …`
- **Date range**: `inicio` / `fim` (inclusive)
- **Include forwardedonly**: checkbox to include messages without a classified status
Click on entries to **view the original email** via `/view_email/<file_name>`.
---
## 10) Testing tips
- Add a small test `.msg` with a short thread containing an English and a Portuguese part referencing export steps (e.g., DUe, invoice approval, warehouse dates).
- Verify:
- `status` gets standardized per prompt rules
- `alert` hits **“YES”** when theres a delay/missing document/urgent note
- Multiple communications in the same thread create **multiple rows** with incrementing `THREAD_INDEX`
- Timeline sorts by `DATE_SENT` ASC
- Create the **REDACTED view** and point the app to it if sharing the UI broadly.
---
## 11) Operations & scaling
- **Throughput**: adjust `MAX_CHARS_PER_CALL` and chunking to balance latency/cost.
- **Retries**: wrap `call_llm_extract()` with exponential backoff if needed.
- **Observability**: log `file_name`, `thread_index`, LLM latency, and DB writes.
- **Security**: keep wallets and OCI config out of source control; use perenvironment profiles.
- **Cost**: batch larger emails offpeak; cache results for unchanged `.msg` object hashes.
---
## 12) Appendix notable functions (by file)
### `process_emails_v2.py`
- `list_files()` list `.msg` in bucket
- `download_file(name)` tempdownload object
- `move_to_processed(name)` move to `processed/`
- `extract_msg_text(path)` read `.msg` headers/body
- `split_refined_email_thread(texto)` / `split_thread_by_headers(texto)` reliable thread splitter
- `chunk_by_size(text)` / `prepare_blocks_for_llm(headers, body)` chunk & pack
- `build_prompt(bloco, headers_topo)` robust JSONonly, domainspecific prompt
- `call_llm_extract(bloco, headers_topo)` OCI GenAI call + JSON parse
- `insert_into_database(item, file_name, thread_index)` DB insert
- `process_emails()` the pipeline entrypoint
### `app_emails.py`
- `search_emails()` main route “/”: filters + timeline using `PROCESSED_EMAILS_REDACTED`
- `get_distinct_status()` populates status dropdown
- `view_email(file_name)` renders original `.msg` body for audit
---
## 13) FAQ
**Can I change the LLM?**
Yes. Edit `MODEL_ID` and `SERVICE_ENDPOINT`. Keep the prompt constraints (JSONonly) for reliable parsing.
**Do I need the redacted view?**
Recommended for sharing the UI. The processor writes raw values; the view masks addresses for privacy.
**Where do processed `.msg` go?**
To `processed/` (prefix configurable by `PROCESSED_PREFIX`).
**How do I handle attachments?**
Extend `extract_msg_text()` to enumerate attachments from `extract_msg` and store metadata in a separate table.
---
## Acknowledgments
- **Author** - Cristiano Hoshikawa (Oracle LAD A-Team Solution Engineer)