SAM Insights Architecture - Implementation Plan
SAM Insights Architecture - Implementation Plan
Created: 2025-01-02
Status: Ready for Implementation
Module: ai_sam_documentation (self-contained)
URL Pattern: /sam_insights/
Executive Summary
Transform ai_sam_documentation into a knowledge publishing system where:
- Local .md files are the source of truth
- Stable URLs never change (even when files reorganize)
- Content auto-syncs on module upgrade
- Website pages render as proper Odoo snippets (not raw HTML)
Architecture Overview
ai_sam_documentation/
│
├── docs/ ← KNOWLEDGE SOURCE
│ ├── _url_registry.json ← Slug → file path mapping
│ ├── knowledge_index.md ← AUTO-GENERATED (don't edit)
│ │
│ ├── 00_sam_skills/ ← Agent capabilities
│ │ ├── cto/
│ │ ├── developer/
│ │ └── architect/
│ │
│ ├── 01_modules/ ← Per-module reference
│ │ ├── ai_brain/
│ │ │ ├── description.md
│ │ │ └── schema.md
│ │ ├── ai_sam/
│ │ └── ai_sam_workflows/
│ │
│ ├── 02_data_flows/ ← How data moves
│ │ ├── apis/
│ │ ├── chat/
│ │ ├── node_creation/
│ │ └── system_prompt_builder/
│ │
│ ├── 03_platform_skins/ ← How skins work
│ │
│ ├── 04_architecture/ ← High-level patterns
│ │
│ └── 05_vision/ ← Strategic direction
│
├── external_registry.json ← Links to files in OTHER modules
│
├── scripts/
│ └── build_knowledge.py ← Runs on upgrade (post_init_hook)
│
├── models/
│ ├── __init__.py
│ ├── insight_page.py ← insight.page model
│ └── insight_category.py ← insight.category model
│
├── controllers/
│ ├── __init__.py
│ └── website_controller.py ← /sam_insights/ routes
│
├── views/
│ ├── website_templates.xml ← Odoo website page templates
│ ├── website_index.xml ← Index page template
│ └── backend_views.xml ← Admin interface (optional)
│
├── security/
│ └── ir.model.access.csv
│
├── static/
│ └── src/
│ └── css/
│ └── insights.css ← Styling for insight pages
│
└── __manifest__.py
Phase 1: Foundation
Task 1.1: Update Manifest
File: __manifest__.py
{
'name': 'SAM AI Documentation & Insights',
'version': '18.0.1.0.0',
'author': 'Anthony Gardiner - Odoo Consulting & Claude AI',
'maintainer': 'Anthony Gardiner <[email protected]>',
'website': 'https://sme.ec',
'category': 'Website/Knowledge',
'license': 'LGPL-3',
'summary': 'SAM AI Knowledge Publishing System - /sam_insights/',
'description': """
SAM AI Documentation & Insights
================================
Knowledge publishing system for SAM AI ecosystem.
Features:
- Markdown files → Odoo website pages
- Stable URLs (files can reorganize, URLs stay same)
- Auto-sync on module upgrade
- Proper Odoo website snippets (theme-aware)
URL Pattern: /sam_insights/<slug>
""",
'depends': [
'base',
'web',
'website',
],
'data': [
'security/ir.model.access.csv',
'views/website_templates.xml',
'views/website_index.xml',
],
'assets': {
'web.assets_frontend': [
'ai_sam_documentation/static/src/css/insights.css',
],
},
'post_init_hook': 'post_init_hook',
'images': ['static/description/icon.png'],
'installable': True,
'application': False,
'auto_install': False,
}
Task 1.2: Create Models
File: models/__init__.py
from . import insight_category
from . import insight_page
File: models/insight_category.py
from odoo import fields, models
class InsightCategory(models.Model):
_name = 'insight.category'
_description = 'Insight Category'
_order = 'sequence, name'
name = fields.Char(required=True)
slug = fields.Char(required=True, index=True)
sequence = fields.Integer(default=10)
description = fields.Text()
page_ids = fields.One2many('insight.page', 'category_id', string='Pages')
page_count = fields.Integer(compute='_compute_page_count')
folder_path = fields.Char(help='Folder path in docs/, e.g., 00_sam_skills')
def _compute_page_count(self):
for rec in self:
rec.page_count = len(rec.page_ids)
File: models/insight_page.py
from odoo import fields, models, api
import re
class InsightPage(models.Model):
_name = 'insight.page'
_description = 'Insight Page'
_order = 'category_id, sequence, name'
name = fields.Char(required=True)
slug = fields.Char(required=True, index=True, unique=True)
category_id = fields.Many2one('insight.category', string='Category', ondelete='set null')
# Content
content_md = fields.Text(string='Markdown Source')
content_html = fields.Html(string='Rendered HTML', sanitize=False)
# Source tracking
source_path = fields.Char(help='Relative path in docs/')
source_type = fields.Selection([
('internal', 'Internal (docs/ folder)'),
('external', 'External (other module)'),
], default='internal')
external_module = fields.Char(help='Module name for external sources')
# Metadata
sequence = fields.Integer(default=10)
last_sync = fields.Datetime()
website_url = fields.Char(compute='_compute_website_url', store=True)
active = fields.Boolean(default=True)
_sql_constraints = [
('slug_unique', 'UNIQUE(slug)', 'Slug must be unique!')
]
@api.depends('slug')
def _compute_website_url(self):
for rec in self:
rec.website_url = f'/sam_insights/{rec.slug}' if rec.slug else False
Task 1.3: Create URL Registry Structure
File: docs/_url_registry.json
{
"_meta": {
"description": "Maps permanent slugs to current file paths. URLs never change, files can move.",
"updated": "2025-01-02"
},
"pages": {
"example-insight": {
"path": "04_architecture/example.md",
"category": "architecture",
"title": "Example Insight",
"created": "2025-01-02"
}
},
"categories": {
"sam-skills": {
"folder": "00_sam_skills",
"sequence": 0,
"description": "Agent capabilities and skills"
},
"modules": {
"folder": "01_modules",
"sequence": 10,
"description": "Per-module reference documentation"
},
"data-flows": {
"folder": "02_data_flows",
"sequence": 20,
"description": "How data moves through the system"
},
"platform-skins": {
"folder": "03_platform_skins",
"sequence": 30,
"description": "How platform skins work"
},
"architecture": {
"folder": "04_architecture",
"sequence": 40,
"description": "High-level patterns and decisions"
},
"vision": {
"folder": "05_vision",
"sequence": 50,
"description": "Strategic direction"
}
}
}
Task 1.4: Create Build Script
File: scripts/__init__.py
from . import build_knowledge
File: scripts/build_knowledge.py
import json
import logging
from pathlib import Path
from datetime import datetime
_logger = logging.getLogger(__name__)
# Optional markdown - fallback to basic conversion if not available
try:
import markdown
MARKDOWN_AVAILABLE = True
except ImportError:
MARKDOWN_AVAILABLE = False
_logger.warning("markdown library not installed. Using basic conversion.")
def get_module_path():
"""Get the ai_sam_documentation module path."""
from odoo.modules.module import get_module_path
return Path(get_module_path('ai_sam_documentation'))
def load_url_registry():
"""Load the URL registry JSON."""
registry_path = get_module_path() / 'docs' / '_url_registry.json'
if registry_path.exists():
with open(registry_path, 'r', encoding='utf-8') as f:
return json.load(f)
return {"_meta": {}, "pages": {}, "categories": {}}
def convert_md_to_html(content):
"""Convert markdown to HTML."""
if MARKDOWN_AVAILABLE:
return markdown.markdown(
content,
extensions=['tables', 'fenced_code', 'toc', 'meta']
)
else:
# Basic conversion
import re
html = content
html = re.sub(r'^### (.+)$', r'<h3>\1</h3>', html, flags=re.MULTILINE)
html = re.sub(r'^## (.+)$', r'<h2>\1</h2>', html, flags=re.MULTILINE)
html = re.sub(r'^# (.+)$', r'<h1>\1</h1>', html, flags=re.MULTILINE)
html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html)
html = re.sub(r'\*(.+?)\*', r'<em>\1</em>', html)
html = re.sub(r'```(.+?)```', r'<pre><code>\1</code></pre>', html, flags=re.DOTALL)
html = re.sub(r'`(.+?)`', r'<code>\1</code>', html)
return f'<div class="markdown-content">{html}</div>'
def sync_categories(env, registry):
"""Sync categories from registry."""
Category = env['insight.category']
for slug, data in registry.get('categories', {}).items():
existing = Category.search([('slug', '=', slug)], limit=1)
vals = {
'name': data.get('description', slug.replace('-', ' ').title()),
'slug': slug,
'sequence': data.get('sequence', 50),
'description': data.get('description', ''),
'folder_path': data.get('folder'),
}
if existing:
existing.write(vals)
else:
Category.create(vals)
_logger.info(f"Synced {len(registry.get('categories', {}))} categories")
def sync_pages(env, registry):
"""Sync pages from registry and docs folder."""
Page = env['insight.page']
Category = env['insight.category']
module_path = get_module_path()
docs_path = module_path / 'docs'
now = datetime.now()
synced = 0
for slug, data in registry.get('pages', {}).items():
file_path = docs_path / data['path']
if not file_path.exists():
_logger.warning(f"File not found for slug '{slug}': {data['path']}")
continue
# Read and convert content
content_md = file_path.read_text(encoding='utf-8')
content_html = convert_md_to_html(content_md)
# Find category
category = Category.search([('slug', '=', data.get('category', ''))], limit=1)
# Create or update page
existing = Page.search([('slug', '=', slug)], limit=1)
vals = {
'name': data.get('title', slug.replace('-', ' ').title()),
'slug': slug,
'category_id': category.id if category else False,
'content_md': content_md,
'content_html': content_html,
'source_path': data['path'],
'source_type': 'internal',
'last_sync': now,
}
if existing:
existing.write(vals)
else:
Page.create(vals)
synced += 1
_logger.info(f"Synced {synced} pages")
def generate_knowledge_index(env):
"""Generate the knowledge_index.md file."""
Category = env['insight.category']
module_path = get_module_path()
index_path = module_path / 'docs' / 'knowledge_index.md'
lines = [
"# SAM AI Knowledge Index",
"",
f"_Auto-generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}_",
"",
"---",
"",
]
categories = Category.search([], order='sequence, name')
for cat in categories:
lines.append(f"## {cat.name}")
lines.append("")
if cat.description:
lines.append(f"_{cat.description}_")
lines.append("")
for page in cat.page_ids:
lines.append(f"- [{page.name}]({page.website_url})")
lines.append("")
index_path.write_text('\n'.join(lines), encoding='utf-8')
_logger.info(f"Generated knowledge_index.md with {len(categories)} categories")
def build_knowledge(env):
"""Main entry point - run on module upgrade."""
_logger.info("Building SAM AI Knowledge...")
registry = load_url_registry()
sync_categories(env, registry)
sync_pages(env, registry)
generate_knowledge_index(env)
_logger.info("SAM AI Knowledge build complete!")
File: __init__.py (update root)
from . import models
from . import controllers
from .scripts.build_knowledge import build_knowledge
def post_init_hook(env):
"""Run knowledge build after module install/upgrade."""
build_knowledge(env)
Task 1.5: Create Website Controller
File: controllers/__init__.py
from . import website_controller
File: controllers/website_controller.py
from odoo import http
from odoo.http import request
class SamInsightsController(http.Controller):
@http.route('/sam_insights', type='http', auth='public', website=True)
def insights_index(self, **kwargs):
"""Index page - list all categories and pages."""
categories = request.env['insight.category'].sudo().search([], order='sequence, name')
return request.render('ai_sam_documentation.insights_index', {
'categories': categories,
})
@http.route('/sam_insights/<string:slug>', type='http', auth='public', website=True)
def insight_page(self, slug, **kwargs):
"""Individual insight page by slug."""
page = request.env['insight.page'].sudo().search([
('slug', '=', slug),
('active', '=', True),
], limit=1)
if not page:
return request.not_found()
# Get sibling pages for navigation
siblings = []
if page.category_id:
siblings = page.category_id.page_ids.filtered(lambda p: p.active)
return request.render('ai_sam_documentation.insight_page', {
'page': page,
'siblings': siblings,
})
@http.route('/sam_insights/category/<string:slug>', type='http', auth='public', website=True)
def insights_category(self, slug, **kwargs):
"""Category listing page."""
category = request.env['insight.category'].sudo().search([
('slug', '=', slug),
], limit=1)
if not category:
return request.not_found()
return request.render('ai_sam_documentation.insights_category', {
'category': category,
'pages': category.page_ids.filtered(lambda p: p.active),
})
Task 1.6: Create Website Templates
File: views/website_index.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Index Page Template -->
<template id="insights_index" name="SAM Insights Index">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<section class="s_title pt48 pb24">
<div class="container">
<h1>SAM AI Knowledge Base</h1>
<p class="lead">Architecture, modules, data flows, and strategic insights.</p>
</div>
</section>
<section class="s_text_block pt24 pb48">
<div class="container">
<div class="row">
<t t-foreach="categories" t-as="category">
<div class="col-lg-4 col-md-6 mb-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">
<a t-attf-href="/sam_insights/category/#{category.slug}">
<t t-esc="category.name"/>
</a>
</h5>
<p class="card-text text-muted" t-if="category.description">
<t t-esc="category.description"/>
</p>
<p class="card-text">
<small class="text-muted">
<t t-esc="category.page_count"/> pages
</small>
</p>
</div>
<div class="card-footer bg-transparent">
<a t-attf-href="/sam_insights/category/#{category.slug}"
class="btn btn-sm btn-outline-primary">
Browse
</a>
</div>
</div>
</div>
</t>
</div>
</div>
</section>
</div>
</t>
</template>
<!-- Category Page Template -->
<template id="insights_category" name="SAM Insights Category">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<section class="s_title pt48 pb24">
<div class="container">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="/sam_insights">Knowledge Base</a>
</li>
<li class="breadcrumb-item active" t-esc="category.name"/>
</ol>
</nav>
<h1 t-esc="category.name"/>
<p class="lead" t-if="category.description" t-esc="category.description"/>
</div>
</section>
<section class="s_text_block pt24 pb48">
<div class="container">
<div class="list-group">
<t t-foreach="pages" t-as="page">
<a t-attf-href="/sam_insights/#{page.slug}"
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span t-esc="page.name"/>
<small class="text-muted" t-if="page.last_sync">
Updated: <t t-esc="page.last_sync" t-options="{'widget': 'date'}"/>
</small>
</a>
</t>
</div>
</div>
</section>
</div>
</t>
</template>
</odoo>
File: views/website_templates.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Individual Insight Page Template -->
<template id="insight_page" name="SAM Insight Page">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<section class="s_title pt48 pb24">
<div class="container">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="/sam_insights">Knowledge Base</a>
</li>
<li class="breadcrumb-item" t-if="page.category_id">
<a t-attf-href="/sam_insights/category/#{page.category_id.slug}">
<t t-esc="page.category_id.name"/>
</a>
</li>
<li class="breadcrumb-item active" t-esc="page.name"/>
</ol>
</nav>
<h1 t-esc="page.name"/>
<p class="text-muted" t-if="page.last_sync">
Last updated: <t t-esc="page.last_sync" t-options="{'widget': 'datetime'}"/>
</p>
</div>
</section>
<section class="s_text_block pt24 pb48">
<div class="container">
<div class="row">
<!-- Main Content -->
<div class="col-lg-9">
<div class="insight-content" t-raw="page.content_html"/>
</div>
<!-- Sidebar Navigation -->
<div class="col-lg-3" t-if="siblings">
<div class="card">
<div class="card-header">
<strong>In This Category</strong>
</div>
<ul class="list-group list-group-flush">
<t t-foreach="siblings" t-as="sibling">
<li class="list-group-item"
t-attf-class="list-group-item #{'active' if sibling.id == page.id else ''}">
<a t-attf-href="/sam_insights/#{sibling.slug}"
t-attf-class="#{'text-white' if sibling.id == page.id else ''}">
<t t-esc="sibling.name"/>
</a>
</li>
</t>
</ul>
</div>
</div>
</div>
</div>
</section>
</div>
</t>
</template>
</odoo>
Task 1.7: Create Security
File: security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_insight_category_public,insight.category.public,model_insight_category,,1,0,0,0
access_insight_page_public,insight.page.public,model_insight_page,,1,0,0,0
access_insight_category_admin,insight.category.admin,model_insight_category,base.group_system,1,1,1,1
access_insight_page_admin,insight.page.admin,model_insight_page,base.group_system,1,1,1,1
Task 1.8: Create CSS
File: static/src/css/insights.css
/* SAM Insights Page Styling */
.insight-content {
line-height: 1.7;
}
.insight-content h1,
.insight-content h2,
.insight-content h3 {
margin-top: 1.5em;
margin-bottom: 0.5em;
}
.insight-content h1 { border-bottom: 2px solid #dee2e6; padding-bottom: 0.3em; }
.insight-content h2 { border-bottom: 1px solid #dee2e6; padding-bottom: 0.2em; }
.insight-content code {
background-color: #f8f9fa;
padding: 0.2em 0.4em;
border-radius: 3px;
font-size: 0.9em;
}
.insight-content pre {
background-color: #2d3748;
color: #e2e8f0;
padding: 1em;
border-radius: 6px;
overflow-x: auto;
}
.insight-content pre code {
background-color: transparent;
padding: 0;
color: inherit;
}
.insight-content table {
width: 100%;
margin: 1em 0;
border-collapse: collapse;
}
.insight-content th,
.insight-content td {
border: 1px solid #dee2e6;
padding: 0.75em;
text-align: left;
}
.insight-content th {
background-color: #f8f9fa;
font-weight: 600;
}
.insight-content blockquote {
border-left: 4px solid #007bff;
padding-left: 1em;
margin-left: 0;
color: #6c757d;
}
Phase 2: Folder Structure Setup
Task 2.1: Create New Folder Structure
Run these commands to set up the docs folder:
cd D:\SAMAI-18-SaaS\github-repos\05-samai-core\ai_sam_documentation
# Create new structure
mkdir -p docs/00_sam_skills
mkdir -p docs/01_modules
mkdir -p docs/02_data_flows/apis
mkdir -p docs/02_data_flows/chat
mkdir -p docs/02_data_flows/node_creation
mkdir -p docs/02_data_flows/system_prompt_builder
mkdir -p docs/03_platform_skins
mkdir -p docs/04_architecture
mkdir -p docs/05_vision
# Create scripts folder
mkdir -p scripts
Task 2.2: Migrate Existing Content
Move existing docs to appropriate folders:
| Current Location | New Location |
|---|---|
docs/architecture/*.md |
docs/04_architecture/ |
docs/canvas/*.md |
docs/04_architecture/ or docs/02_data_flows/ |
docs/development/*.md |
docs/05_vision/ or archive |
docs/current_state.md |
docs/04_architecture/current_state.md |
Note: This is manual curation. Not all old docs need to migrate - some may be obsolete.
Phase 3: First Content
Task 3.1: Create Initial Registry Entries
After moving files, update _url_registry.json with entries like:
{
"pages": {
"current-state": {
"path": "04_architecture/current_state.md",
"category": "architecture",
"title": "SAM AI Current State",
"created": "2025-01-02"
},
"canvas-skeleton": {
"path": "04_architecture/CANVAS_SKELETON_CORE_ARCHITECTURE.md",
"category": "architecture",
"title": "Canvas Skeleton Architecture",
"created": "2025-01-02"
}
}
}
Task 3.2: Test the System
- Install/upgrade module:
-u ai_sam_documentation - Visit:
http://localhost:8069/sam_insights - Verify categories appear
- Click through to individual pages
- Check URLs are stable
Validation Checklist
- [ ] Module installs without errors
- [ ]
/sam_insights/shows index page - [ ] Categories appear with correct names
- [ ] Individual pages render markdown as HTML
- [ ] URLs are slug-based (not file-path based)
- [ ] CSS styling applies correctly
- [ ] Breadcrumbs navigate properly
- [ ]
knowledge_index.mdauto-generates
Future Enhancements (Not in Scope Now)
- Search functionality
- Full-text search across content
- External registry for other module docs
- API endpoint for AI sessions to fetch content
- Version history (currently rely on git)
- Admin interface for editing in Odoo
Developer Handoff
When ready to implement, create a new developer session with:
Implement Phase 1 of the SAM Insights architecture plan.
Plan location: D:\SAMAI-18-SaaS\github-repos\05-samai-core\ai_sam_documentation\plans\2025-01-02_sam_insights_architecture.md
Start with Tasks 1.1 through 1.8.
End of Plan