"""Memo generation tool - generate_investment_memo and system pitch."""
import os
from pathlib import Path
from datetime import datetime
from typing import Any, Optional
try:
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
DOCX_AVAILABLE = True
except ImportError:
DOCX_AVAILABLE = False
from fccs_agent.client.fccs_client import FccsClient
_client: Optional[FccsClient] = None
_app_name: Optional[str] = None
def set_client(client: FccsClient):
"""Set the FCCS client instance."""
global _client
_client = client
def set_app_name(app_name: str):
"""Set the application name."""
global _app_name
_app_name = app_name
async def generate_system_pitch() -> dict[str, Any]:
"""Generate a one-pager pitch document about the system's capabilities.
Generate a professional one-page Word document highlighting the FCCS MCP Agent
system capabilities, features, and value proposition.
Returns:
dict: Path to generated document and summary.
"""
if not DOCX_AVAILABLE:
return {
"status": "error",
"error": "python-docx not available. Install with: pip install python-docx"
}
try:
# Create document
doc = Document()
# Set margins for one-pager (narrow margins)
sections = doc.sections
for section in sections:
section.top_margin = Inches(0.4)
section.bottom_margin = Inches(0.4)
section.left_margin = Inches(0.5)
section.right_margin = Inches(0.5)
# Title
title = doc.add_heading('FCCS MCP Agentic Server', 0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
title.runs[0].font.color.rgb = RGBColor(31, 71, 136)
subtitle = doc.add_paragraph('AI-Powered Oracle EPM Cloud Financial Consolidation Assistant')
subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
subtitle_format = subtitle.runs[0].font
subtitle_format.size = Pt(12)
subtitle_format.bold = True
subtitle_format.color.rgb = RGBColor(31, 71, 136)
doc.add_paragraph() # Spacing
# Value Proposition (condensed)
value_text = doc.add_paragraph(
'Transform your financial close process with an intelligent AI assistant that provides '
'seamless access to Oracle EPM Cloud Financial Consolidation and Close (FCCS) through '
'natural language. Built on Google ADK with MCP protocol support.'
)
value_text_format = value_text.runs[0].font
value_text_format.size = Pt(10)
value_text_format.bold = True
value_text.paragraph_format.space_after = Pt(8)
# Key Capabilities (condensed format)
capabilities_heading = doc.add_paragraph()
cap_heading_run = capabilities_heading.add_run('CORE CAPABILITIES')
cap_heading_run.font.bold = True
cap_heading_run.font.size = Pt(11)
cap_heading_run.font.color.rgb = RGBColor(31, 71, 136)
capabilities_heading.paragraph_format.space_after = Pt(4)
capabilities = [
('25+ FCCS Tools', 'Complete Oracle FCCS REST API coverage'),
('Dual Access', 'MCP server (Claude Desktop) + FastAPI web server'),
('Smart Queries', 'Intelligent 14-dimension cube handling'),
('Journal Automation', 'Complete lifecycle: create, approve, post'),
('Consolidation', 'Business rules, metadata validation, intercompany matching'),
('Report Generation', 'Multi-format: PDF, HTML, XLSX, CSV'),
('Bilingual', 'English & Portuguese support'),
('Learning System', 'SQLite feedback tracking & metrics')
]
for cap_title, cap_desc in capabilities:
cap_para = doc.add_paragraph()
cap_run = cap_para.add_run(f'• {cap_title}: ')
cap_run.font.bold = True
cap_run.font.size = Pt(9)
cap_run.font.color.rgb = RGBColor(31, 71, 136)
desc_run = cap_para.add_run(cap_desc)
desc_run.font.size = Pt(9)
cap_para.paragraph_format.space_after = Pt(3)
doc.add_paragraph() # Spacing
# Technical & Use Cases (combined, condensed)
tech_heading = doc.add_paragraph()
tech_heading_run = tech_heading.add_run('TECHNICAL HIGHLIGHTS & USE CASES')
tech_heading_run.font.bold = True
tech_heading_run.font.size = Pt(11)
tech_heading_run.font.color.rgb = RGBColor(31, 71, 136)
tech_heading.paragraph_format.space_after = Pt(4)
combined_points = [
'Google ADK + MCP protocol | Docker & Cloud Run ready | Mock mode for testing',
'Query financial data | Automate journal workflows | Run consolidation rules',
'Generate reports on-demand | Explore hierarchies | Monitor job execution'
]
for point in combined_points:
point_para = doc.add_paragraph(point, style='List Bullet')
point_para.runs[0].font.size = Pt(9)
point_para.paragraph_format.space_after = Pt(2)
# Footer
doc.add_paragraph() # Spacing
footer = doc.add_paragraph()
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
footer_run = footer.add_run(
f'FCCS MCP Agentic Server | Generated: {datetime.now().strftime("%B %d, %Y")} | '
'Oracle EPM Cloud Financial Consolidation and Close'
)
footer_run.font.size = Pt(8)
footer_run.font.color.rgb = RGBColor(128, 128, 128)
footer_run.font.italic = True
# Save document
filename = f"FCCS_System_Pitch_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
filepath = Path(filename).absolute()
doc.save(filename)
return {
"status": "success",
"data": {
"file_path": str(filepath),
"filename": filename,
"message": "One-pager system pitch document generated successfully",
"note": "Document highlights system capabilities, features, and value proposition"
}
}
except Exception as e:
return {
"status": "error",
"error": f"Failed to generate pitch document: {str(e)}"
}
async def generate_investment_memo(ticker: str) -> dict[str, Any]:
"""Generate a 2-page investment memo (Word doc) with financial analysis.
Gerar memorando de investimento (Word) com analise financeira.
Args:
ticker: Company ticker symbol (e.g., 'TECH').
Returns:
dict: Path to generated memo file and summary.
"""
if not DOCX_AVAILABLE:
return {
"status": "error",
"error": "python-docx not available. Install with: pip install python-docx"
}
try:
# Try to get real FCCS data, otherwise generate realistic mock data
financial_data = await _get_financial_data(ticker)
# Perform financial analysis
analysis = _analyze_financials(financial_data, ticker)
# Generate Word document
doc = Document()
# Set margins for 2-page memo
sections = doc.sections
for section in sections:
section.top_margin = Inches(0.75)
section.bottom_margin = Inches(0.75)
section.left_margin = Inches(1.0)
section.right_margin = Inches(1.0)
# PAGE 1: Executive Summary & Key Metrics
_add_memo_header(doc, ticker, financial_data.get("company_name", f"{ticker} Corp"))
# Executive Summary
exec_summary = doc.add_heading('Executive Summary', 1)
exec_summary.runs[0].font.color.rgb = RGBColor(31, 71, 136)
exec_summary.paragraph_format.space_after = Pt(6)
summary_text = (
f"{financial_data.get('company_name', ticker)} demonstrates {'strong' if analysis['recommendation'] == 'BUY' else 'moderate' if analysis['recommendation'] == 'HOLD' else 'weak'} "
f"financial performance with revenue of ${financial_data['revenue']:,.0f}M and net income of ${financial_data['net_income']:,.0f}M. "
f"The company shows a {'positive' if analysis['revenue_growth'] > 0 else 'negative'} revenue growth trend "
f"of {abs(analysis['revenue_growth']):.1f}% and maintains a {'healthy' if analysis['profit_margin'] > 0.15 else 'moderate' if analysis['profit_margin'] > 0.05 else 'narrow'} "
f"profit margin of {analysis['profit_margin']:.1%}. "
f"Based on our analysis, we recommend a {analysis['recommendation']} position."
)
doc.add_paragraph(summary_text)
doc.add_paragraph() # Spacing
# Key Financial Metrics
metrics_heading = doc.add_heading('Key Financial Metrics', 1)
metrics_heading.runs[0].font.color.rgb = RGBColor(31, 71, 136)
metrics_heading.paragraph_format.space_after = Pt(6)
metrics = [
("Revenue (TTM)", f"${financial_data['revenue']:,.0f}M"),
("Net Income (TTM)", f"${financial_data['net_income']:,.0f}M"),
("Profit Margin", f"{analysis['profit_margin']:.1%}"),
("Revenue Growth (YoY)", f"{analysis['revenue_growth']:+.1f}%"),
("ROE", f"{analysis['roe']:.1%}"),
("Current Ratio", f"{analysis['current_ratio']:.2f}"),
("Debt-to-Equity", f"{analysis['debt_to_equity']:.2f}"),
]
for metric_name, metric_value in metrics:
metric_para = doc.add_paragraph()
metric_run = metric_para.add_run(f"{metric_name}: ")
metric_run.font.bold = True
metric_run.font.size = Pt(10)
value_run = metric_para.add_run(metric_value)
value_run.font.size = Pt(10)
metric_para.paragraph_format.space_after = Pt(3)
doc.add_paragraph() # Spacing
# Financial Highlights Table
highlights_heading = doc.add_heading('Financial Highlights', 1)
highlights_heading.runs[0].font.color.rgb = RGBColor(31, 71, 136)
highlights_heading.paragraph_format.space_after = Pt(6)
table = doc.add_table(rows=1, cols=3)
table.style = 'Light Grid Accent 1'
# Header row
header_cells = table.rows[0].cells
header_cells[0].text = 'Metric'
header_cells[1].text = 'Current'
header_cells[2].text = 'Prior Year'
for cell in header_cells:
cell.paragraphs[0].runs[0].font.bold = True
cell.paragraphs[0].runs[0].font.color.rgb = RGBColor(255, 255, 255)
cell.paragraphs[0].runs[0].font.size = Pt(9)
# Data rows
table_data = [
("Revenue", f"${financial_data['revenue']:,.0f}M", f"${financial_data.get('prior_revenue', financial_data['revenue'] * 0.95):,.0f}M"),
("Net Income", f"${financial_data['net_income']:,.0f}M", f"${financial_data.get('prior_net_income', financial_data['net_income'] * 0.92):,.0f}M"),
("Total Assets", f"${financial_data.get('total_assets', financial_data['revenue'] * 2.5):,.0f}M", f"${financial_data.get('prior_total_assets', financial_data.get('total_assets', financial_data['revenue'] * 2.5) * 0.96):,.0f}M"),
("Shareholders' Equity", f"${financial_data.get('equity', financial_data['revenue'] * 1.2):,.0f}M", f"${financial_data.get('prior_equity', financial_data.get('equity', financial_data['revenue'] * 1.2) * 0.94):,.0f}M"),
]
for row_data in table_data:
row_cells = table.add_row().cells
for i, cell_text in enumerate(row_data):
row_cells[i].text = cell_text
row_cells[i].paragraphs[0].runs[0].font.size = Pt(9)
# PAGE 2: Analysis & Recommendation
doc.add_page_break()
# Financial Analysis
analysis_heading = doc.add_heading('Financial Analysis', 1)
analysis_heading.runs[0].font.color.rgb = RGBColor(31, 71, 136)
analysis_heading.paragraph_format.space_after = Pt(6)
# Revenue Analysis
revenue_para = doc.add_paragraph()
revenue_run = revenue_para.add_run("Revenue Performance: ")
revenue_run.font.bold = True
revenue_run.font.size = Pt(10)
revenue_para.add_run(
f"The company reported revenue of ${financial_data['revenue']:,.0f}M, "
f"{'representing strong growth' if analysis['revenue_growth'] > 10 else 'showing moderate growth' if analysis['revenue_growth'] > 0 else 'reflecting a decline'} "
f"of {abs(analysis['revenue_growth']):.1f}% compared to the prior year. "
f"This performance {'exceeds' if analysis['revenue_growth'] > 10 else 'meets' if analysis['revenue_growth'] > 0 else 'falls short of'} "
f"industry expectations."
)
revenue_para.paragraph_format.space_after = Pt(6)
# Profitability Analysis
profit_para = doc.add_paragraph()
profit_run = profit_para.add_run("Profitability: ")
profit_run.font.bold = True
profit_run.font.size = Pt(10)
profit_para.add_run(
f"Net income of ${financial_data['net_income']:,.0f}M results in a profit margin of {analysis['profit_margin']:.1%}, "
f"which is {'above' if analysis['profit_margin'] > 0.15 else 'in line with' if analysis['profit_margin'] > 0.05 else 'below'} "
f"industry standards. The company demonstrates {'strong' if analysis['roe'] > 0.15 else 'adequate' if analysis['roe'] > 0.10 else 'weak'} "
f"return on equity of {analysis['roe']:.1%}, indicating {'efficient' if analysis['roe'] > 0.15 else 'moderate'} capital utilization."
)
profit_para.paragraph_format.space_after = Pt(6)
# Financial Position
position_para = doc.add_paragraph()
position_run = position_para.add_run("Financial Position: ")
position_run.font.bold = True
position_run.font.size = Pt(10)
position_para.add_run(
f"The company maintains a current ratio of {analysis['current_ratio']:.2f}, "
f"indicating {'strong' if analysis['current_ratio'] > 2.0 else 'adequate' if analysis['current_ratio'] > 1.0 else 'limited'} "
f"liquidity. Debt-to-equity ratio of {analysis['debt_to_equity']:.2f} suggests "
f"{'conservative' if analysis['debt_to_equity'] < 0.5 else 'moderate' if analysis['debt_to_equity'] < 1.0 else 'aggressive'} "
f"leverage positioning."
)
position_para.paragraph_format.space_after = Pt(6)
# Investment Recommendation
rec_heading = doc.add_heading('Investment Recommendation', 1)
rec_heading.runs[0].font.color.rgb = RGBColor(31, 71, 136)
rec_heading.paragraph_format.space_after = Pt(6)
recommendation_text = (
f"Based on our comprehensive financial analysis, we recommend a {analysis['recommendation']} position "
f"on {ticker}. "
f"{'The company demonstrates strong fundamentals with' if analysis['recommendation'] == 'BUY' else 'The company shows mixed signals with' if analysis['recommendation'] == 'HOLD' else 'The company faces challenges with'} "
f"{'robust' if analysis['recommendation'] == 'BUY' else 'moderate' if analysis['recommendation'] == 'HOLD' else 'weak'} "
f"financial metrics and {'favorable' if analysis['recommendation'] == 'BUY' else 'neutral' if analysis['recommendation'] == 'HOLD' else 'concerning'} "
f"growth prospects. "
f"{'Key strengths include' if analysis['recommendation'] == 'BUY' else 'Areas of concern include' if analysis['recommendation'] == 'SELL' else 'Notable factors include'} "
f"{'strong profitability, healthy balance sheet, and positive revenue trends' if analysis['recommendation'] == 'BUY' else 'declining margins, high leverage, and negative growth' if analysis['recommendation'] == 'SELL' else 'mixed performance indicators and uncertain outlook'}."
)
doc.add_paragraph(recommendation_text)
doc.add_paragraph() # Spacing
# Risk Factors
risk_heading = doc.add_heading('Key Risk Factors', 1)
risk_heading.runs[0].font.color.rgb = RGBColor(31, 71, 136)
risk_heading.paragraph_format.space_after = Pt(6)
risk_factors = [
"Market volatility and economic uncertainty may impact performance",
"Competitive pressures in the industry could affect market share",
"Regulatory changes may require operational adjustments",
"Currency fluctuations could impact international operations"
]
for risk in risk_factors:
risk_para = doc.add_paragraph(risk, style='List Bullet')
risk_para.runs[0].font.size = Pt(9)
risk_para.paragraph_format.space_after = Pt(2)
# Footer
doc.add_paragraph() # Spacing
footer = doc.add_paragraph()
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
footer_run = footer.add_run(
f'Investment Memo - {ticker} | Generated: {datetime.now().strftime("%B %d, %Y")} | '
'FCCS MCP Agentic Server'
)
footer_run.font.size = Pt(8)
footer_run.font.color.rgb = RGBColor(128, 128, 128)
footer_run.font.italic = True
# Save document
filename = f"Investment_Memo_{ticker}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
filepath = Path(filename).absolute()
doc.save(filename)
return {
"status": "success",
"data": {
"file_path": str(filepath),
"filename": filename,
"ticker": ticker,
"recommendation": analysis['recommendation'],
"message": f"2-page investment memo generated successfully for {ticker}",
"note": f"Document includes financial analysis with {analysis['recommendation']} recommendation"
}
}
except Exception as e:
return {
"status": "error",
"error": f"Failed to generate investment memo: {str(e)}"
}
def _add_memo_header(doc: Document, ticker: str, company_name: str):
"""Add memo header with title and company info."""
title = doc.add_heading(f'Investment Memo: {ticker}', 0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
title.runs[0].font.color.rgb = RGBColor(31, 71, 136)
subtitle = doc.add_paragraph(company_name)
subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
subtitle_format = subtitle.runs[0].font
subtitle_format.size = Pt(12)
subtitle_format.bold = True
subtitle_format.color.rgb = RGBColor(31, 71, 136)
date_para = doc.add_paragraph(datetime.now().strftime("%B %d, %Y"))
date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
date_para.runs[0].font.size = Pt(10)
date_para.runs[0].font.color.rgb = RGBColor(128, 128, 128)
doc.add_paragraph() # Spacing
async def _get_financial_data(ticker: str) -> dict[str, Any]:
"""Get financial data from FCCS or generate realistic mock data."""
# Try to use FCCS client if available
global _client, _app_name
if _client and _app_name:
# Try to get real data from FCCS
try:
# Build grid definition for revenue
revenue_grid = {
"suppressMissingBlocks": True,
"pov": {
"members": [
["FY25"], ["Actual"], ["FCCS_YTD"], ["FCCS_Entity Total"],
["FCCS_Intercompany Top"], ["FCCS_Total Data Source"],
["FCCS_Mvmts_Total"], [f"FCCS_{ticker}"], ["Entity Currency"],
["Total Custom 3"], ["Total Region"], ["Total Venturi Entity"],
["Total Custom 4"]
]
},
"columns": [{"members": [["Dec"]]}],
"rows": [{"members": [["FCCS_Revenue"]]}]
}
net_income_grid = {
"suppressMissingBlocks": True,
"pov": {
"members": [
["FY25"], ["Actual"], ["FCCS_YTD"], ["FCCS_Entity Total"],
["FCCS_Intercompany Top"], ["FCCS_Total Data Source"],
["FCCS_Mvmts_Total"], [f"FCCS_{ticker}"], ["Entity Currency"],
["Total Custom 3"], ["Total Region"], ["Total Venturi Entity"],
["Total Custom 4"]
]
},
"columns": [{"members": [["Dec"]]}],
"rows": [{"members": [["FCCS_Net Income"]]}]
}
revenue_result = await _client.export_data_slice(_app_name, "Consol", revenue_grid)
net_income_result = await _client.export_data_slice(_app_name, "Consol", net_income_grid)
if revenue_result and net_income_result:
rev_rows = revenue_result.get("rows", [])
ni_rows = net_income_result.get("rows", [])
if rev_rows and ni_rows:
rev_data = rev_rows[0].get("data", [0])[0] or 0
ni_data = ni_rows[0].get("data", [0])[0] or 0
if rev_data and ni_data:
return {
"company_name": f"{ticker} Corporation",
"revenue": float(rev_data) / 1_000_000, # Convert to millions
"net_income": float(ni_data) / 1_000_000,
"source": "FCCS"
}
except Exception:
pass # Fall through to mock data
# Generate realistic mock financial data based on ticker
import hashlib
# Use ticker to generate consistent mock data
ticker_hash = int(hashlib.md5(ticker.encode()).hexdigest()[:8], 16)
# Generate realistic financial metrics
base_revenue = 500 + (ticker_hash % 2000) # $500M - $2.5B
revenue = base_revenue
profit_margin = 0.08 + (ticker_hash % 20) / 100 # 8% - 28%
net_income = revenue * profit_margin
return {
"company_name": f"{ticker} Corporation",
"revenue": revenue,
"net_income": net_income,
"total_assets": revenue * (2.0 + (ticker_hash % 10) / 10), # 2x - 3x revenue
"equity": revenue * (1.0 + (ticker_hash % 8) / 10), # 1x - 1.8x revenue
"source": "Mock"
}
def _analyze_financials(data: dict[str, Any], ticker: str) -> dict[str, Any]:
"""Perform financial analysis and generate metrics."""
revenue = data.get("revenue", 0)
net_income = data.get("net_income", 0)
total_assets = data.get("total_assets", revenue * 2.5)
equity = data.get("equity", revenue * 1.2)
# Calculate ratios
profit_margin = net_income / revenue if revenue > 0 else 0
roe = net_income / equity if equity > 0 else 0
# Generate growth (mock)
import hashlib
ticker_hash = int(hashlib.md5(ticker.encode()).hexdigest()[:8], 16)
revenue_growth = -5 + (ticker_hash % 30) # -5% to +25%
# Calculate other metrics
current_assets = total_assets * 0.3 # Assume 30% current
current_liabilities = total_assets * 0.15 # Assume 15% current liabilities
current_ratio = current_assets / current_liabilities if current_liabilities > 0 else 2.0
total_debt = total_assets - equity
debt_to_equity = total_debt / equity if equity > 0 else 0.5
# Determine recommendation
score = 0
if profit_margin > 0.15:
score += 2
elif profit_margin > 0.05:
score += 1
if revenue_growth > 10:
score += 2
elif revenue_growth > 0:
score += 1
if roe > 0.15:
score += 2
elif roe > 0.10:
score += 1
if current_ratio > 1.5 and debt_to_equity < 1.0:
score += 1
if score >= 5:
recommendation = "BUY"
elif score >= 3:
recommendation = "HOLD"
else:
recommendation = "SELL"
return {
"profit_margin": profit_margin,
"revenue_growth": revenue_growth,
"roe": roe,
"current_ratio": current_ratio,
"debt_to_equity": debt_to_equity,
"recommendation": recommendation
}
TOOL_DEFINITIONS = [
{
"name": "generate_system_pitch",
"description": "Generate a one-pager pitch document about the system's capabilities / Gerar documento de apresentacao do sistema",
"inputSchema": {
"type": "object",
"properties": {},
"required": [],
},
},
{
"name": "generate_investment_memo",
"description": "Generate a 2-page investment memo (Word doc) with financial analysis / Gerar memorando de investimento",
"inputSchema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Company ticker symbol (e.g., 'TECH')",
},
},
"required": ["ticker"],
},
},
]