| import uuid |
| from datetime import datetime |
|
|
| from ...config.logfire_config import get_logger |
| from ...repositories.knowledge_repository import KnowledgeRepository |
| from ...services.embeddings.embedding_service import create_embedding |
| from ...services.source_management_service import update_source_info |
| from ...utils import get_supabase_client |
| from ...utils.retry_utils import retry_with_backoff |
| from ..shared_constants import AgentUUIDs |
|
|
| logger = get_logger(__name__) |
|
|
|
|
| class BusinessArchiver: |
| def __init__(self, supabase=None, repo=None): |
| self.supabase = supabase or get_supabase_client() |
| self.repo = repo or KnowledgeRepository(self.supabase) |
|
|
| async def archive_sales_pitch(self, company: str, job_title: str, content: str, references: list[str]) -> str: |
| """ |
| Archives a generated sales pitch into the knowledge base. |
| """ |
| try: |
| |
| safe_company = "".join(c for c in company if c.isalnum()).lower() |
| unique_suffix = str(uuid.uuid4())[:8] |
| source_id = f"pitch-{safe_company}-{unique_suffix}" |
|
|
| |
| title = f"Pitch: {company} - {job_title}" |
| summary = f"Auto-generated sales pitch for {job_title} at {company}." |
| word_count = len(content.split()) |
| tags = ["sales_pitch", "outbound", "email"] |
| if references: |
| tags.append("has_references") |
|
|
| metadata = { |
| "knowledge_type": "sales_pitch", |
| "tags": tags, |
| "references": references, |
| "target_company": company, |
| "target_job": job_title, |
| "source_type": "generated", |
| "auto_generated": True, |
| "created_at": datetime.now().isoformat(), |
| } |
|
|
| logger.info(f"Librarian: Archiving pitch | source_id={source_id} | company={company}") |
|
|
| |
| await update_source_info( |
| client=self.supabase, |
| source_id=source_id, |
| summary=summary, |
| word_count=word_count, |
| content=content, |
| knowledge_type="sales_pitch", |
| tags=tags, |
| source_display_name=title, |
| ) |
|
|
| |
| try: |
| embedding_vector = await create_embedding(content) |
| except Exception as e: |
| logger.error(f"Librarian: Failed to generate embedding for pitch {source_id} | error={str(e)}") |
| embedding_vector = None |
|
|
| page_data = { |
| "source_id": source_id, |
| "url": f"generated://pitch/{source_id}", |
| "chunk_number": 0, |
| "content": content, |
| "embedding": embedding_vector, |
| "metadata": {**metadata, "title": title}, |
| } |
|
|
| self.repo.insert_crawled_page(page_data) |
|
|
| |
| self.repo.insert_document_version( |
| document_id=source_id, |
| field_name="sales_pitch", |
| change_summary=f"Archived generated pitch for {company}", |
| content={"source_id": source_id, "company": company, "job": job_title}, |
| created_by=AgentUUIDs.LIBRARIAN, |
| ) |
|
|
| logger.info(f"Librarian: Pitch archived successfully | source_id={source_id}") |
| return source_id |
|
|
| except Exception as e: |
| logger.error(f"Librarian: Failed to archive pitch | error={str(e)}") |
| return "" |
|
|
| async def get_style_constraints(self, category: str = "marketing") -> str: |
| """ |
| Retrieves physical brand voice constraints and style rules from the knowledge base. |
| Fulfills EXP-03 (Creative Resilience) requirements for Phase 4.6.46. |
| """ |
| try: |
| |
| res = ( |
| self.supabase.table("archon_sources") |
| .select("title, content_summary") |
| .ilike("source_id", "style-lesson-%") |
| .limit(5) |
| .execute() |
| ) |
|
|
| rules = [] |
| if res.data: |
| for entry in res.data: |
| rules.append(f"Rule: {entry.get('title')}\nConstraint: {entry.get('content_summary')}") |
|
|
| if not rules: |
| return "No specific brand voice constraints found. Use professional, data-driven tone." |
|
|
| return "\n---\n".join(rules) |
| except Exception as e: |
| logger.warning(f"Librarian: Failed to fetch style constraints: {e}") |
| return "" |
|
|
| async def archive_failure_case( |
| self, content: str, reason: str, company: str, job_title: str, metadata: dict | None = None |
| ) -> str: |
| """ |
| Archives a failed sales lead or rejected content as negative expertise. |
| """ |
| try: |
| unique_id = str(uuid.uuid4())[:8] |
| source_id = f"fail-{company.lower().replace(' ', '-')}-{unique_id}" |
|
|
| tags = ["failure_case", "risk_factor", "lesson_learned"] |
| title = f"Failure Analysis: {company} - {job_title}" |
| summary = f"Loss analysis for {company}. Reason: {reason}" |
|
|
| full_content = ( |
| f"# Failure Analysis Report\n" |
| f"**Entity**: {company} | **Context**: {job_title}\n" |
| f"**Root Cause**: {reason}\n\n" |
| f"## Full Context\n{content}" |
| ) |
|
|
| await update_source_info( |
| client=self.supabase, |
| source_id=source_id, |
| summary=summary, |
| word_count=len(full_content.split()), |
| content=full_content, |
| knowledge_type="failure_analysis", |
| tags=tags, |
| source_display_name=title, |
| ) |
|
|
| embedding_vector = await create_embedding(full_content[:8000]) |
| page_data = { |
| "source_id": source_id, |
| "url": f"analysis://failure/{source_id}", |
| "chunk_number": 0, |
| "content": full_content, |
| "embedding": embedding_vector, |
| "metadata": { |
| "outcome": "failure", |
| "reason": reason, |
| "company": company, |
| "job": job_title, |
| **(metadata or {}), |
| }, |
| } |
| self.repo.insert_crawled_page(page_data) |
|
|
| return source_id |
|
|
| except Exception as e: |
| logger.error(f"Librarian: Failed to archive failure case: {e}") |
| return "" |
|
|
| async def archive_style_critique(self, post_title: str, original_content: str, review_notes: str) -> str: |
| """ |
| Processes manager's review notes to extract reusable style constraints. |
| """ |
| source_id = "" |
| try: |
| from google import genai |
| from google.genai import types |
|
|
| from ...config.model_ssot import SYSTEM_MODELS |
| from ...services.credential_service import credential_service |
|
|
| api_key = await credential_service.get_credential( |
| "GEMINI_API_KEY" |
| ) or await credential_service.get_credential("GOOGLE_API_KEY") |
| client = genai.Client(api_key=api_key) |
|
|
| from src.server.services.prompt_service import prompt_service |
|
|
| default_prompt_template = ( |
| "You are an AI Style Auditor. Analyze the following 'Review Notes' provided by a manager " |
| "regarding a blog post. Extract 1-2 concrete, reusable 'Brand Voice Constraints' or 'Style Rules' " |
| "that should be followed in the future. Avoid fluff.\n\n" |
| "Post Title: {post_title}\n" |
| "Review Notes: {review_notes}\n\n" |
| "Return the rules as a clear Markdown list." |
| ) |
| prompt_template = prompt_service.get_prompt("AI_STYLE_AUDITOR", default=default_prompt_template) |
| extraction_prompt = prompt_template.format(post_title=post_title, review_notes=review_notes) |
|
|
| @retry_with_backoff(max_retries=2) |
| async def _call_gemini(): |
| return await client.aio.models.generate_content( |
| model=SYSTEM_MODELS["DEFAULT_TEXT"], |
| contents=extraction_prompt, |
| config=types.GenerateContentConfig(temperature=0.1), |
| ) |
|
|
| response = await _call_gemini() |
| extracted_rules = response.text |
|
|
| unique_id = str(uuid.uuid4())[:8] |
| source_id = f"style-lesson-{unique_id}" |
| tags = ["brand_voice_constraint", "style_lesson", "bob_feedback"] |
|
|
| summary = f"Style lesson learned from rejection of '{post_title}'" |
| full_lesson = ( |
| f"# Style Lesson: {post_title}\n" |
| f"## Feedback Received\n{review_notes}\n\n" |
| f"## Extracted Constraints\n{extracted_rules}\n" |
| ) |
|
|
| await update_source_info( |
| client=self.supabase, |
| source_id=source_id, |
| summary=summary, |
| word_count=len(full_lesson.split()), |
| content=full_lesson, |
| knowledge_type="brand_voice", |
| tags=tags, |
| source_display_name=f"Style Lesson: {post_title}", |
| ) |
|
|
| embedding_vector = await create_embedding(full_lesson[:8000]) |
| page_data = { |
| "source_id": source_id, |
| "url": f"lesson://style/{source_id}", |
| "chunk_number": 0, |
| "content": full_lesson, |
| "embedding": embedding_vector, |
| "metadata": {"type": "style_constraint", "tags": tags, "post_title": post_title}, |
| } |
| self.repo.insert_crawled_page(page_data) |
|
|
| return source_id |
|
|
| except Exception as e: |
| logger.error(f"Librarian: Failed to archive style critique: {e}") |
| return "" |
|
|