# Project export: MarketGap

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: A simplified market research and gap analysis system powered by Letta's native multi-agent capabilities.
- Devpost: https://devpost.com/software/marketgap
- GitHub: https://github.com/SheteUC/market-gap
- Team: 1 GitHub contributor(s) — Atharv Shete (10 commits)

## Devpost submission (written by the team)

### Inspiration

We've all heard someone say "There has to be a gap in the market for this…"—and then the room goes quiet because nobody can prove it. Traditional market research is pricey, slow, and overkill for hackathons or small teams. We wanted a one-click, weekend-scale tool that tells you: • Which pain points are still unsolved • How big they are • Who actually cares

### What it does

Crawls fresh white-papers from top consulting firms. Market-Analyzer agent ranks the biggest unmet needs. Social-Listener agent pulls Reddit chatter + Google snapshots to hear how real people discuss those gaps. Solution-Generator (demo-mode) proposes first-draft ideas. All data lives in Letta's shared memory blocks so the team—or more agents—can keep iterating. The output: a ranked list of market gaps plus audience insights, perfect for founders, PMs, or hackathon teams who need validation fast.

### How we built it

Backend Node.js + TypeScript on Letta (stateful multi-agent OS) Agents • Orchestrator (manager) • Market-Research (PDF crawler) • Market-Analyzer (TF-IDF + sentiment) • Social-Listener (Reddit API + Google search) • Solution-Generator (ICE-scored ideas) Models GPT-4o-mini for workers, GPT-4.1 for the orchestrator Front-end Next.js dashboard with live workflow tracking Storage Letta shared memory (Postgres under the hood) DevOps Simple web dashboard + direct API testing → full pipeline demo-ready for Letta Cloud

### Challenges we ran into

Learning Letta's manager-worker pattern in one weekend. Wrestling with weird PDF layouts from consulting firms. Reddit & Google rate limits during testing. Balancing "cool ML" vs. shipping something that works. Accomplishments we're proud of • End-to-end workflow now completes via clean web dashboard on a fresh workspace. • Zero custom glue code—everything flows through Letta agents & shared memory. • Clean, testable architecture with live progress tracking. • Already surfaced some surprising, real market opportunities!

### What we learned

• Stateful agents > giant prompt chains. • Tiny, single-purpose workers are easier to debug than one mega-agent. • Shared memory blocks beat brittle REST hand-offs. • A working dashboard beats perfect UI when you need to ship fast.

### What's next

Polish the React dashboard (live charts & WebSocket updates). More data sources: Product Hunt, Twitter/X, patent filings. Re-enable competitor analysis + iterative idea loop for full novelty check. Deploy to Letta Cloud so anyone can paste an industry keyword and get gaps back in minutes. Open-source templates so other builders can fork and extend. Thanks for reading—feedback welcome!

## README (from the GitHub repository)

# MarketGap AI 🚀

A simplified market research and gap analysis system powered by **Letta's native multi-agent capabilities**. MarketGap AI discovers untapped market opportunities by analyzing consulting firm white papers, social signals, and industry trends using a clean, Letta-native architecture.

## 🎯 Why Letta-Native?

This implementation follows **Letta best practices exactly**:

- **🤝 Manager-Worker Pattern**: Single Orchestrator Agent manages all worker agents
- **📝 Shared Memory Blocks**: All agents share state via Letta's native memory system  
- **🛠️ Built-in Tools**: Uses Letta's native multi-agent communication tools
- **🔄 Exact Workflow**: Follows the precise 8-step sequence from workspace rules
- **💾 Persistent State**: All agent state persists automatically in Letta's database

## 🏗️ Simplified Architecture

```mermaid
graph TD
    A[Orchestrator Agent] --> B[Shared Memory Blocks]
    A --> C[Worker Agents Created On-Demand]
    
    B --> D[consulting_groups]
    B --> E[consulting_docs] 
    B --> F[gap_list]
    B --> G[audience_signals]
    B --> H[problem_queue]
    B --> I[user_feedback]
    B --> J[idea_history]
    B --> K[final_ideas]
    
    C --> L[Market Research]
    L --> |white-papers| E
    C --> N[Market Analyzer]
    N --> |gaps| F
    C --> M[Social Listener]
    M --> |signals| G
    C --> O[Solution Generator]
```

### **Shared Memory Blocks (Per Workspace Rules)**
- `consulting_groups` (100 KB) - CSV/JSON of consulting firms
- `consulting_docs` (10 MB) - PDF chunks {tag, text}
- `gap_list` (256 KB) - Market gaps {id, title, severity, summary}
- `audience_signals` (5 MB) - Social signals {platform, author, text, sentiment}
- `problem_queue` (64 KB) - Ordered gaps sent to UI
- `user_feedback` (64 KB) - User feedback {problemId, action, notes}
- `idea_history` (2 MB) - All brainstorming iterations
- `final_ideas` (128 KB) - Approved novel ideas

### **Exact Workflow Sequence**
1. **marketResearch (async)** – crawl PDFs from consulting firms  
2. **marketAnalyzer (wait)** – produce `gap_list`  
3. **socialListener (wait)** – write combined audience & Google insights to `audience_signals`  
4. **solutionGenerator (wait)** – brainstorm first batch of ideas → `final_ideas`  
5. (Competitor check skipped in demo)  
6. Emit `workflow_complete` and show ideas on dashboard

## 🛠️ Setup & Installation

### Prerequisites
- **Node.js 16+** and **npm**
- **Letta Cloud Account** ([Sign up here](https://app.letta.com))
- **Letta API Key** ([Get one here](https://app.letta.com/api-keys))

### Installation
```bash
# Clone the repository
git clone <your-repo-url>
cd market-gap

# Install dependencies
npm install

# Set up environment variables
cp .env.example .env.local
# Edit .env.local and add your LETTA_API_KEY
```

### Environment Configuration
Create a `.env.local` file:
```bash
# Required: Letta Configuration
LETTA_API_KEY=your_letta_cloud_api_key_here
LETTA_BASE_URL=https://api.letta.com
```

## 🚀 Quick Start

### Option 1: Web Interface
```bash
npm run dev
# Navigate to http://localhost:3000/workflow
```

### Option 2: API Direct
```bash
# Start workflow
curl -X POST http://localhost:3000/api/letta-workflow \
  -H "Content-Type: application/json" \
  -d '{"action": "start", "industry": "FinTech"}'

# Check status
curl http://localhost:3000/api/letta-workflow?action=status
```

## 📁 Project Structure

```
market-gap/
├── src/
│   ├── agents/
│   │   ├── orchestrator/            # Manager agent
│   │   ├── marketResearch/          # Worker agents
│   │   ├── marketAnalyzer/
│   │   ├── socialListener/
│   │   ├── solutionGenerator/
│   │   └── competitorResearch/
│   └── simple-orchestrator.ts    # Main Letta-native orchestrator
├── components/                  # React UI components
├── context/                     # React context
└── types/                       # TypeScript types
├── app/
│   ├── api/
│   │   └── letta-workflow/         # Single API endpoint
│   ├── workflow/                   # Dashboard page
│   └── [other pages]/              # Additional UI pages
├── README.md                        # This file
└── SIMPLIFIED_APPROACH.md          # Detailed documentation
```

## 🧪 Testing

You can validate everything is wired up in two ways:

1. **Web UI** – Run `npm run dev` and watch the workflow update live at `http://localhost:3000/workflow`.
2. **Direct API** – Use the cURL commands in the Quick-Start section to start a workflow and poll for status.

_No extra scripts are required._

## 🔧 How It Works

### **Single Orchestrator Agent**
- Creates and manages all worker agents using Letta's built-in tools
- Uses `send_message_to_agent_async` for non-blocking tasks
- Uses `send_message_to_agent_and_wait_for_reply` for blocking steps
- Maintains workflow state in shared memory blocks

### **Shared Memory System**
- All agents access the same memory blocks
- Memory persists automatically across sessions
- Follows exact memory block structure from workspace rules
- 10 MB block cap with automatic archival

### **Built-in Letta Tools**
- `web_search` – For research tasks
- `run_code` – For data processing
- Native multi-agent communication tools
- No custom tools needed

## 📊 Benefits of Simplified Approach

| Before (Complex) | After (Simplified) |
|------------------|-------------------|
| 7 agent classes + custom tools | 1 orchestrator using Letta's built-ins |
| Multiple API routes | 1 API endpoint |
| Complex agent manager | Native Letta multi-agent system |
| Custom memory system | Shared memory blocks |
| 500+ lines of config | Environment variables only |

## 📚 Documentation

- **[SIMPLIFIED_APPROACH.md](./SIMPLIFIED_APPROACH.md)** – Detailed technical documentation
- **[Letta Multi-Agent Systems](https://docs.letta.com/guides/agents/multi-agent)** – Official Letta docs
- **[Multi-Agent Shared Memory](https://docs.letta.com/guides/agents/multi-agent-shared-memory)** – Shared memory guide

## ✅ Key Features

- ✅ **Letta-Native**: Uses built-in multi-agent capabilities
- ✅ **Shared Memory**: Proper shared memory blocks following workspace rules
- ✅ **Manager-Worker**: Single orchestrator manages worker agents  
- ✅ **Exact Workflow**: Follows 8-step sequence precisely
- ✅ **Dashboard & API Testing**: No extra scripts required
- ✅ **Clean Architecture**: No unnecessary complexity
- ✅ **Proper Documentation**: Clear, comprehensive guides

## 🎯 Next Steps

1. Open http://localhost:3000/workflow and start a new workflow.
2. Check shared memory blocks are updating correctly via the dashboard or API.
3. Extend with additional worker agents as needed.
4. Deploy to production using Letta Cloud.

## ⚠️ Status: Backend Stable · Front-End WIP

The multi-agent **backend** (Orchestrator → Market-Research → Market-Analyzer → Social-Listener) is fully functional and passes all automated tests.  
However, the **Next.js front-end is still under heavy development** – navigation works, but many pages show placeholder data and WebSocket updates are stubbed. Use the dashboard or the API endpoints to exercise the workflow until the UI is finished.

## 🔄 Idea Loop (Hackathon Demo)

For demo speed we run **one** Solution-Generator pass and stop. Novelty & competitor checks can be re-enabled after the event.

---

**Built with Letta's stateful agent framework - the future of AI applications.**

## Detected evidence (automated analysis)

Indexed codebase: 32 recognized source files, 114 KB.
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- Vercel AI SDK (technology) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (40 of 40)

```
.bolt/config.json
.bolt/ignore
.bolt/prompt
.eslintrc.json
.gitignore
app/api/letta-workflow/route.ts
app/client-layout.tsx
app/globals.css
app/layout.tsx
app/page.tsx
app/workflow/page.tsx
next.config.js
package.json
README.md
src/agents/competitorResearch/index.ts
src/agents/marketAnalyzer/index.ts
src/agents/marketResearch/index.ts
src/agents/orchestrator/index.ts
src/agents/socialListener/index.ts
src/agents/solutionGenerator/index.ts
src/components/AudienceInsights.tsx
src/components/CategoriesPicker.tsx
src/components/CompetitorTable.tsx
src/components/ConsultingGroupSelector.tsx
src/components/DocsCrawlerProgress.tsx
src/components/ExportSummary.tsx
src/components/GapTable.tsx
src/components/HackathonForm.tsx
src/components/IndustrySelector.tsx
src/components/ProblemPicker.tsx
src/components/SolutionWorkbench.tsx
src/components/StackRecommender.tsx
src/components/TopNav.tsx
src/components/WebsocketStatus.tsx
src/constants.ts
src/context/WebsocketContext.tsx
src/types/index.ts
src/utils/api.ts
tsconfig.json
tsconfig.node.json
```

### Dependencies

- package.json: @ant-design/colors@^7.1.0, @ant-design/cssinjs@^1.21.1, @ant-design/icons@^5.4.0, @ant-design/nextjs-registry@^1.0.2, @letta-ai/letta-client@^0.1.132, @letta-ai/vercel-ai-sdk-provider@^0.0.8, @types/node@20.6.2, @types/node-fetch@^2.6.12, @types/react@18.2.22, @types/react-dom@18.2.7, @typescript-eslint/eslint-plugin@^6.21.0, @typescript-eslint/parser@^6.21.0, ai@^4.3.16, antd@^5.21.4, dayjs@^1.11.13, dotenv@^16.5.0, eslint@^8.57.0, eslint-config-airbnb@^19.0.4, eslint-config-airbnb-typescript@^17.1.0, eslint-config-next@14.2.30, eslint-plugin-import@^2.30.0, eslint-plugin-jsx-a11y@^6.10.0, eslint-plugin-react@^7.35.2, eslint-plugin-react-hooks@^4.6.2, husky@^9.1.6, next@14.2.30, node-fetch@^2.7.0, pdf-parse@^1.1.1, react@18.2.0, react-dom@18.2.0, ts-node@^10.9.2, typescript@5.2.2, zustand@^4.5.5

### Recent commits (newest first)

- updated readme
- minor changes
- readme updated
- solution agent and comp analysis agent added
- market analyzer and social listener added
- orchestrator and market research agent added
- feat: improved user flow - removed consulting selection step, direct industry to comprehensive research
- minor changes
- UI changes
- Start repository

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "marketgap-ai",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "typecheck": "tsc --noEmit",
    "prepare": "husky install",
    "test:simple": "npx tsx src/scripts/test-simple.ts",
    "test:letta": "npx tsx src/scripts/test-letta-agents.ts"
  },
  "dependencies": {
    "@ant-design/colors": "^7.1.0",
    "@ant-design/cssinjs": "^1.21.1",
    "@ant-design/icons": "^5.4.0",
    "@ant-design/nextjs-registry": "^1.0.2",
    "@letta-ai/letta-client": "^0.1.132",
    "@letta-ai/vercel-ai-sdk-provider": "^0.0.8",
    "@types/node": "20.6.2",
    "@types/react": "18.2.22",
    "@types/react-dom": "18.2.7",
    "ai": "^4.3.16",
    "antd": "^5.21.4",
    "dayjs": "^1.11.13",
    "dotenv": "^16.5.0",
    "next": "14.2.30",
    "node-fetch": "^2.7.0",
    "pdf-parse": "^1.1.1",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "typescript": "5.2.2",
    "zustand": "^4.5.5"
  },
  "devDependencies": {
    "@types/node-fetch": "^2.6.12",
    "@typescript-eslint/eslint-plugin": "^6.21.0",
    "@typescript-eslint/parser": "^6.21.0",
    "eslint": "^8.57.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-config-airbnb-typescript": "^17.1.0",
    "eslint-config-next": "14.2.30",
    "eslint-plugin-import": "^2.30.0",
    "eslint-plugin-jsx-a11y": "^6.10.0",
    "eslint-plugin-react": "^7.35.2",
    "eslint-plugin-react-hooks": "^4.6.2",
    "husky": "^9.1.6",
    "ts-node": "^10.9.2"
  }
}

```

### app/page.tsx

```typescript
'use client';

import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Spin } from 'antd';

export default function Home() {
  const router = useRouter();

  useEffect(() => {
    router.push('/industry');
  }, [router]);

  return (
    <div style={{ 
      display: 'flex', 
      justifyContent: 'center', 
      alignItems: 'center', 
      height: '50vh' 
    }}>
      <Spin size="large" />
    </div>
  );
}
```

### app/layout.tsx

```typescript
import { AntdRegistry } from '@ant-design/nextjs-registry';
import type { Metadata } from 'next';
import { ClientLayout } from './client-layout';

export const metadata: Metadata = {
  title: 'MarketGap AI',
  description: 'AI-powered market gap analysis and business strategy platform',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <AntdRegistry>
          <ClientLayout>{children}</ClientLayout>
        </AntdRegistry>
      </body>
    </html>
  );
}
```

### app/client-layout.tsx

```typescript
'use client';

import { ConfigProvider, Layout, Menu, Progress, Badge, Typography, Avatar } from 'antd';
import { useRouter, usePathname } from 'next/navigation';
import { useState, useEffect } from 'react';
import { WebsocketProvider } from '@/context/WebsocketContext';
import { 
  BuildOutlined, 
  TeamOutlined, 
  SearchOutlined,
  BarChartOutlined,
  UsergroupAddOutlined,
  BulbOutlined,
  ExperimentOutlined,
  DiffOutlined,
  TrophyOutlined,
  ApiOutlined,
  FileTextOutlined,
  RobotOutlined
} from '@ant-design/icons';

const { Sider, Content } = Layout;
const { Title } = Typography;

const theme = {
  token: {
    colorPrimary: '#6366f1',
    colorSuccess: '#10b981',
    colorError: '#ef4444',
    borderRadius: 8,
    fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
  },
  components: {
    Layout: {
      siderBg: '#1e293b',
    },
    Menu: {
      darkBg: '#1e293b',
      darkItemBg: 'transparent',
      darkItemSelectedBg: '#334155',
      darkItemHoverBg: '#334155',
    },
  },
};

const menuItems = [
  { key: '/industry', label: 'Industry', icon: <BuildOutlined /> },
  { key: '/research', label: 'Research', icon: <SearchOutlined /> },
  { key: '/gaps', label: 'Gaps', icon: <BarChartOutlined /> },
  { key: '/audience', label: 'Audience', icon: <UsergroupAddOutlined /> },
  { key: '/problems', label: 'Problems', icon: <BulbOutlined /> },
  { key: '/solutions', label: 'Solutions', icon: <ExperimentOutlined /> },
  { key: '/competitors', label: 'Competitors', icon: <DiffOutlined /> },
  { key: '/hackathon', label: 'Hackathon', icon: <TrophyOutlined /> },
  { key: '/tech-stack', label: 'Tech Stack', icon: <ApiOutlined /> },
  { key: '/summary', label: 'Summary', icon: <FileTextOutlined /> },
];

export function ClientLayout({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  const pathname = usePathname();
  const [collapsed, setCollapsed] = useState(false);
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const currentIndex = menuItems.findIndex(item => item.key === pathname);
    if (currentIndex >= 0) {
      setProgress(((currentIndex + 1) / menuItems.length) * 100);
    }
  }, [pathname]);

  const handleMenuClick = ({ key }: { key: string }) => {
    router.push(key);
  };

  return (
    <ConfigProvider theme={theme}>
      <WebsocketProvider>
        <Layout style={{ minHeight: '100vh' }}>
          <Sider 
            collapsible 
            collapsed={collapsed} 
            onCollapse={setCollapsed}
            theme="dark"
            width={240}
            style={{ boxShadow: '2px 0 8px rgba(0, 0, 0, 0.1)' }}
          >
            {/* Logo */}
            <div style={{ 
              padding: '20px', 
              borderBottom: '1px solid #334155',
              display: 'flex', 
              alignItems: 'center', 
              gap: '12px' 
            }}>
              <Avatar 
                size="default" 
                style={{ backgroundColor: '#6366f1' }}
                icon={<RobotOutlined />}
              />
              {!collapsed && (
                <Title level={4} style={{ margin: 0, color: 'white' }}>
                  MarketGap AI
                </Title>
              )}
            </div>

            {/* Progress */}
            {!collapsed && (
              <div style={{ padding: '16px 20px' }}>
                <Progress 
                  percent={Math.round(progress)} 
                  showInfo={false} 
                  strokeColor="#6366f1"
                  size="small"
                />
              </div>
            )}

            {/* Menu */}
            <Menu
              mode="inline"
              selectedKeys={[pathname]}
              onClick={handleMenuClick}
              theme="dark"
              style={{ borderRight: 'none' }}
              items={menuItems.map(item => ({
                key: item.key,
                icon: item.icon,
                label: item.label,
              }))}
            />
          </Sider>

          <div style={{ 
            flex: 1,
            padding: '32px',
            background: '#f8fafc',
            minHeight: '100vh'
          }}>
            {children}
          </div>
        </Layout>
      </WebsocketProvider>
    </ConfigProvider>
  );
} 
```

### src/types/index.ts

```typescript
export interface ConsultingGroup {
  id: string;
  name: string;
  reports: number;
  description: string;
}

export interface MarketGap {
  id: string;
  description: string;
  marketSize: string;
  opportunityScore: number;
  severity: 'critical' | 'high' | 'medium' | 'low';
  category: string;
}

export interface AudienceSignal {
  id: string;
  platform: 'reddit' | 'meetup' | 'producthunt';
  title: string;
  content: string;
  sentiment: 'positive' | 'negative' | 'neutral';
  timestamp: string;
  engagement: number;
}

export interface IdeaIteration {
  id: string;
  title: string;
  description: string;
  features: string[];
  targetMarket: string;
  businessModel: string;
  viabilityScore: number;
  technicalFeasibility: number;
  marketPotential: number;
  competitiveAdvantage: number;
  timestamp: string;
}

export interface Competitor {
  id: string;
  name: string;
  description: string;
  website: string;
  stage: 'Early Stage' | 'Growth' | 'Mature' | 'Enterprise';
  funding: string;
  similarity: number;
  differentiator: string;
}

export interface TechStackRecommendation {
  id: string;
  category: 'frontend' | 'backend' | 'database' | 'cloud' | 'mobile';
  matchScore: number;
  technologies: {
    name: string;
    version: string;
    recommended: boolean;
  }[];
  reasoning: string[];
  resources: {
    name: string;
    url: string;
    type: 'docs' | 'tutorial' | 'course' | 'hands-on';
  }[];
  estimatedLearningTime?: string;
}
```

### app/workflow/page.tsx

```typescript
"use client";

import { useEffect, useState } from 'react';

const industries = [
  'FinTech',
  'Healthcare',
  'EdTech',
  'Retail',
  'Logistics',
  'Energy',
  'Real Estate',
  'Manufacturing',
  'Agriculture',
  'Entertainment',
];

interface BlockInfo {
  value: string;
  size: number;
  lastUpdated: string;
}

interface StatusResponse {
  sharedBlocks: Record<string, BlockInfo>;
}

export default function WorkflowDashboard() {
  const [status, setStatus] = useState<Record<string, BlockInfo>>({});
  const [loading, setLoading] = useState(false);
  const [industry, setIndustry] = useState('FinTech');
  const [started, setStarted] = useState(false);

  const fetchStatus = async () => {
    setLoading(true);
    try {
      const res = await fetch('/api/letta-workflow?action=status');
      const json = await res.json();
      if (json?.status?.sharedBlocks) {
        setStatus(json.status.sharedBlocks as StatusResponse['sharedBlocks']);
      }
      if (json?.status && !started) setStarted(true);
    } catch (err) {
      console.error(err);
    } finally {
      setLoading(false);
    }
  };

  const handleStart = async () => {
    try {
      setLoading(true);
      await fetch('/api/letta-workflow', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'start', industry }),
      });
      setStarted(true);
      fetchStatus();
    } catch (err) {
      console.error(err);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    if (started) {
      fetchStatus();
      const interval = setInterval(fetchStatus, 5000);
      return () => clearInterval(interval);
    }
  }, [started]);

  return (
    <main className="p-6">
      <h1 className="text-3xl font-extrabold mb-6 text-slate-800">MarketGap AI – Live Workflow</h1>

      {!started && (
        <div className="mb-6">
          <label className="mr-2 font-medium">Select Industry:</label>
          <select
            className="border px-2 py-1 mr-4"
            value={industry}
            onChange={(e) => setIndustry(e.target.value)}
          >
            {industries.map((ind) => (
              <option key={ind} value={ind}>
                {ind}
              </option>
            ))}
          </select>
          <button
            onClick={handleStart}
            className="bg-blue-600 text-white px-4 py-1 rounded"
            disabled={loading}
          >
            {loading ? 'Starting…' : 'Start Workflow'}
          </button>
        </div>
      )}
      {loading && <p className="text-sm text-gray-600">Refreshing…</p>}

      {/* Terminal Log */}
      {status['workflow_state'] && (
        <div className="mb-6 border rounded bg-black text-green-400 p-4 font-mono text-xs max-h-64 overflow-y-auto">
          {status['workflow_state'].value || '—'}
        </div>
      )}

      <div className="grid md:grid-cols-2 gap-4 mt-4">
        {Object.entries(status).map(([label, info]) => (
          <div key={label} className="border rounded p-4 shadow-sm bg-white">
            <h2 className="font-semibold mb-2 text-slate-700">{label}</h2>
            <p className="text-xs text-gray-500 mb-1">{(info.size/1024).toFixed(1)} KB</p>
            <p className="text-xs text-gray-500 mb-2">Last Updated: {new Date(info.lastUpdated).toLocaleString()}</p>
            <pre className="whitespace-pre-wrap text-sm max-h-60 overflow-y-auto bg-gray-100 p-2 rounded">
              {info.value || '—'}
            </pre>
          </div>
        ))}
      </div>
    </main>
  );
} 
```

### src/agents/solutionGenerator/index.ts

```typescript
/**
 * Solution Generator Agent – Worker for MarketGap AI
 *
 * Reads: gap_list, audience_signals, user_feedback
 * Writes: idea_history (append every iteration), final_ideas (overwrite with most novel ideas)
 *
 * Loop logic handled by Orchestrator; this worker produces up to 3 ideas per call.
 */

import { LettaClient } from '@letta-ai/letta-client';

export async function createSolutionGeneratorAgent(
  client: LettaClient,
  sharedBlockIds: string[]
): Promise<string> {
  console.log('🧪 Creating Solution Generator Agent…');

  const agent = await client.agents.create({
    name: 'SolutionGeneratorWorker',
    memoryBlocks: [
      {
        label: 'persona',
        value:
          `I am the Solution Generator worker. Input: gap_list + audience_signals (+ user_feedback). Task: brainstorm up to 3 innovative, feasible product ideas addressing the highest-severity gaps. For each idea compute an ICE score (Impact, Confidence, Ease) 1-5 each.

Return JSON array [{id, title, description, ICE}]. Append the raw JSON to idea_history, and write ONLY the ideas that meet ICE ≥ 7 (avg) into final_ideas (overwrite). Respond "done" when finished.`,
      },
      {
        label: 'human',
        value: 'Generate innovative solutions for identified market gaps.',
      },
    ],
    blockIds: sharedBlockIds,
    tools: ['run_code'],
    model: 'openai/gpt-4o-mini',
    embedding: 'openai/text-embedding-3-small',
  });

  console.log(`✅ Solution Generator Agent created: ${agent.id}`);
  return agent.id;
}

export async function runSolutionIteration(
  client: LettaClient,
  agentId: string,
  iteration: number
) {
  const instruction = `Iteration ${iteration}: Generate up to 3 ideas now.`;

  return client.agents.messages.create(
    agentId,
    {
      messages: [{ role: 'user', content: instruction }],
    },
    { timeoutInSeconds: 600 }
  );
} 
```

### src/agents/marketResearch/index.ts

```typescript
/**
 * Market-Research Agent - Worker Agent for MarketGap AI
 * 
 * This agent is created by the Orchestrator and only responds to direct messages.
 * It doesn't manage its own lifecycle - the Orchestrator handles everything.
 */

import { LettaClient } from '@letta-ai/letta-client';

export interface MarketResearchConfig {
  lettaApiKey: string;
  lettaBaseUrl?: string;
}

/**
 * Simple factory function to create a Market Research worker agent
 * This agent will be created by the Orchestrator when needed
 */
export async function createMarketResearchAgent(
  client: LettaClient,
  sharedBlockIds: string[],
  industry: string
): Promise<string> {
  console.log(`📊 Creating Market-Research Worker Agent for industry ${industry}...`);

  try {
    const agent = await client.agents.create({
      name: `MarketResearchWorker-${industry.replace(/[^a-zA-Z0-9_\-]/g, '')}`,
      memoryBlocks: [
        {
          label: 'persona',
          value: `I am a Market-Research worker. I respond only to the Orchestrator. When instructed, I iterate over a list of consulting firms provided in the consulting_groups block and find the latest 2 whitepapers/reports for each firm in the ${industry} industry (years 2023-2025). I extract key findings, pain points, and opportunities, then append structured JSON into the consulting_docs shared memory block using the firm's name as the top-level key. I am free to call web_search and run_code, and I may execute multiple fetch_pdf or web_search calls in parallel where helpful.`,
        },
        {
          label: 'human',
          value: `Researching multiple consulting firms for the ${industry} industry.`,
        }
      ],
      blockIds: sharedBlockIds, // Attach shared memory blocks
      tools: ['web_search', 'run_code'],
      model: 'openai/gpt-4o-mini',
      embedding: 'openai/text-embedding-3-small',
    });

    console.log(`✅ Market-Research Worker Agent created: ${agent.id}`);
    return agent.id;

  } catch (error) {
    console.error('❌ Error creating Market-Research worker:', error);
    throw error;
  }
} 
```

### app/api/letta-workflow/route.ts

```typescript
/**
 * Letta Workflow API Route (replaces /api/simple-workflow)
 */

import { NextRequest, NextResponse } from 'next/server';
import { OrchestratorAgent } from '../../../src/agents/orchestrator';

let orchestratorAgent: OrchestratorAgent | null = null;

export async function POST(request: NextRequest) {
  try {
    const { action, industry } = await request.json();

    if (!process.env.LETTA_API_KEY) {
      return NextResponse.json(
        { error: 'LETTA_API_KEY environment variable is required' },
        { status: 500 }
      );
    }

    // Initialise orchestrator once
    if (!orchestratorAgent) {
      console.log('🎯 Creating new Orchestrator Agent...');
      orchestratorAgent = new OrchestratorAgent({
        lettaApiKey: process.env.LETTA_API_KEY,
        lettaBaseUrl: process.env.LETTA_BASE_URL,
      });
      await orchestratorAgent.initialize();
    }

    switch (action) {
      case 'start':
        console.log(`🚀 Starting workflow for ${industry}`);
        const workflowResult = await orchestratorAgent.executeWorkflow(industry);
        return NextResponse.json({
          success: true,
          message: 'Workflow started successfully',
          result: workflowResult,
          agentId: orchestratorAgent.getAgentId(),
        });

      case 'status':
        console.log('📊 Getting workflow status...');
        const status = await orchestratorAgent.getWorkflowStatus();
        return NextResponse.json({ success: true, status });

      case 'research-progress':
        console.log('📈 Checking research progress...');
        const progress = await orchestratorAgent.checkResearchProgress();
        return NextResponse.json({ success: true, progress });

      default:
        return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
    }
  } catch (error) {
    console.error('❌ API Error:', error);
    return NextResponse.json(
      { error: 'Internal server error', details: (error as Error).message },
      { status: 500 },
    );
  }
}

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    const action = searchParams.get('action');

    if (action === 'status') {
      if (!orchestratorAgent) {
        return NextResponse.json({ success: false, message: 'No active workflow' });
      }
      const status = await orchestratorAgent.getWorkflowStatus();
      return NextResponse.json({ success: true, status, hasOrchestrator: !!orchestratorAgent });
    }
    return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
  } catch (error) {
    console.error('❌ GET API Error:', error);
    return NextResponse.json(
      { error: 'Internal server error', details: error instanceof Error ? error.message : 'Unknown error' },
      { status: 500 },
    );
  }
} 
```

### src/agents/socialListener/index.ts

```typescript
/**
 * Social Listener Agent - Worker Agent for MarketGap AI
 *
 * Purpose: After the Market-Analyzer produces a gap list, this agent gathers
 * audience sentiment (Reddit) and high-level market context (Google search)
 * for each gap. It then stores the combined result in the `audience_signals`
 * shared memory block where the Solution-Generator will consume it.
 *
 * Custom Tools
 * 1. reddit_audience_research(keywords: string): string
 *    ‑ Calls Reddit API (or Pushshift) to fetch recent posts/comments that
 *      mention the provided keywords, summarising demographics & sentiment.
 * 2. google_market_research(query: string): string
 *    ‑ Performs a Google Custom Search (or SerpAPI) on the query and
 *      returns a concise summary of top results describing competitors,
 *      market size, and trends.
 *
 * The agent will iterate through the gaps, run both tools, and write an
 * array of objects to the audience_signals block:
 *   [{ gapId, redditInsights, googleInsights }]
 */

import { LettaClient } from '@letta-ai/letta-client';

/**
 * Create the Social Listener worker agent.
 */
export async function createSocialListenerAgent(
  client: LettaClient,
  sharedBlockIds: string[]
): Promise<string> {
  console.log('📡 Creating Social Listener Agent…');

  const agent = await client.agents.create({
    name: 'SocialListenerWorker',
    memoryBlocks: [
      {
        label: 'persona',
        value:
          `I am the Social Listener worker. For each market gap in gap_list I will:
1. Use the built-in run_code tool to call Reddit's API (or Pushshift) and gather recent posts/comments for the gap keywords. Summarise sentiment and audience demographics.
2. Use the built-in web_search tool (Google mode) to collect market-level insights for the same keywords.
3. Merge the two sources into an object {gapId, redditInsights, googleInsights} and append it to the audience_signals shared block.

Respond "done" after all gaps are processed.`,
      },
      {
        label: 'human',
        value: 'Analyse social and market signals for each identified gap.',
      },
    ],
    blockIds: sharedBlockIds,
    tools: ['run_code', 'web_search'],
    model: 'openai/gpt-4o-mini',
    embedding: 'openai/text-embedding-3-small',
  });

  console.log(`✅ Social Listener Agent created: ${agent.id}`);
  return agent.id;
}

/**
 * Trigger the social listening phase.
 */
export async function runSocialListening(
  client: LettaClient,
  socialAgentId: string,
  industry: string
): Promise<any> {
  const instruction = `The gap_list is ready. For each gap, gather audience and market signals using your tools. Industry context: ${industry}.`;

  return client.agents.messages.create(
    socialAgentId,
    {
      messages: [{ role: 'user', content: instruction }],
    },
    { timeoutInSeconds: 600 }
  );
} 
```

[22 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]