From 2a568255eef5dacad513eb51c59d6044275288a0 Mon Sep 17 00:00:00 2001 From: 112-stack Date: Fri, 14 Nov 2025 11:02:55 -0800 Subject: [PATCH 1/2] Fix template discovery for Flask app --- .env.example | 4 + .gitignore | 3 + README.md | 70 ++++++++++++++- app.py | 7 ++ app/__init__.py | 32 +++++++ app/chatgpt_client.py | 111 ++++++++++++++++++++++++ app/routes.py | 151 ++++++++++++++++++++++++++++++++ install.bat | 41 +++++++++ install.py | 42 +++++++++ requirements.txt | 3 + static/css/styles.css | 85 ++++++++++++++++++ templates/base.html | 78 +++++++++++++++++ templates/chat.html | 49 +++++++++++ templates/index.html | 48 +++++++++++ templates/predictor.html | 180 +++++++++++++++++++++++++++++++++++++++ templates/results.html | 157 ++++++++++++++++++++++++++++++++++ 16 files changed, 1060 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 app.py create mode 100644 app/__init__.py create mode 100644 app/chatgpt_client.py create mode 100644 app/routes.py create mode 100644 install.bat create mode 100644 install.py create mode 100644 requirements.txt create mode 100644 static/css/styles.css create mode 100644 templates/base.html create mode 100644 templates/chat.html create mode 100644 templates/index.html create mode 100644 templates/predictor.html create mode 100644 templates/results.html diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d1e47fa --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# Copy this file to .env and provide your private OpenAI API key. +OPENAI_API_KEY=replace-with-your-openai-api-key +# Optionally override the Flask secret key for session security +# FLASK_SECRET_KEY=change-me diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6cf5f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +.env diff --git a/README.md b/README.md index 3a08a72..cbf1c16 100644 --- a/README.md +++ b/README.md @@ -1 +1,69 @@ -# beta \ No newline at end of file +# Insight Navigator + +Insight Navigator is a local-first business intelligence assistant that combines +predictive analytics, risk management, and guided advisory conversations through +your ChatGPT connection. + +## Features + +- **Business predictor module** that transforms your metrics into growth outlooks + and actionable recommendations. +- **Risk management insights** with mitigation strategies aligned to your stated + tolerance level. +- **Dynamic SWOT summaries** and professional-grade data visualizations powered by Chart.js. +- **Advisor chat workspace** for continuing strategic discussions and next steps. +- **Responsive, multi-page Flask interface** with polished styling. + +## Prerequisites + +1. Python 3.10+ +2. An OpenAI API key with access to the ChatGPT Responses API. + +## Quick start + +1. Clone the repository and move into the project directory. +2. Run the auto-installer (skips packages that are already installed): + + ```bash + python install.py + ``` + + On Windows you can double-click `install.bat` or run it from a terminal: + + ```bat + install.bat + ``` + +3. Create a `.env` file based on the provided template and add your private key: + + ```bash + cp .env.example .env + # edit .env and set OPENAI_API_KEY without quotes + ``` + +4. Launch the application: + + ```bash + python app.py + ``` + +5. Open your browser at [http://localhost:5000](http://localhost:5000). + +## Usage tips + +- Populate the predictor form with the most recent financial metrics to receive + tailored forecasts, mitigation plans, and SWOT analysis. +- Use the advisor chat page to iterate on board decks, investor updates, or go-to-market plans. +- The application never stores your API key; it reads the value from the + environment at runtime. + +## Development + +- Additional Python dependencies can be installed with `pip install -r requirements.txt`. +- The Flask app entry point is `app.py`; the blueprint routes live in `app/routes.py`. +- Static assets are managed in `static/` and HTML templates in `templates/`. + +## Security note + +Never commit your real API key to version control. Keep `.env` files private and +rotate keys regularly according to your organization’s security policies. diff --git a/app.py b/app.py new file mode 100644 index 0000000..7ea0616 --- /dev/null +++ b/app.py @@ -0,0 +1,7 @@ +from app import create_app + +app = create_app() + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..8d2490f --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,32 @@ +import logging +import os +from pathlib import Path + +from dotenv import load_dotenv +from flask import Flask + +from .routes import bp + + +def create_app() -> Flask: + """Application factory for the business intelligence assistant.""" + env_path = Path('.') / '.env' + if env_path.exists(): + load_dotenv(env_path) + + package_root = Path(__file__).resolve().parent + project_root = package_root.parent + + app = Flask( + __name__, + template_folder=str(project_root / 'templates'), + static_folder=str(project_root / 'static'), + ) + app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'change-me') + + # Configure logging for better observability in production deployments. + logging.basicConfig(level=logging.INFO) + + app.register_blueprint(bp) + + return app diff --git a/app/chatgpt_client.py b/app/chatgpt_client.py new file mode 100644 index 0000000..4ef6246 --- /dev/null +++ b/app/chatgpt_client.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import json +import os +from typing import Any, Dict + +from openai import OpenAI + + +class ChatGPTClient: + """Wrapper around the OpenAI Responses API with structured outputs.""" + + def __init__(self) -> None: + api_key = os.getenv('OPENAI_API_KEY') + if not api_key: + raise RuntimeError( + 'OPENAI_API_KEY is not configured. Set it in your environment or .env file.' + ) + self._client = OpenAI(api_key=api_key) + + def generate_business_insights(self, payload: Dict[str, Any]) -> Dict[str, Any]: + """Generate business outlook, risk profile, and SWOT analysis.""" + system_prompt = ( + "You are a senior strategy consultant creating succinct, data-driven business " + "evaluations. Return valid JSON with keys business_outlook, risk_management, " + "recommendations, and swot (containing strengths, weaknesses, opportunities, threats)." + ) + user_prompt = ( + "Create a forecast, risk review, and SWOT based on the following scenario." + "\nRespond with concise bullet points or short paragraphs." + f"\n\nScenario:\n{json.dumps(payload, indent=2)}" + ) + + response = self._client.responses.create( + model='gpt-4.1-mini', + input=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ], + temperature=0.3, + max_output_tokens=800, + response_format={"type": "json_schema", "json_schema": self._schema()}, + ) + return self._parse_response(response) + + def continue_conversation(self, conversation: str, prompt: str) -> str: + """Send a free-form prompt to ChatGPT for additional assistance.""" + system_prompt = ( + "You are an experienced business analyst. Provide specific, actionable guidance " + "grounded in the provided conversation notes." + ) + user_prompt = ( + "Conversation summary:\n" + f"{conversation}\n\n" + "User request:\n" + f"{prompt}" + ) + response = self._client.responses.create( + model='gpt-4.1-mini', + input=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ], + temperature=0.4, + max_output_tokens=600, + ) + return response.output_text.strip() + + @staticmethod + def _schema() -> Dict[str, Any]: + return { + "name": "business_analysis", + "schema": { + "type": "object", + "properties": { + "business_outlook": {"type": "string"}, + "risk_management": {"type": "string"}, + "recommendations": {"type": "string"}, + "swot": { + "type": "object", + "properties": { + "strengths": {"type": "string"}, + "weaknesses": {"type": "string"}, + "opportunities": {"type": "string"}, + "threats": {"type": "string"}, + }, + "required": [ + "strengths", + "weaknesses", + "opportunities", + "threats", + ], + }, + }, + "required": [ + "business_outlook", + "risk_management", + "recommendations", + "swot", + ], + "additionalProperties": False, + }, + } + + @staticmethod + def _parse_response(response: Any) -> Dict[str, Any]: + try: + return json.loads(response.output[0].content[0].text) + except (KeyError, AttributeError, IndexError, json.JSONDecodeError) as exc: + raise RuntimeError('Unable to parse response from ChatGPT.') from exc + diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..9b83f81 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +import json +from dataclasses import dataclass +from typing import Dict, List + +from flask import Blueprint, flash, render_template, request + +from .chatgpt_client import ChatGPTClient + +bp = Blueprint('main', __name__) + + +@dataclass +class ScenarioData: + business_name: str + industry: str + revenue_current: float + revenue_target: float + expenses_current: float + expenses_target: float + market_share_current: float + market_share_target: float + risk_tolerance: int + strengths: List[str] + weaknesses: List[str] + opportunities: List[str] + threats: List[str] + additional_notes: str + + @classmethod + def from_form(cls, form: Dict[str, str]) -> "ScenarioData": + return cls( + business_name=form.get('business_name', '').strip(), + industry=form.get('industry', '').strip(), + revenue_current=float(form.get('revenue_current', 0) or 0), + revenue_target=float(form.get('revenue_target', 0) or 0), + expenses_current=float(form.get('expenses_current', 0) or 0), + expenses_target=float(form.get('expenses_target', 0) or 0), + market_share_current=float(form.get('market_share_current', 0) or 0), + market_share_target=float(form.get('market_share_target', 0) or 0), + risk_tolerance=int(form.get('risk_tolerance', 3) or 3), + strengths=_split_lines(form.get('strengths', '')), + weaknesses=_split_lines(form.get('weaknesses', '')), + opportunities=_split_lines(form.get('opportunities', '')), + threats=_split_lines(form.get('threats', '')), + additional_notes=form.get('additional_notes', '').strip(), + ) + + def to_payload(self) -> Dict[str, object]: + return { + "business_name": self.business_name, + "industry": self.industry, + "financials": { + "revenue": { + "current": self.revenue_current, + "target": self.revenue_target, + }, + "expenses": { + "current": self.expenses_current, + "target": self.expenses_target, + }, + "market_share": { + "current": self.market_share_current, + "target": self.market_share_target, + }, + }, + "risk_tolerance": self.risk_tolerance, + "swot": { + "strengths": self.strengths, + "weaknesses": self.weaknesses, + "opportunities": self.opportunities, + "threats": self.threats, + }, + "notes": self.additional_notes, + } + + +@bp.route('/') +def index(): + return render_template('index.html') + + +@bp.route('/predictor', methods=['GET', 'POST']) +def predictor(): + context = {"form_values": request.form} + if request.method == 'POST': + try: + scenario = ScenarioData.from_form(request.form) + except ValueError: + flash('Numeric fields must contain valid numbers.', 'danger') + return render_template('predictor.html', **context) + + if not scenario.business_name or not scenario.industry: + flash('Business name and industry are required.', 'danger') + return render_template('predictor.html', **context) + + try: + client = ChatGPTClient() + analysis = client.generate_business_insights(scenario.to_payload()) + except RuntimeError as exc: + flash(str(exc), 'danger') + return render_template('predictor.html', **context) + + metrics = _build_metric_summary(scenario) + return render_template( + 'results.html', + scenario=scenario, + metrics=json.dumps(metrics), + analysis=analysis, + ) + + return render_template('predictor.html', **context) + + +@bp.route('/chat', methods=['GET', 'POST']) +def chat(): + response_text = None + if request.method == 'POST': + conversation = request.form.get('conversation', '') + user_prompt = request.form.get('prompt', '') + if not user_prompt.strip(): + flash('Please enter a prompt to continue the discussion.', 'warning') + else: + try: + client = ChatGPTClient() + response_text = client.continue_conversation(conversation, user_prompt) + except RuntimeError as exc: + flash(str(exc), 'danger') + + return render_template('chat.html', response_text=response_text) + + +def _split_lines(value: str) -> List[str]: + return [line.strip() for line in value.splitlines() if line.strip()] + + +def _build_metric_summary(scenario: ScenarioData) -> Dict[str, List[float]]: + return { + "labels": ["Revenue", "Expenses", "Market Share"], + "current": [ + scenario.revenue_current, + scenario.expenses_current, + scenario.market_share_current, + ], + "target": [ + scenario.revenue_target, + scenario.expenses_target, + scenario.market_share_target, + ], + } diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..46894ac --- /dev/null +++ b/install.bat @@ -0,0 +1,41 @@ +@echo off +setlocal enabledelayedexpansion + +REM Determine which Python executable is available +where python >nul 2>&1 +if %errorlevel%==0 ( + set "PYTHON_EXEC=python" +) else ( + where py >nul 2>&1 + if %errorlevel%==0 ( + set "PYTHON_EXEC=py -3" + ) else ( + echo Python 3 is required but was not found in PATH. + echo Please install Python 3 and try again. + exit /b 1 + ) +) + +for /f "tokens=2 delims= " %%a in ('%PYTHON_EXEC% --version 2^>^&1') do ( + set "PY_VERSION=%%a" + goto version_found +) +:version_found +if not defined PY_VERSION ( + echo Unable to determine Python version. + echo Ensure Python 3 is correctly installed and available on PATH. + exit /b 1 +) + +echo Detected Python !PY_VERSION! +echo Running Insight Navigator dependency check... +%PYTHON_EXEC% install.py +if %errorlevel% neq 0 ( + echo Installer encountered an error. + exit /b %errorlevel% +) + +echo. +echo Installation check complete. +echo Launch the app with: %PYTHON_EXEC% app.py +endlocal diff --git a/install.py b/install.py new file mode 100644 index 0000000..ca8644c --- /dev/null +++ b/install.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Simple dependency installer for Insight Navigator.""" + +from __future__ import annotations + +import importlib +import subprocess +import sys +from pathlib import Path + +REQUIREMENTS = { + "flask": "flask", + "dotenv": "python-dotenv", + "openai": "openai", +} + + +def ensure_packages() -> None: + missing = [] + for module_name, package_name in REQUIREMENTS.items(): + if not _is_installed(module_name): + missing.append(package_name) + if not missing: + print("All dependencies already satisfied.") + return + + print("Installing missing packages:", ", ".join(missing)) + subprocess.check_call([sys.executable, "-m", "pip", "install", *missing]) + + +def _is_installed(module_name: str) -> bool: + try: + importlib.import_module(module_name) + return True + except ModuleNotFoundError: + return False + + +if __name__ == "__main__": + ensure_packages() + requirements_path = Path(__file__).with_name("requirements.txt") + print(f"For reproducible environments run: pip install -r {requirements_path}") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9cb367c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask>=3.0.0 +python-dotenv>=1.0.0 +openai>=1.30.0 diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..b46a73f --- /dev/null +++ b/static/css/styles.css @@ -0,0 +1,85 @@ +:root { + --brand-primary: #0d6efd; + --brand-accent: #20c997; + --brand-dark: #052c65; + --brand-muted: #6c757d; +} + +body { + font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif; + background-color: #f5f6f8; + color: #212529; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +main { + flex: 1 0 auto; +} + +.navbar-brand { + letter-spacing: 0.04em; +} + +.feature-card { + border-radius: 1rem; +} + +.badge.bg-gradient { + background: linear-gradient(135deg, var(--brand-primary), var(--brand-accent)); + letter-spacing: 0.08em; +} + +.alert-stack > .alert { + box-shadow: 0 0.5rem 1.2rem rgba(13, 110, 253, 0.15); +} + +.card { + border-radius: 1rem; +} + +textarea.form-control { + resize: vertical; +} + +.swot-tile { + border-radius: 1rem; + padding: 1.25rem; + background-color: #f8f9fa; + box-shadow: inset 0 0 0 1px rgba(13, 110, 253, 0.05); +} + +.swot-strengths { + border-left: 4px solid var(--brand-accent); +} + +.swot-weaknesses { + border-left: 4px solid #f97316; +} + +.swot-opportunities { + border-left: 4px solid #198754; +} + +.swot-threats { + border-left: 4px solid #dc3545; +} + +.advisor-response { + border-radius: 1rem; + background: #f0f7ff; + border: 1px solid rgba(13, 110, 253, 0.2); + padding: 1.5rem; + line-height: 1.65; +} + +footer { + background: #ffffff; +} + +@media (max-width: 767px) { + .navbar-brand { + font-size: 1.1rem; + } +} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..6c7fab3 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,78 @@ + + + + + + {% block title %}Business Intelligence Assistant{% endblock %} + + + {% block head %}{% endblock %} + + + + +
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} + + {% endfor %} +
+ {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+
+ + + + + {% block scripts %}{% endblock %} + + diff --git a/templates/chat.html b/templates/chat.html new file mode 100644 index 0000000..ceef69a --- /dev/null +++ b/templates/chat.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} +{% block title %}Insight Navigator · Advisor Chat{% endblock %} +{% block content %} +
+
+
+
+

Advisor Chat

+

+ Continue the conversation with your virtual strategist. Paste relevant notes or highlights + from previous analyses and request next steps, pitch outlines, or operational guidance. +

+
+
+ + +
+
+ + +
+
+ +
+
+ {% if response_text %} +
+

Advisor Response

+

{{ response_text }}

+
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..46a6775 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,48 @@ +{% extends 'base.html' %} +{% block title %}Insight Navigator · Home{% endblock %} +{% block content %} +
+
+

Business insight without the noise

+

+ Insight Navigator transforms your raw business metrics into forward-looking forecasts, + risk mitigation strategies, and investor-ready narratives powered by your ChatGPT + connection. +

+ +
+
+
+
+

Why executives rely on Insight Navigator

+
    +
  • + 01 +
    +

    Predictive intelligence

    +

    Scenario-aware growth projections tailored to your inputs.

    +
    +
  • +
  • + 02 +
    +

    Risk under control

    +

    Automated heat-mapping with mitigation plans you can act on.

    +
    +
  • +
  • + 03 +
    +

    Board-ready visuals

    +

    Interactive comparisons and SWOT summaries packaged for stakeholders.

    +
    +
  • +
+
+
+
+
+{% endblock %} diff --git a/templates/predictor.html b/templates/predictor.html new file mode 100644 index 0000000..96e40bc --- /dev/null +++ b/templates/predictor.html @@ -0,0 +1,180 @@ +{% extends 'base.html' %} +{% block title %}Insight Navigator · Predictor{% endblock %} +{% block content %} +
+
+
+
+
+
+

Business Scenario Input

+

Populate the fields below to generate forecasts and risk profiles.

+
+ Predictor module +
+
+
+
+ + +
+
+ + +
+
+ +

Key Metrics

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +

SWOT Inputs

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
+
+{% endblock %} diff --git a/templates/results.html b/templates/results.html new file mode 100644 index 0000000..ad26eba --- /dev/null +++ b/templates/results.html @@ -0,0 +1,157 @@ +{% extends 'base.html' %} +{% block title %}Insight Navigator · Results{% endblock %} +{% block head %} + +{% endblock %} +{% block content %} +
+
+
+
+
+
+

Strategic Outlook

+

{{ scenario.business_name }} · {{ scenario.industry }}

+
+ Analyze another scenario +
+
+

Business Forecast

+

{{ analysis.business_outlook }}

+
+
+

Risk Management

+

{{ analysis.risk_management }}

+
+
+

Key Recommendations

+

{{ analysis.recommendations }}

+
+
+
+ +
+
+

SWOT Intelligence

+
+
+
+

Strengths

+

{{ analysis.swot.strengths }}

+
+
+
+
+

Weaknesses

+

{{ analysis.swot.weaknesses }}

+
+
+
+
+

Opportunities

+

{{ analysis.swot.opportunities }}

+
+
+
+
+

Threats

+

{{ analysis.swot.threats }}

+
+
+
+
+
+
+ +
+
+
+

Financial Trajectory

+ +

+ Current vs. target performance. Use this to align strategy with revenue, expense, and market share goals. +

+
+
+ +
+
+

Scenario Summary

+
+
Risk tolerance
+
{{ scenario.risk_tolerance }}
+
Current revenue
+
${{ '{:,.2f}'.format(scenario.revenue_current) }}
+
Target revenue
+
${{ '{:,.2f}'.format(scenario.revenue_target) }}
+
Current expenses
+
${{ '{:,.2f}'.format(scenario.expenses_current) }}
+
Target expenses
+
${{ '{:,.2f}'.format(scenario.expenses_target) }}
+
Current market share
+
{{ '{:.2f}%'.format(scenario.market_share_current) }}
+
Target market share
+
{{ '{:.2f}%'.format(scenario.market_share_target) }}
+
+
+
+
+
+{% endblock %} +{% block scripts %} + {{ super() }} + +{% endblock %} From 43ca4fe81108b9f0b21b0d754cfaec883f6555e9 Mon Sep 17 00:00:00 2001 From: 112-stack Date: Fri, 14 Nov 2025 11:45:16 -0800 Subject: [PATCH 2/2] Enhance predictor experience with document ingestion --- README.md | 5 +- app/chatgpt_client.py | 57 +++++++-- app/routes.py | 199 ++++++++++++++++++++++++++++++- static/css/styles.css | 252 +++++++++++++++++++++++++++++++++++++++ templates/index.html | 107 +++++++++++------ templates/predictor.html | 117 ++++++++++++++++-- templates/results.html | 103 +++++++++++++++- 7 files changed, 771 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index cbf1c16..ef75da2 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ your ChatGPT connection. - **Risk management insights** with mitigation strategies aligned to your stated tolerance level. - **Dynamic SWOT summaries** and professional-grade data visualizations powered by Chart.js. +- **Document intelligence ingestion** for CSV, JSON, or text files that are parsed and incorporated into the forecast. - **Advisor chat workspace** for continuing strategic discussions and next steps. - **Responsive, multi-page Flask interface** with polished styling. @@ -51,8 +52,8 @@ your ChatGPT connection. ## Usage tips -- Populate the predictor form with the most recent financial metrics to receive - tailored forecasts, mitigation plans, and SWOT analysis. +- Populate the predictor form with the most recent financial metrics and optionally upload + up to three supporting CSV, JSON, or TXT documents to weave qualitative context into forecasts. - Use the advisor chat page to iterate on board decks, investor updates, or go-to-market plans. - The application never stores your API key; it reads the value from the environment at runtime. diff --git a/app/chatgpt_client.py b/app/chatgpt_client.py index 4ef6246..97b38c9 100644 --- a/app/chatgpt_client.py +++ b/app/chatgpt_client.py @@ -28,19 +28,29 @@ def generate_business_insights(self, payload: Dict[str, Any]) -> Dict[str, Any]: user_prompt = ( "Create a forecast, risk review, and SWOT based on the following scenario." "\nRespond with concise bullet points or short paragraphs." + "\nAlways return STRICT JSON matching the schema when providing your answer." f"\n\nScenario:\n{json.dumps(payload, indent=2)}" ) - response = self._client.responses.create( - model='gpt-4.1-mini', - input=[ + request_arguments: Dict[str, Any] = { + 'model': 'gpt-4.1-mini', + 'input': [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], - temperature=0.3, - max_output_tokens=800, - response_format={"type": "json_schema", "json_schema": self._schema()}, - ) + 'temperature': 0.3, + 'max_output_tokens': 900, + } + + try: + response = self._client.responses.create( + **request_arguments, + response_format={"type": "json_schema", "json_schema": self._schema()}, + ) + except TypeError: + # Older versions of the SDK do not yet support ``response_format``. + response = self._client.responses.create(**request_arguments) + return self._parse_response(response) def continue_conversation(self, conversation: str, prompt: str) -> str: @@ -104,8 +114,35 @@ def _schema() -> Dict[str, Any]: @staticmethod def _parse_response(response: Any) -> Dict[str, Any]: + raw_payload = _extract_response_text(response) try: - return json.loads(response.output[0].content[0].text) - except (KeyError, AttributeError, IndexError, json.JSONDecodeError) as exc: - raise RuntimeError('Unable to parse response from ChatGPT.') from exc + return json.loads(raw_payload) + except json.JSONDecodeError: + sanitized = raw_payload.strip() + if sanitized.startswith('```'): + segments = sanitized.split('```') + for segment in segments: + segment = segment.strip() + if segment and not segment.startswith('{') and '{' not in segment: + continue + if segment.startswith('{'): + try: + return json.loads(segment) + except json.JSONDecodeError: + continue + raise RuntimeError('Unable to parse response from ChatGPT.') + + +def _extract_response_text(response: Any) -> str: + """Return the textual payload from an OpenAI Responses result.""" + + for attribute in ('output_text', 'text'): # type: ignore[attr-defined] + value = getattr(response, attribute, None) + if isinstance(value, str) and value.strip(): + return value + + try: + return response.output[0].content[0].text # type: ignore[index] + except (AttributeError, IndexError, KeyError): + raise RuntimeError('Unable to read response payload from ChatGPT.') diff --git a/app/routes.py b/app/routes.py index 9b83f81..85b7237 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,9 +1,15 @@ from __future__ import annotations import json -from dataclasses import dataclass +import csv +import io +import itertools +from dataclasses import dataclass, field +from pathlib import Path from typing import Dict, List +from werkzeug.datastructures import FileStorage + from flask import Blueprint, flash, render_template, request from .chatgpt_client import ChatGPTClient @@ -27,6 +33,7 @@ class ScenarioData: opportunities: List[str] threats: List[str] additional_notes: str + uploaded_documents: List[Dict[str, str]] = field(default_factory=list) @classmethod def from_form(cls, form: Dict[str, str]) -> "ScenarioData": @@ -48,7 +55,7 @@ def from_form(cls, form: Dict[str, str]) -> "ScenarioData": ) def to_payload(self) -> Dict[str, object]: - return { + payload: Dict[str, object] = { "business_name": self.business_name, "industry": self.industry, "financials": { @@ -74,6 +81,9 @@ def to_payload(self) -> Dict[str, object]: }, "notes": self.additional_notes, } + if self.uploaded_documents: + payload["uploaded_documents"] = self.uploaded_documents + return payload @bp.route('/') @@ -95,6 +105,21 @@ def predictor(): flash('Business name and industry are required.', 'danger') return render_template('predictor.html', **context) + uploads: List[Dict[str, str]] = [] + uploaded_files = [file for file in request.files.getlist('data_file') if file and file.filename] + if len(uploaded_files) > 3: + flash('Please upload up to three supporting files at a time.', 'warning') + return render_template('predictor.html', **context) + + for uploaded_file in uploaded_files: + try: + uploads.append(_process_uploaded_file(uploaded_file)) + except ValueError as exc: + flash(str(exc), 'danger') + return render_template('predictor.html', **context) + + scenario.uploaded_documents = uploads + try: client = ChatGPTClient() analysis = client.generate_business_insights(scenario.to_payload()) @@ -103,11 +128,15 @@ def predictor(): return render_template('predictor.html', **context) metrics = _build_metric_summary(scenario) + highlights = _derive_growth_highlights(scenario) + risk_profile = _describe_risk_profile(scenario.risk_tolerance) return render_template( 'results.html', scenario=scenario, metrics=json.dumps(metrics), analysis=analysis, + highlights=highlights, + risk_profile=risk_profile, ) return render_template('predictor.html', **context) @@ -149,3 +178,169 @@ def _build_metric_summary(scenario: ScenarioData) -> Dict[str, List[float]]: scenario.market_share_target, ], } + + +MAX_UPLOAD_BYTES = 1_000_000 # 1 MB cap keeps prompts responsive. +PREVIEW_CHAR_LIMIT = 1_200 + + +def _process_uploaded_file(file: FileStorage) -> Dict[str, str]: + filename = Path(file.filename or '').name + if not filename: + raise ValueError('Select a data file to upload.') + + raw_bytes = file.read() + if not raw_bytes: + raise ValueError('The uploaded file is empty.') + + if len(raw_bytes) > MAX_UPLOAD_BYTES: + raise ValueError('Uploaded file exceeds the 1 MB limit.') + + try: + text = raw_bytes.decode('utf-8') + except UnicodeDecodeError: + text = raw_bytes.decode('utf-8', errors='ignore') + + extension = Path(filename).suffix.lower() + preview = text.strip() + summary = '' + + if extension == '.csv': + preview, summary = _summarize_csv(text) + elif extension == '.json': + preview, summary = _summarize_json(text) + else: + preview, summary = _summarize_text(text) + + return { + "filename": filename, + "format": extension.lstrip('.') or 'text', + "size_kb": f"{len(raw_bytes) / 1024:.1f}", + "summary": summary, + "excerpt": preview[:PREVIEW_CHAR_LIMIT].strip(), + "line_count": str(len([line for line in text.splitlines() if line.strip()])), + } + + +def _summarize_csv(text: str) -> (str, str): + buffer = io.StringIO(text) + reader = csv.reader(buffer) + rows = list(itertools.islice(reader, 0, 11)) + if not rows: + return '', 'CSV file with no rows detected.' + + header = rows[0] + body_rows = rows[1:] + row_count = len(body_rows) + preview_rows = [header] + body_rows[:5] + preview = '\n'.join(', '.join(cell.strip() for cell in row) for row in preview_rows) + summary = ( + f"CSV dataset with {row_count} data rows. Columns: " + + ', '.join(header[:6]) + + ('...' if len(header) > 6 else '') + ) + return preview, summary + + +def _summarize_json(text: str) -> (str, str): + try: + parsed = json.loads(text) + preview = json.dumps(parsed, indent=2) + if isinstance(parsed, dict): + keys = list(parsed.keys()) + summary = f"JSON document with {len(keys)} top-level keys: {', '.join(keys[:6])}" + ( + '...' if len(keys) > 6 else '' + ) + elif isinstance(parsed, list): + summary = f"JSON array with {len(parsed)} entries." + else: + summary = f"JSON value of type {type(parsed).__name__}." + except json.JSONDecodeError: + preview, summary = _summarize_text(text) + else: + preview = preview + return preview, summary + + +def _summarize_text(text: str) -> (str, str): + stripped = text.strip() + words = [word for word in stripped.replace('\n', ' ').split(' ') if word] + summary = f"Narrative brief with approximately {len(words)} words." if words else 'Text document.' + preview = stripped or text + return preview, summary + + +def _derive_growth_highlights(scenario: ScenarioData) -> List[Dict[str, str]]: + highlights: List[Dict[str, str]] = [] + highlights.append(_build_growth_card('Revenue trajectory', scenario.revenue_current, scenario.revenue_target, 'USD')) + highlights.append(_build_growth_card('Expense efficiency', scenario.expenses_current, scenario.expenses_target, 'USD', invert=True)) + highlights.append( + _build_growth_card('Market share momentum', scenario.market_share_current, scenario.market_share_target, 'percentage') + ) + return [item for item in highlights if item] + + +def _build_growth_card(title: str, current: float, target: float, unit: str, invert: bool = False) -> Dict[str, str]: + if current == 0 and target == 0: + return { + "title": title, + "current": '0', + "target": '0', + "direction": 'steady', + "delta": '0', + "percent": '0', + "descriptor": 'No change from current baseline.', + "badge": 'secondary', + } + + delta = target - current + direction = 'increase' if delta >= 0 else 'decrease' + baseline = current if current != 0 else None + percent = (abs(delta) / baseline * 100) if baseline else None + formatted_delta = _format_value(abs(delta), unit) + positive_change = delta >= 0 + if invert: + positive_change = not positive_change + descriptor = ( + f"Targeting a {formatted_delta} {direction} relative to the current baseline." + if percent is None + else f"Targeting a {percent:.1f}% {direction} relative to the current baseline." + ) + badge = 'success' if positive_change else 'warning' + + return { + "title": title, + "current": _format_value(current, unit), + "target": _format_value(target, unit), + "direction": direction, + "delta": formatted_delta, + "percent": f"{percent:.1f}%" if percent is not None else '—', + "descriptor": descriptor, + "badge": badge, + } + + +def _format_value(value: float, unit: str) -> str: + if unit == 'USD': + return f"${value:,.2f}" + if unit == 'percentage': + return f"{value:.2f}%" + return f"{value:,.2f}" + + +def _describe_risk_profile(score: int) -> Dict[str, str]: + categories = { + 1: ('Very conservative', 'Low tolerance for volatility. Prioritize preservation and incremental gains.'), + 2: ('Conservative', 'Measured appetite for risk. Focus on steady growth with guardrails.'), + 3: ('Balanced', 'Comfortable with balanced risk-reward trade-offs to accelerate growth.'), + 4: ('Growth-oriented', 'Open to calculated risks that can unlock competitive advantages.'), + 5: ('Aggressive', 'Seeks outsized returns and is prepared for significant variability.'), + } + normalized = max(1, min(5, score)) + label, description = categories.get(normalized, categories[3]) + return { + "score": str(normalized), + "label": label, + "description": description, + "progress": f"{normalized / 5 * 100:.0f}", + } diff --git a/static/css/styles.css b/static/css/styles.css index b46a73f..8399638 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -18,6 +18,77 @@ main { flex: 1 0 auto; } +.landing-hero { + background: linear-gradient(135deg, rgba(13, 110, 253, 0.08), rgba(32, 201, 151, 0.08)); + border-radius: 1.5rem; + padding: 3rem 3rem 2.5rem; +} + +.kpi-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 1rem; +} + +.kpi-card { + background: #ffffff; + border-radius: 1rem; + padding: 1rem 1.25rem; + box-shadow: 0 0.5rem 1.5rem rgba(13, 110, 253, 0.08); +} + +.insight-showcase { + display: grid; + gap: 1.5rem; +} + +.showcase-card { + background: #ffffff; + border-radius: 1.25rem; + padding: 2rem; +} + +.showcase-steps { + margin: 0; + padding-left: 1.25rem; + display: grid; + gap: 0.75rem; +} + +.showcase-steps li span { + display: block; + color: #495057; +} + +.showcase-footer { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + margin-top: 1.5rem; +} + +.feature-list li { + position: relative; + padding-left: 1.5rem; + margin-bottom: 0.75rem; + color: #495057; +} + +.feature-list li::before { + content: '•'; + position: absolute; + left: 0; + color: var(--brand-primary); + font-weight: 700; +} + +.value-card { + background: #ffffff; + border-radius: 1.25rem; + padding: 1.75rem; + border: 1px solid rgba(13, 110, 253, 0.12); +} + .navbar-brand { letter-spacing: 0.04em; } @@ -39,10 +110,182 @@ main { border-radius: 1rem; } +.insight-progress .progress-chip { + display: flex; + gap: 1rem; + background: #f8f9fa; + border-radius: 1rem; + padding: 1rem 1.25rem; + border: 1px solid rgba(13, 110, 253, 0.12); + height: 100%; +} + +.progress-chip.active { + background: linear-gradient(135deg, rgba(13, 110, 253, 0.18), rgba(32, 201, 151, 0.18)); + border-color: transparent; + color: #052c65; +} + +.chip-index { + font-weight: 700; + font-size: 1.25rem; + background: #ffffff; + border-radius: 50%; + width: 2.75rem; + height: 2.75rem; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0.5rem 1.5rem rgba(13, 110, 253, 0.15); +} + +.chip-title { + margin-bottom: 0.25rem; + font-weight: 600; +} + +.chip-description { + margin: 0; + color: #6c757d; + font-size: 0.9rem; +} + +.upload-panel { + border: 1px dashed rgba(13, 110, 253, 0.4); + border-radius: 1rem; + padding: 1.75rem; + background: rgba(13, 110, 253, 0.04); +} + +.upload-icon { + width: 3rem; + height: 3rem; + background: linear-gradient(135deg, var(--brand-primary), var(--brand-accent)); + color: #ffffff; + font-size: 1.5rem; + box-shadow: 0 0.5rem 1.5rem rgba(13, 110, 253, 0.35); +} + +.upload-glyph { + font-weight: 700; + transform: translateY(-1px); +} + +.file-feedback { + min-width: 200px; + white-space: pre-line; +} + textarea.form-control { resize: vertical; } +.insight-callout { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + background: #ffffff; + border-radius: 1.25rem; + padding: 1.5rem 1.75rem; + border: 1px solid rgba(13, 110, 253, 0.12); + margin-top: 3rem; +} + +.insight-list { + list-style: none; + padding-left: 0; + display: grid; + gap: 0.5rem; +} + +.insight-list li { + position: relative; + padding-left: 1.5rem; + color: #495057; +} + +.insight-list li::before { + content: ''; + position: absolute; + width: 0.6rem; + height: 0.6rem; + border-radius: 50%; + background: var(--brand-primary); + left: 0; + top: 0.35rem; +} + +.insight-stat { + background: #ffffff; + border-radius: 1.25rem; + padding: 1.75rem; + border: 1px solid rgba(13, 110, 253, 0.12); + box-shadow: 0 0.5rem 1.2rem rgba(13, 110, 253, 0.08); +} + +.badge-tag { + letter-spacing: 0.08em; + font-size: 0.7rem; +} + +.badge-tag.badge-success { + background: rgba(32, 201, 151, 0.15); + color: #0f5132; +} + +.badge-tag.badge-warning { + background: rgba(255, 193, 7, 0.2); + color: #664d03; +} + +.badge-tag.badge-secondary { + background: rgba(73, 80, 87, 0.2); + color: #343a40; +} + +.document-grid { + display: grid; + gap: 1.5rem; +} + +.document-chip { + background: #f8f9fa; + border-radius: 1.25rem; + padding: 1.5rem; + border: 1px solid rgba(13, 110, 253, 0.12); +} + +.document-preview { + background: #ffffff; + border-radius: 0.75rem; + border: 1px solid rgba(13, 110, 253, 0.12); + padding: 1rem; + max-height: 12rem; + overflow: auto; + white-space: pre-wrap; + font-family: 'IBM Plex Mono', 'SFMono-Regular', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + font-size: 0.85rem; + color: #495057; +} + +.risk-panel { + background: #f8f9fa; + border-radius: 1rem; + padding: 1.25rem; + border: 1px solid rgba(13, 110, 253, 0.12); +} + +.risk-score { + font-weight: 700; + font-size: 1.25rem; + color: var(--brand-primary); +} + +.risk-progress .progress-bar { + background: linear-gradient(135deg, var(--brand-primary), var(--brand-accent)); +} + .swot-tile { border-radius: 1rem; padding: 1.25rem; @@ -82,4 +325,13 @@ footer { .navbar-brand { font-size: 1.1rem; } + + .landing-hero { + padding: 2rem 1.5rem; + } + + .insight-callout { + flex-direction: column; + align-items: stretch; + } } diff --git a/templates/index.html b/templates/index.html index 46a6775..6e2b5e8 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,48 +1,77 @@ {% extends 'base.html' %} {% block title %}Insight Navigator · Home{% endblock %} {% block content %} -
-
-

Business insight without the noise

-

- Insight Navigator transforms your raw business metrics into forward-looking forecasts, - risk mitigation strategies, and investor-ready narratives powered by your ChatGPT - connection. -

-
- Launch Predictor - Ask an Advisor +
+
+
+

Executive-grade intelligence on demand

+

+ Insight Navigator transforms raw metrics and uploaded evidence into forecasts, risk heatmaps, SWOT insights, and a guided action plan — all powered securely through your ChatGPT connection. +

+ +
+
+

90s

+

to upload data and receive a strategic playbook

+
+
+

4 pillars

+

Forecast · Risk · Recommendations · SWOT

+
+
+

100%

+

private — API keys loaded from your environment

+
+
-
-
-
-
-

Why executives rely on Insight Navigator

-
    -
  • - 01 -
    -

    Predictive intelligence

    -

    Scenario-aware growth projections tailored to your inputs.

    -
    -
  • -
  • - 02 -
    -

    Risk under control

    -

    Automated heat-mapping with mitigation plans you can act on.

    -
    -
  • -
  • - 03 -
    -

    Board-ready visuals

    -

    Interactive comparisons and SWOT summaries packaged for stakeholders.

    -
    -
  • -
+
+
+
+

Workflow snapshot

+
    +
  1. Upload your KPIs and documentation.
  2. +
  3. Insight Navigator benchmarks targets against baselines.
  4. +
  5. AI-powered advisor surfaces risk mitigations and priorities.
  6. +
+ +
+
+

Built for decision makers

+
    +
  • Smart upload parsing for CSV, JSON, and free-form notes.
  • +
  • Radar benchmarking with instant highlight cards.
  • +
  • Advisor chat workspace for follow-up guidance.
  • +
+
+ +
+
+
+

Predict with confidence

+

Blend baseline metrics with scenario targets to build credible revenue, expense, and market share trajectories.

+
+
+
+
+

Control your exposure

+

Automated risk narratives highlight mitigation levers tailored to your declared tolerance.

+
+
+
+
+

Present like a pro

+

SWOT matrices, radar charts, and document intelligence summaries are ready for board decks.

+
+
+
{% endblock %} diff --git a/templates/predictor.html b/templates/predictor.html index 96e40bc..fc0425d 100644 --- a/templates/predictor.html +++ b/templates/predictor.html @@ -2,17 +2,51 @@ {% block title %}Insight Navigator · Predictor{% endblock %} {% block content %}
-
+
-
+
-

Business Scenario Input

-

Populate the fields below to generate forecasts and risk profiles.

+

Scenario architect

+

Feed in performance data, attach support files, and Insight Navigator will deliver projections, risks, and action plans.

+
+
+ Predictor module + AI copiloted +
+
+ +
+
+
+
1
+
+

Profile the business

+

Core identifiers and financial baselines to tailor the analysis.

+
+
+
+
+
+
2
+
+

Attach intelligence

+

Upload CSV, JSON, or notes to ground the model in your data.

+
+
+
+
+
+
3
+
+

Review the playbook

+

Receive forecasts, SWOT, and risk navigation guidance.

+
+
- Predictor module
-
+ +
@@ -22,6 +56,7 @@

Business Scenario Input

name="business_name" value="{{ form_values.get('business_name', '') }}" required + placeholder="Acme Labs" />
@@ -32,12 +67,12 @@

Business Scenario Input

name="industry" value="{{ form_values.get('industry', '') }}" required + placeholder="Healthcare analytics" />
-

Key Metrics

-
+
Key Metrics />
- +
+
+
+
+
+
+ +
+
+ +

Attach up to three CSV, JSON, or TXT files (max 1 MB each). We will surface highlights and incorporate them in the forecast.

+
+ +
No file selected.
+
+ +
+
+
+
+
+

SWOT Inputs

@@ -169,8 +232,12 @@

SWOT Inputs

-
- +
+
+

Quality assurance

+

We validate numeric fields, secure your API key from the environment, and sanitize uploaded text before sending it to ChatGPT.

+
+
@@ -178,3 +245,27 @@

SWOT Inputs

{% endblock %} + +{% block scripts %} + {{ super() }} + +{% endblock %} diff --git a/templates/results.html b/templates/results.html index ad26eba..f5ae76e 100644 --- a/templates/results.html +++ b/templates/results.html @@ -4,6 +4,35 @@ {% endblock %} {% block content %} + {% if highlights %} +
+ {% for card in highlights %} +
+
+
+
+

{{ loop.index }} · Focus Area

+

{{ card.title }}

+
+ {{ card.direction }} +
+
+
Current
+
{{ card.current }}
+
Target
+
{{ card.target }}
+
Delta
+
{{ card.delta }}
+
Shift
+
{{ card.percent }}
+
+

{{ card.descriptor }}

+
+
+ {% endfor %} +
+ {% endif %} +
@@ -21,11 +50,33 @@

Business Forecast

Risk Management

-

{{ analysis.risk_management }}

+ {% set risk_lines = analysis.risk_management.split('\n') %} + {% if risk_lines|length > 1 %} +
    + {% for line in risk_lines %} + {% if line.strip() %} +
  • {{ line }}
  • + {% endif %} + {% endfor %} +
+ {% else %} +

{{ analysis.risk_management }}

+ {% endif %}

Key Recommendations

-

{{ analysis.recommendations }}

+ {% set recommendation_lines = analysis.recommendations.split('\n') %} + {% if recommendation_lines|length > 1 %} +
    + {% for item in recommendation_lines %} + {% if item.strip() %} +
  • {{ item }}
  • + {% endif %} + {% endfor %} +
+ {% else %} +

{{ analysis.recommendations }}

+ {% endif %}
@@ -61,6 +112,39 @@

Threats

+ + {% if scenario.additional_notes %} +
+
+

Scenario Notes

+

{{ scenario.additional_notes }}

+
+
+ {% endif %} + + {% if scenario.uploaded_documents %} +
+
+

Document intelligence

+

Key highlights extracted from your upload. Full excerpts are trimmed for security before being shared with ChatGPT.

+
+ {% for doc in scenario.uploaded_documents %} +
+
+
+

{{ doc.filename }}

+

{{ doc.summary }}

+
+ {{ doc.size_kb }} KB +
+
{{ doc.format|upper }} · {{ doc.line_count }} lines processed
+
{{ doc.excerpt }}
+
+ {% endfor %} +
+
+
+ {% endif %}
@@ -77,7 +161,7 @@

Financial Trajectory

Scenario Summary

-
+
Risk tolerance
{{ scenario.risk_tolerance }}
Current revenue
@@ -93,6 +177,19 @@

Scenario Summary

Target market share
{{ '{:.2f}%'.format(scenario.market_share_target) }}
+
+
+
+

Risk posture

+

{{ risk_profile.label }}

+
+
{{ risk_profile.score }}/5
+
+
+
+
+

{{ risk_profile.description }}

+