# Welcome to JamAI Base

> AI Platform for Secure, Controlled, and Rapid AI Solution Development and Deployment. AI made Insanely Simple!

Plug Large Language Models into Your Projects as Easily as Firebase—**No AI Stack Needed**.

* **Approach:** Introducing a serverless LLM native database featuring Generative Table. Begin your journey in minutes.
* **Advantage:** Bid farewell to the complexities and extra dependencies of platforms like LangChain or LlamaIndex. We simplify your workflow, so you can concentrate on creating amazing applications.
* **Key Feature:** Generative Table technology that transforms static database tables into dynamic, AI-enhanced entities capable of generating and interacting with data autonomously.
* **Application:** Ideal for developers seeking to rapidly deploy AI-powered features in web and mobile applications, enhancing user experience with minimal coding and reduced development time.

{% embed url="<https://www.jamaibase.com/_app/immutable/assets/Jamaibase.C2harmRl.mp4>" %}

{% hint style="info" %}
The name JamAI Base is derived from a blend of JamStack, AI, and Database, encapsulating its core functionality and target architecture.
{% endhint %}

## Core Concepts

#### Generative Table <a href="#generativetable" id="generativetable"></a>

* **Core Concept:** Foundational element of JamAIBase; all tables are types of Generative Tables.
* **Functionality:** Uses LLMs to autonomously generate and populate data based on application needs.

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Action Table</strong></td><td>Facilitates real-time interactions between the application frontend and the LLM backend.</td><td>Ideal for orchestrating complex, multi-step LLM workflows with the simplicity of a spreadsheet.</td><td><a href="/using-the-platform/action-table">Action Table</a></td></tr><tr><td><strong>Chat Table</strong></td><td>Manages real-time multi-turn conversations for applications like chatbots.</td><td>Allows developers to create custom agent (GPTs) that combine instructions and extra knowledge.</td><td><a href="/using-the-platform/chat-table">Chat Table</a></td></tr><tr><td><strong>Knowledge Table</strong></td><td>Manages RAG data and structured metadata.</td><td><p></p><p><br>Serves as an "AI content management system."<br></p></td><td><a href="/using-the-platform/knowledge-table">Knowledge Table</a></td></tr></tbody></table>


# Why Choose JamAI Base?

* **Effortless AI Integration:** JamAI Base eliminates the technical complexities of integrating LLM into your web applications. Its intuitive, table-based interface streamlines development, empowering you to focus on creating unique user experiences.
* **Accelerate Your Workflow:** Experiment with ease and iterate on your designs at lightning speed. JamAI Base's intuitive Chat, Action, and Knowledge Tables give you the flexibility to rapidly incorporate user feedback and adapt to project requirements.
* **Harness the Power of Top LLMs:** Seamlessly connect JamAI Base to cutting-edge Large Language Models like OpenAI GPT-4, Anthropic Claude 3, and Google Gemini. Unlock their advanced capabilities to elevate your applications.
* **Deliver Engaging Real-Time Experiences:** Create dynamic, interactive web interfaces with Action and Chat Tables that provide instant, conversational feedback.
* **Take Control of Your AI:** JamAI Base's unique AI Content Management system empowers you to fine-tune how your AI applications process information. Customize responses through Knowledge Tables, ensuring the AI behaves precisely the way you envision.
* **Built to Grow:** JamAI Base's scalable and serverless architecture effortlessly handle increasing traffic and usage as your application expands.
* **Open Source:** JamAI Base is open-source under the Apache 2.0 License. Scale your project freely and avoid vendor lock-in.

### Differences Between JamAI Base and Other Platforms <a href="#differencesbetweenjamaibaseandotherplatforms" id="differencesbetweenjamaibaseandotherplatforms"></a>

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FqrKQD7yWwgI3uyloFs9g%2Fstack-vs-jamai.png?alt=media&amp;token=910d6e39-5570-4182-8b50-127f1dc5afde" alt=""><figcaption><p><strong>JamAI Base vs. LLM Orchestration Frameworks + VectorDB + Database</strong></p></figcaption></figure>

**JamAI Base vs. LLM Orchestration Frameworks + VectorDB:**

**Integrated AI Capabilities and Seamless LLM Integration:**

* **JamAI Base**: Integrates database functionalities with advanced AI features like direct LLM integration, embedding, reranker, and vector databases. This all-in-one platform facilitates real-time AI interactions and data processing without external tools.
* **Other Platforms**: Require additional tools like LangChain or LlamaIndex for full LLM functionality, adding complexity to the development process.

**Ease of Use and Setup:**

* **JamAI Base**: Features a straightforward, table-based interface that simplifies AI integration, allowing developers to focus on application development rather than complex AI configurations.
* **Other Platforms**: More demanding in terms of setup for advanced AI features; keeping up with RAG best practices in the fast-evolving LLM field can be challenging.

**Comprehensive AI Content Management:**

* **JamAI Base**: Includes Knowledge Tables for sophisticated AI content management, enabling contextually relevant and personalized responses integrated directly with the database system.
* **Other Platforms**: Generally lack specialized AI content management features, which can restrict the ability to provide context-aware AI responses without further customization.&#x20;


# Key Features

JamAI Base offers a suite of innovative features centered around the concept of Generative Tables, which transform traditional database tables into dynamic, intelligent entities powered by Large Language Models (LLMs). Here are the key features of JamAI Base:

## Generative Table

Foundational element of JamAI Base; all tables are types of Generative Tables.

* **Core Feature:** Transforms standard tables into intelligent, LLM-powered entities.
* **Functionality:**
  * Developers can add columns to these tables, which are then autonomously populated with data generated by the LLM.
  * Supports features like Retrieval-Augmented Generation (RAG) to enhance the quality and relevance of generated content.
* **Benefits:**
  * Simplifies the integration of AI capabilities into applications.
  * Enables dynamic data generation and interaction, reducing manual data handling and enhancing user engagement.

## Types of Generative Table

### Action Table <a href="#actiontable" id="actiontable"></a>

* **Purpose:** Streamlines real-time interactions between the application frontend and the LLM backend.
* **Functionality:**
  * Processes user inputs through an integrated API, generating LLM responses directly streamed to the frontend.
  * Automatically records outputs, facilitating seamless data flow and interaction.
  * Enables the creation of complex LLM workflows by chaining multiple LLMs together, allowing for sophisticated processing and response generation.
* **Benefits:**
  * Provides a real-time, responsive AI interaction layer for applications.
  * Eliminates the need for manual backend management of user inputs and outputs.
  * Ideal for orchestrating complex, multi-step LLM workflows with the simplicity of a spreadsheet.

### Chat Table <a href="#chattable" id="chattable"></a>

* **Purpose:** Facilitates the creation and management of intelligent chatbot applications.
* **Functionality:**
  * Manages and stores multi-turn conversation threads.
  * Maintains context and continuity in conversations, enhancing the chatbot’s interactive capabilities.
* **Features:**
  * Allows customization of chatbot characteristics through agent settings (e.g., Agent Name, Instructions, Additional Context).
  * Manages conversation threads in the database, ensuring continuity and context.
  * Integrates with RAG to utilize content from any Knowledge Table, enhancing response accuracy and context-awareness.
* **Benefits:**
  * Simplifies the development and operational management of chatbots.
  * Enhances user engagement through intelligent and context-aware interactions.

### Knowledge Table <a href="#knowledgetable" id="knowledgetable"></a>

* **Purpose:** Acts as a repository for structured data and documents to support the LLM’s contextual understanding. Serves as an "AI content management system."
* **Functionality:**
  * Enables uploading and synchronization of documents and data.
  * Organizes data into structured formats with metadata generation, enhancing data retrieval capabilities.
  * Manages RAG data and structured metadata.
* **Benefits:**
  * Provides a rich contextual backdrop for LLM operations, improving response accuracy and relevance.
  * Supports other generative tables by supplying detailed, structured contextual information.

## Seamless Integration and Ease of Use <a href="#seamlessintegrationandeaseofuse" id="seamlessintegrationandeaseofuse"></a>

* **Advantage:** Designed to be as straightforward as integrating services like Firebase, focusing on ease of use without sacrificing power or flexibility.
* **Benefits:**
  * Allows developers to focus on building features and user experiences rather than managing complex AI integrations.
  * Reduces the learning curve and development time, accelerating the deployment of AI-powered applications.

## Best in class Retrieval-Augmented Generation (RAG)

* **Advantage:**&#x20;
  * **Intuitive, Table-Based Interface:** Build and manage your knowledge base like a spreadsheet - no more code wrestling.
  * **Flexible Indexing:** Keywords, text embeddings, images, even LLM-augmented knowledge tables – index the way that makes sense for your data.
  * **Powerhouse Retrieval:** Keyword matching, similarity searches, hybrid methods, multi-path recall – we've got you covered.
  * **Relevance FTW:** Re-ranking models optimize results, so your users get the most relevant info first.
* **Benefits:**
  * **Faster Development Cycles:** Streamlined knowledge management gets your projects to market quicker.
  * **Enhanced User Experience:** Accurate, relevant retrieval creates smarter, more satisfying applications.
  * **Effortless Scalability:** JamAI Base's adaptable design grows seamlessly with your ambitions.

## ETL capabilities

* **Advantage:**&#x20;
  * **One-Stop Data Cleanup:** TXT, Markdown, PDF, HTML, DOC, CSV... we handle the most common formats, so you don't have to write custom cleaning scripts.
  * **Unstructured Data? No Problem:** Our unstructured service tackles those gnarly data sources, maximizing what you can use.
* **Benefits:**
  * **Focus on Your Logic, Not Data Prep:** Less time on cleanup, more time building impactful features.
  * **Wider Range of Data Sources:** Expand your project scope without worrying about compatibility.

## **Open Source and Community-Driven**

* **Feature:** As an open-source platform, JamAIBase thrives on the contributions of its vibrant community.
* **Benefits:**
  * Fosters continuous enhancements, customizations, and expansions to meet the evolving needs of its users.

## **Designed for the JAMstack**

* **Feature:** Embraces the principles of JAMstack, offering a serverless architecture.
* **Benefits:**
  * Ensures improved performance, heightened security, and reduced costs for applications.

These features collectively make JamAIBase a powerful, flexible, and user-friendly platform for integrating advanced AI functionalities into various applications, making it an ideal choice for developers looking to leverage LLMs efficiently and effectively.


# Architecture

JamAI Base is an open-source platform that simplifies the integration of Large Language Models (LLMs) into applications. We choose open-source tools that are scalable and make them easy to use, focusing on AI-powered features and seamless LLM integration.

JamAI Base is a Backend as a service for LLM integration. We use LanceDB, an open-source vector database designed for AI workloads, and support existing tools wherever possible rather than developing from scratch.

Our choice of LanceDB is deliberate. We believe that LanceDB offers the functionality required to compete with other platforms while maintaining the scalability needed to go beyond them. LanceDB's ability to store and query embeddings on large-scale multi-modal data makes it an ideal choice for AI applications.

### Choose your comfort level:&#x20;

Our goal at JamAI Base is to make LLMs and AI accessible to developers of all skill levels. If you're an experienced AI developer, you'll appreciate the powerful tools and customization options we offer. If you're new to AI, start with our simple, intuitive interface and grow into more advanced features as needed.

## Key Components:

* Generative Tables: Transform static database tables into dynamic, AI-enhanced entities capable of generating and interacting with data autonomously.
  1. Action Tables: Create columns for user prompts, automatically generating API endpoints that interpret and respond to queries, with LLM-generated results streamed directly to the frontend.
  2. Knowledge Tables: Serve as repositories for collections of documents, forming a multimodal context that can be integrated into applications, enhancing the structure of retrieval systems with generated metadata columns.
* LanceDB Integration: Leverage LanceDB's vector database capabilities to store, manage, query, and retrieve embeddings on large-scale multi-modal data, enabling powerful AI applications.
* Seamless LLM Integration: Integrate with state-of-the-art LLMs like OpenAI GPT-4, Anthropic Claude, Google Gemini, and more, abstracting away the complexities of working directly with these models.

By focusing on simplicity, scalability, and AI-powered features, JamAI Base empowers developers to rapidly create and deploy intelligent applications without the steep learning curve associated with other platforms. Our open-source, community-driven approach ensures continuous improvement and adaptation to the evolving needs of AI developers.

### Declarative Paradigm as a Core Principle <a href="#declarative-paradigm-as-a-core-principle" id="declarative-paradigm-as-a-core-principle"></a>

JamAI Base embraces a declarative paradigm, focusing on the "what" rather than the "how." This approach empowers developers and data professionals to efficiently work with complex datasets without specifying the computation order. It is characterized by:

* **Declarative Programming**: Users define relationships and desired outcomes, and JamAI Base handles the underlying computations.
* **Non-Procedural**: There is no need to write procedures; the platform manages the data flow based on user declarations.
* **Functional**: While it supports functional programming through LLMs, users can request any action without specifying the method of execution.

Traditional no-code platforms often require users to build pipelines using drag-and-drop components to specify the "how," which can still be challenging to build and maintain. JamAI Base abstracts this complexity, allowing users to focus on defining what they want to achieve rather than how to achieve it. This simplification makes complex data operations accessible to users with varying levels of technical expertise and supports the creation of dynamic, intelligent applications.

This declarative nature simplifies interactions, making complex data operations accessible to users with varying levels of technical expertise, and supports the creation of dynamic, intelligent applications.

## Product Principles for JamAI Base

1. **AI-Driven Simplicity**: JamAI Base is designed to simplify the integration of Large Language Models (LLMs) into applications, making it accessible to developers of all skill levels. By leveraging the power of LanceDB and automating orchestration under the hood, JamAI Base enables users to focus on defining their data requirements through prompts, while the platform handles the complexities of data generation and management.
2. **Seamless Data Population:** With JamAI Base, users can effortlessly declare the data they need using natural language prompts. The platform intelligently interprets these prompts and automatically populates the corresponding database columns with relevant data in real-time. This eliminates the need for manual data entry or complex data retrieval processes, streamlining the development workflow.
3. **Real-Time API Access:** JamAI Base provides developers with instant access to the generated data through APIs. Similar to Firebase, developers can easily retrieve the populated data and stream it directly to their frontend applications. This real-time data synchronization ensures that applications always have access to the most up-to-date information, enabling dynamic and responsive user experiences.
4. **Collaborative AI Agents:** JamAI Base introduces a paradigm shift in data generation and management. Instead of relying solely on user-generated content, JamAI Base leverages the power of multiple AI agents to contribute and enrich the database. These AI agents work collaboratively to generate diverse and high-quality data based on the defined prompts, ensuring a rich and constantly evolving dataset.
5. **Scalability and Performance:** Built on top of LanceDB, JamAI Base inherits its scalability and performance characteristics. The platform is designed to handle large-scale data generation and retrieval efficiently, ensuring optimal performance even as the dataset grows. JamAI Base seamlessly scales to accommodate increasing demands, allowing developers to build applications that can handle high traffic and data-intensive workloads.
6. **Extensibility and Customization:** JamAI Base provides a flexible and extensible architecture that allows developers to customize and extend the platform to suit their specific needs. Developers can define custom prompts, configure AI agents, and integrate additional data sources to enhance the generated content. The platform's modular design enables seamless integration with existing tools and frameworks, facilitating a smooth development experience.
7. **Developer-Centric Ecosystem:** JamAI Base fosters a vibrant developer community, providing comprehensive documentation, tutorials, and resources to support developers in building AI-powered applications. The platform encourages collaboration, knowledge sharing, and the creation of reusable components and templates, enabling developers to leverage the collective intelligence of the community.
8. **Privacy and Security:** JamAI Base prioritizes the privacy and security of user data. The platform implements robust security measures to protect sensitive information and ensures compliance with relevant data protection regulations. Developers can trust JamAI Base to handle their data securely and maintain the confidentiality of their applications.

<br>


# New Features & Required Actions

Welcome to JamAI Base v2! This major update brings powerful new features, enhanced project-level role-based access control (RBAC), and improved performance to elevate your experience.

For users with old Organization Secret Keys (keys that look like <mark style="color:red;">"</mark><mark style="color:red;">`jamai_sk_6d5cdff`</mark>"), you will need to create new PATs to authenticate API access and take advantage of the enhanced project-level RBAC. \
\
\- Old keys: Organization-level keys.\
\- New PAT: Each token/key is now linked to a specific user (and optionally project) to enable granular access control. Linking a project is optional, but it will allow you to omit the "`X-PROJECT-ID`" header when performing API requests.

#### Quick Comparison: JamAI Base v1 vs JamAI Base v2

<div align="center" data-full-width="false"><figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FkcGrFYMlOS6dQHJdTSqL%2Fcomparison.png?alt=media&amp;token=eda41120-6813-49ff-9477-38a062e9ac85" alt=""><figcaption></figcaption></figure></div>

We will remove your Organization Secret Keys at the end of 2025 (they will remain functional for the time being). Please follow these steps to generate your PATs:

1. Log in to your JamAIBase account after the v2 update.
2. Go to User Settings.<br>

   <figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FQMKzspOzNBB4xrfq5meS%2FFrame%2028430.png?alt=media&amp;token=e9317fe6-eb36-4599-bae6-35cd1531c1f8" alt=""><figcaption></figcaption></figure>
3. Click "Create a Personal Access Token" under the Personal Access Token section.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FCifJeCvSLMPuHrTYi0bZ%2FFrame%2028426.png?alt=media&amp;token=d5450fec-257c-4981-ac63-137b57274b24" alt=""><figcaption></figcaption></figure>

4. Give the token a name, optionally set an expiry date and link it to a project. Click "Create". If you choose to create project-linked keys, then create one key for each project you access.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FKjNbgwWktjDGG1u9MN8Z%2Fguide3.png?alt=media&amp;token=9bc22e58-dede-4b04-80ea-a104702175bc" alt=""><figcaption></figcaption></figure>

5. Securely store the PAT (do not share it publicly).
6. Update your scripts, applications, or services to use the new PAT.<br>

### 🚀 What’s New in JamAIBase v2

JamAI Base v2 is packed with features to empower your workflows:

#### 🎉 JamAI Chat: Custom AI Agents

* Build Tailored AI Agents: Create AI agents customized for your business needs, teams, or knowledge domains using Retrieval-Augmented Generation (RAG) with Knowledge Tables. Get precise, relevant insights for every project.
* Advanced RBAC: Control access to AI agents with project-based roles. Admins can define who interacts with each agent, ensuring secure and focused collaboration.
* Seamless Interaction: End-users can engage with AI agents directly in the JamAI Chat interface, streamlining workflows and boosting productivity.

#### ⚡ Performance & Intelligence Upgrades

* Smarter Action Tables: Enhanced multimodal prompts deliver richer context, improving AI responses and workflow automation for more accurate results.
* Unified Backend: We’ve upgraded to a PostgreSQL stack from LanceDB and SQLite, delivering faster response times, better scalability, and rock-solid reliability. Our open-source commitment ensures ongoing innovation.

#### 🌟 Templates (Coming Soon!)

Soon, you’ll access a gallery of ready-to-use templates with pre-configured Action, Knowledge, and Chat Tables. These will simplify agent creation and accelerate your workflows.

#### 💡 FAQ

* What if I don’t update my PATs? Your old PAT will stop working, disrupting API-based workflows. Update promptly to avoid issues.
* Need help? Visit our [support page](https://support.jamaibase.com) or [contact our team](mailto:undefined) for assistance with PATs or v2 features.

Thank you for being part of the JamAI Base community! We’re excited for you to explore v2 and can’t wait to see what you build. Get started today, and reach out if you need support!


# Python SDK Documentation

JamAI Base is a **backend-as-a-service for AI applications**. You define tables with columns that automatically process data through AI pipelines.

### Column Types

| Type       | Purpose                 | Example                                       |
| ---------- | ----------------------- | --------------------------------------------- |
| **Input**  | Your data               | `dtype="str"`, `"file"`, `"image"`, `"audio"` |
| **LLM**    | AI generates content    | `gen_config=t.LLMGenConfig(...)`              |
| **Python** | Custom logic/validation | `gen_config=t.PythonGenConfig(...)`           |

**Data Types**: `str` (text), `file` (generic), `image` (.jpeg/.jpg/.png/.gif/.webp), `audio` (.mp3/.wav)

### How It Works

{% stepper %}
{% step %}
Define a table with input + AI columns
{% endstep %}

{% step %}
Add a row with input data
{% endstep %}

{% step %}
AI columns auto-generate based on your prompts
{% endstep %}

{% step %}
Read the completed row with all outputs
{% endstep %}
{% endstepper %}

### Available Models

```python
# List all chat models
chat_models = jamai.model_ids(capabilities=["chat"])
print(chat_models[:5])  # ['openai/gpt-5.2', 'openai/gpt-5.2-mini', 'anthropic/claude-3-5-sonnet', ...]

# List embedding models
embed_models = jamai.model_ids(capabilities=["embed"])
print(embed_models)  # ['ellm/BAAI/bge-m3', ...]

# Get full model info
models = jamai.model_info()
for m in models.data[:3]:
    print(f"{m.id}: context={getattr(m, 'context_length', 'N/A')}")
```

### Row Structure

Every row returned from `list_table_rows` contains:

```python
{
    'ID': 'uuid-string',           # Use for updates/deletes
    'Updated at': '2026-01-07...',  # Timestamp
    'column_name': {'value': 'actual_data'},  # WRAPPED!
    ...
}
```

{% hint style="warning" %}
**Value Wrapping Context:**

* **SDK reads** (`list_table_rows`, `get_table_row`): Values ARE wrapped → use `row['col']['value']`
* **Python Columns** (`row` dict inside `PythonGenConfig`): Values are NOT wrapped → use `row['col']` directly
  {% endhint %}

***

## QUICK REFERENCE

```python
# INSTALL
pip install jamaibase python-dotenv

# INITIALIZE
from jamaibase import JamAI
import jamaibase.types as t
jamai = JamAI(token="YOUR_PAT", project_id="YOUR_PROJECT_ID")

# CRITICAL: Values are wrapped - ALWAYS extract
def get_value(field):
    if isinstance(field, dict) and 'value' in field:
        return field['value']
    return field

# MAX LIMIT IS 100 - use pagination for more
```

***

## 1. SETUP

### Install

Python Version (>= 3.12)

```bash
pip install jamaibase python-dotenv
```

### Get Credentials

{% stepper %}
{% step %}
Sign up: <https://cloud.jamaibase.com/>
{% endstep %}

{% step %}
Create project
{% endstep %}

{% step %}
Get PAT key: Click on your user name on top right corner > [⚙ Account Settings](https://cloud.jamaibase.com/settings/account) > Create a Personal Access Token
{% endstep %}

{% step %}
Get Project ID: from project URL
{% endstep %}
{% endstepper %}

### Initialize Client

```python
from jamaibase import JamAI
import jamaibase.types as t
from dotenv import load_dotenv
import os

load_dotenv()

jamai = JamAI(
    token=os.getenv('JAMAI_TOKEN'),
    project_id=os.getenv('JAMAI_PROJECT_ID')
)

# Or auto-load from env vars
jamai = JamAI()

# Async version available
from jamaibase import JamAIAsync
```

### .env File

```env
# Default env vars (auto-loaded by SDK)
JAMAI_TOKEN=your_PAT
JAMAI_PROJECT_ID=your_project_id

# Optional
JAMAI_API_BASE=https://api.jamaibase.com/api
JAMAI_TIMEOUT_SEC=300
JAMAI_FILE_UPLOAD_TIMEOUT_SEC=900
```

**Note**: When using `JamAI()` without arguments, it auto-loads from environment variables.

***

## 2. TABLE TYPES

| Type        | Use Case                       | Create Method              |
| ----------- | ------------------------------ | -------------------------- |
| `action`    | AI chains, document processing | `create_action_table()`    |
| `knowledge` | RAG, embeddings, vector search | `create_knowledge_table()` |
| `chat`      | Conversational AI with context | `create_chat_table()`      |

***

## 3. ACTION TABLES (Most Common)

### Create

```python
table = jamai.table.create_action_table(
    t.ActionTableSchemaCreate(
        id="my_table",
        cols=[
            # Input column
            t.ColumnSchemaCreate(id="input", dtype="str"),

            # File input
            t.ColumnSchemaCreate(id="image", dtype="file"),

            # LLM output column
            t.ColumnSchemaCreate(
                id="output",
                dtype="str",
                gen_config=t.LLMGenConfig(
                    model="openai/gpt-5.2",
                    system_prompt="You are helpful.",
                    prompt="Process: ${input}\nImage: ${image}",
                    temperature=0.7,
                    max_tokens=500
                )
            ),

            # Python computed column
            t.ColumnSchemaCreate(
                id="word_count",
                dtype="str",
                gen_config=t.PythonGenConfig(
                    python_code='row["word_count"] = str(len(row["output"].split()))'
                )
            )
        ]
    )
)
```

### Column Reference Syntax

Use `${column_name}` in prompts to reference other columns. At runtime, each reference is replaced with the corresponding cell value from the current row.

```python
# Example prompt
prompt="Translate \"${input}\" into Italian:"

# If input column contains "Good morning", actual prompt sent to LLM:
# "Translate \"Good morning\" into Italian:"
```

### How LLM Columns Work

The LLM Column will:

1. **Gather prompts** - System Prompt and Prompt (which can reference upstream columns)
2. **Optional RAG** - Augment prompt with references from a Knowledge Table
3. **Send to LLM** - With your chosen generation settings (model, temperature, max\_tokens)
4. **Write response** - Model's response becomes the cell value

### LLM Generation Settings

| Parameter       | Description                                                             |
| --------------- | ----------------------------------------------------------------------- |
| `model`         | LLM model to use (e.g., `openai/gpt-5.2`)                               |
| `system_prompt` | Passed as-is as system message. Define role, style, global instructions |
| `prompt`        | Main user message with `${column}` references                           |
| `temperature`   | Controls randomness (0.0-2.0)                                           |
| `max_tokens`    | Maximum output length                                                   |

### RAG (Retrieval Augmented Generation)

Link an LLM column to a Knowledge Table for grounded responses. See [Section 9](#9-knowledge-tables-rag) for full Knowledge Table setup.

**RAG Flow:**

1. **Formulate query** → LLM generates retrieval query from your Prompt
2. **Retrieve** → Fetch relevant rows from Knowledge Table
3. **Rerank** → Optional reranking model (RRF Ranker by default)
4. **Inject** → Top-k references added to prompt
5. **Cite** → Optional inline citations: `[@ref0; @ref1; @ref2]`

```python
t.LLMGenConfig(
    model="openai/gpt-5.2",
    prompt="${question}",
    rag_params=t.RAGParams(
        table_id="my_knowledge_table",  # Must exist (see Section 9)
        k=3,  # Number of references to inject
        # reranking_model="...",  # Optional
    ),
    max_tokens=500
)
```

### Multi-turn Chat in Action Tables

Enable multi-turn chat to use previous rows as conversation history:

```python
t.ColumnSchemaCreate(
    id="response",
    dtype="str",
    gen_config=t.LLMGenConfig(
        model="openai/gpt-5.2",
        system_prompt="You are helpful.",
        prompt="${query}",
        multi_turn=True,  # Enable conversation history
        max_tokens=500
    )
)
```

With multi-turn enabled, each generation sees all previous rows as context.

### Prompting Tips

Separate column references using XML tags or Markdown headings:

```python
# XML tags (recommended)
prompt="""
<user-query>
${input}
</user-query>

Translate user query into Italian.
"""

# Markdown headings
prompt="""
# User Query
${input}

# Instruction
Translate user query into Italian.
"""
```

***

## 4. ADD ROWS

### Non-Streaming (Wait for Complete Response)

```python
response = jamai.table.add_table_rows(
    'action',
    t.MultiRowAddRequest(
        table_id="my_table",
        data=[
            {"input": "Hello world"},
            {"input": "Goodbye world"}
        ],
        stream=False
    )
)

# Get LLM output
print(response.rows[0].columns["output"].text)
```

### Streaming (Real-time Output)

```python
completion = jamai.table.add_table_rows(
    'action',
    t.MultiRowAddRequest(
        table_id="my_table",
        data=[{"input": "Hello"}],
        stream=True
    )
)

for chunk in completion:
    if chunk.output_column_name == "output":
        print(chunk.text, end="", flush=True)
```

### With File Upload

```python
# Upload file first
file_response = jamai.file.upload_file("/path/to/image.png")

# Use URI in row
jamai.table.add_table_rows(
    'action',
    t.MultiRowAddRequest(
        table_id="my_table",
        data=[{"image": file_response.uri, "input": "Describe this"}],
        stream=False
    )
)
```

### Get Row ID After Adding (Non-Streaming)

```python
response = jamai.table.add_table_rows(
    'action',
    t.MultiRowAddRequest(
        table_id="my_table",
        data=[{"input": "Hello"}],
        stream=False
    )
)

# Get the row ID for later updates/deletes
row_id = response.rows[0].row_id
print(f"Created row: {row_id}")

# Get LLM output
output = response.rows[0].columns["output"].text
```

### Get Row ID After Adding (Streaming)

```python
completion = jamai.table.add_table_rows(
    'action',
    t.MultiRowAddRequest(
        table_id="my_table",
        data=[{"input": "Hello"}],
        stream=True
    )
)

row_id = None
for chunk in completion:
    if hasattr(chunk, 'row_id') and chunk.row_id:
        row_id = chunk.row_id
    if chunk.output_column_name == "output" and hasattr(chunk, 'text'):
        print(chunk.text, end="", flush=True)

print(f"\nRow ID: {row_id}")
```

***

## 5. READ ROWS

### Basic List

```python
rows = jamai.table.list_table_rows(
    'action',
    'my_table',
    offset=0,
    limit=100  # MAX IS 100!
)

for row in rows.items:
    # IMPORTANT: Extract value from wrapper
    value = row['input']['value']  # or use get_value()
```

### With WHERE Filter

```python
# Syntax: "column" (double quotes) = 'value' (single quotes)
rows = jamai.table.list_table_rows(
    'action', 'my_table',
    where='"status" = \'active\''
)

# LIKE pattern
where='"name" LIKE \'%Smith%\''

# AND conditions
where='"status" = \'active\' AND "type" = \'premium\''
```

### Select Specific Columns

```python
rows = jamai.table.list_table_rows(
    'action', 'my_table',
    columns=['input', 'output']  # ID, Updated at always included
)
```

### Full-Text Search

```python
rows = jamai.table.list_table_rows(
    'action', 'my_table',
    search_query="machine learning"
)
```

### Pagination (REQUIRED for >100 rows)

```python
def get_all_rows(table_id, table_type='action'):
    all_rows = []
    offset = 0
    while True:
        response = jamai.table.list_table_rows(table_type, table_id, offset=offset, limit=100)
        all_rows.extend(response.items)
        if len(response.items) < 100:
            break
        offset += 100
    return all_rows
```

### Get Single Row

```python
row = jamai.table.get_table_row('action', 'my_table', 'row-uuid')
```

***

## 6. UPDATE ROWS

```python
jamai.table.update_table_rows(
    'action',
    t.MultiRowUpdateRequest(
        table_id="my_table",
        data={
            "row-uuid-1": {"column1": "new_value"},
            "row-uuid-2": {"column1": "value", "column2": "value2"}
        }
    )
)
```

***

## 7. DELETE ROWS

```python
jamai.table.delete_table_rows(
    'action',
    t.MultiRowDeleteRequest(
        table_id="my_table",
        row_ids=["uuid1", "uuid2"]
    )
)
```

***

## 8. TABLE OPERATIONS

### List Tables

```python
tables = jamai.table.list_tables('action', count_rows=True)
for t in tables.items:
    print(f"{t.id}: {t.num_rows} rows")
```

### Get Schema

```python
table = jamai.table.get_table('action', 'my_table')
for col in table.cols:
    print(f"{col.id}: {col.dtype}")
```

### Delete Table

```python
jamai.table.delete_table('action', 'my_table')
```

### Duplicate Table

```python
# With data
new_table = jamai.table.duplicate_table('action', 'source', 'copy')

# Schema only
new_table = jamai.table.duplicate_table('action', 'source', 'copy', include_data=False)
```

### Check If Table Exists

```python
def table_exists(jamai, table_type, table_id):
    """Check if a table exists before creating/using it."""
    try:
        jamai.table.get_table(table_type, table_id)
        return True
    except Exception:
        return False

# Usage
if not table_exists(jamai, 'action', 'my_table'):
    jamai.table.create_action_table(...)
```

### Safe Table Creation (Delete if Exists)

```python
def ensure_table(jamai, table_type, table_id, create_func):
    """Delete existing table and create fresh."""
    try:
        jamai.table.delete_table(table_type, table_id)
    except Exception:
        pass  # Table didn't exist
    return create_func()

# Usage
table = ensure_table(jamai, 'action', 'my_table', lambda:
    jamai.table.create_action_table(t.ActionTableSchemaCreate(...))
)
```

***

## 9. KNOWLEDGE TABLES (RAG)

### Create

```python
table = jamai.table.create_knowledge_table(
    t.KnowledgeTableSchemaCreate(
        id="my_kb",
        cols=[],  # Title, Text auto-created
        embedding_model="ellm/BAAI/bge-m3"
    )
)
```

### Add Data

```python
jamai.table.add_table_rows(
    'knowledge',
    t.MultiRowAddRequest(
        table_id="my_kb",
        data=[{"Title": "Doc1", "Text": "Content here..."}],
        stream=False
    )
)
```

### Embed File

```python
response = jamai.table.embed_file("/path/to/doc.txt", "my_kb")
```

### Create RAG Action Table

```python
table = jamai.table.create_action_table(
    t.ActionTableSchemaCreate(
        id="rag_qa",
        cols=[
            t.ColumnSchemaCreate(id="question", dtype="str"),
            t.ColumnSchemaCreate(
                id="answer",
                dtype="str",
                gen_config=t.LLMGenConfig(
                    model="openai/gpt-5.2",
                    prompt="${question}",
                    rag_params=t.RAGParams(
                        table_id="my_kb",  # Link to Knowledge Table
                        k=3                 # Top k chunks
                    ),
                    max_tokens=200
                )
            )
        ]
    )
)
```

***

## 10. CHAT TABLES

### Create

```python
table = jamai.table.create_chat_table(
    t.ChatTableSchemaCreate(
        id="my_chatbot",
        cols=[
            t.ColumnSchemaCreate(id="User", dtype="str"),
            t.ColumnSchemaCreate(
                id="AI",
                dtype="str",
                gen_config=t.LLMGenConfig(
                    model="openai/gpt-5.2",
                    system_prompt="You are helpful.",
                    max_tokens=500
                )
            )
        ]
    )
)
```

### Chat (Streaming)

```python
completion = jamai.table.add_table_rows(
    'chat',
    t.MultiRowAddRequest(
        table_id="my_chatbot",
        data=[{"User": "Hello!"}],
        stream=True
    )
)

for chunk in completion:
    if chunk.output_column_name == "AI":
        print(chunk.text, end="", flush=True)
```

### How Chat History Works

Chat tables **automatically maintain conversation history**. Each row added becomes part of the context for subsequent rows.

```python
# Turn 1
jamai.table.add_table_rows('chat', t.MultiRowAddRequest(
    table_id="my_chatbot",
    data=[{"User": "My name is Alice"}],
    stream=False
))

# Turn 2 - AI remembers the name from Turn 1
jamai.table.add_table_rows('chat', t.MultiRowAddRequest(
    table_id="my_chatbot",
    data=[{"User": "What's my name?"}],
    stream=False
))

# AI will respond: "Your name is Alice"

# View conversation history (get_value defined in Quick Reference)
rows = jamai.table.list_table_rows('chat', 'my_chatbot', limit=100)
for row in rows.items:
    print(f"User: {get_value(row.get('User'))}")
    print(f"AI: {get_value(row.get('AI'))}\n")
```

**Note**: Each chat table is a separate conversation. Create multiple tables for multiple users/sessions.

***

## 11. FILE OPERATIONS

### Upload

```python
file_response = jamai.file.upload_file("/path/to/file.png")
s3_uri = file_response.uri  # s3://devcloud-file/...
```

### Get Presigned URL (for display)

```python
import requests

def get_presigned_url(s3_url, jamai):
    if not s3_url or not s3_url.startswith('s3://'):
        return None
    response = requests.post(
        "https://api.jamaibase.com/api/v2/files/url/raw",
        headers={'Authorization': f'Bearer {jamai.token}', 'Content-Type': 'application/json'},
        json={'uris': [s3_url]}
    )
    if response.status_code == 200:
        return response.json().get('urls', [None])[0]
    return None
```

***

## 12. PYTHON COLUMNS

### Basic Concept

The Python Column lets you generate or transform cell values using custom Python code. All upstream columns (columns to the left) are passed as a dictionary named `row`.

* Keys in `row` are column names (strings, case-sensitive)
* Values are the corresponding cell values for that row
* Assign result to `row["Python Column Name"]` to set the output

### Syntax

```python
t.PythonGenConfig(
    python_code="""
try:
    # Read from upstream columns
    value_a = row["Input Column A"]
    value_b = row["Input Column B"]

    # Do some processing
    result = f"{value_a} - processed with {value_b}"

    # Write to this column
    row["Python Column Name"] = result
except Exception as e:
    row["Python Column Name"] = f"ERROR: {str(e)}"
"""
)
```

### Preinstalled Libraries

The following libraries are available:

| Library          | Use Case               |
| ---------------- | ---------------------- |
| `aiohttp`        | Async HTTP client      |
| `audioop-lts`    | Audio operations       |
| `beautifulsoup4` | HTML/XML parsing       |
| `httpx`          | HTTP requests          |
| `matplotlib`     | Plotting/visualization |
| `numpy`          | Numerical computing    |
| `opencv-python`  | Computer vision        |
| `orjson`         | Fast JSON parsing      |
| `pandas`         | Data manipulation      |
| `Pillow`         | Image processing       |
| `pyyaml`         | YAML parsing           |
| `regex`          | Advanced regex         |
| `requests`       | HTTP requests          |
| `ruamel.yaml`    | YAML parsing           |
| `scikit-image`   | Image processing       |
| `simplejson`     | JSON parsing           |
| `soundfile`      | Audio file I/O         |
| `sympy`          | Symbolic math          |
| `tiktoken`       | Token counting         |

### Column Data Types

| dtype   | Description                                 |
| ------- | ------------------------------------------- |
| `str`   | Text output                                 |
| `file`  | Generic file                                |
| `image` | Image file (.jpeg, .jpg, .png, .gif, .webp) |
| `audio` | Audio file (.mp3, .wav)                     |

### Working with Images

When an upstream column contains an image, its value in `row` is raw binary data (bytes).

```python
t.ColumnSchemaCreate(
    id="processed_image",
    dtype="image",  # Output type is image
    gen_config=t.PythonGenConfig(
        python_code="""
from PIL import Image
import io

try:
    # 1. Access the input image bytes
    image_bytes = row["Input Image Column"]

    # 2. Open as PIL Image
    with Image.open(io.BytesIO(image_bytes)) as img:
        # 3. Process (example: convert to grayscale)
        img = img.convert("L")

        # 4. Save to bytes buffer
        output_buffer = io.BytesIO()
        img.save(output_buffer, format="PNG")

        # 5. Assign bytes to column
        row["processed_image"] = output_buffer.getvalue()
except Exception as e:
    row["processed_image"] = None
"""
    )
)
```

### Working with Audio

When an upstream column contains audio, its value in `row` is also raw binary data (bytes).

```python
t.ColumnSchemaCreate(
    id="processed_audio",
    dtype="audio",  # Output type is audio
    gen_config=t.PythonGenConfig(
        python_code="""
import soundfile as sf
import io

try:
    # 1. Read input audio bytes
    with io.BytesIO(row["Input Audio Column"]) as input_buffer:
        data, samplerate = sf.read(input_buffer)

    # 2. Process (example: reduce volume by half)
    data = data * 0.5

    # 3. Write to buffer
    output_buffer = io.BytesIO()
    sf.write(output_buffer, data, samplerate, format="WAV", subtype="PCM_16")

    # 4. Assign bytes to column
    row["processed_audio"] = output_buffer.getvalue()
except Exception as e:
    row["processed_audio"] = None
"""
    )
)
```

### Making Web Requests

Use `httpx` to fetch data from the web:

```python
t.PythonGenConfig(
    python_code="""
import httpx
from bs4 import BeautifulSoup

try:
    # 1. Access the URL
    url = row["url_column"]

    # 2. Fetch HTML content
    response = httpx.get(url)

    # 3. Parse with BeautifulSoup
    soup = BeautifulSoup(response.text, "html.parser")

    # 4. Extract data (example: first h1 tag)
    extracted_text = soup.find("h1").text

    # 5. Assign to column
    row["extracted_title"] = extracted_text
except Exception as e:
    row["extracted_title"] = f"ERROR: {str(e)}"
"""
)
```

### Example: Name Matching

```python
t.PythonGenConfig(
    python_code="""
from difflib import SequenceMatcher
try:
    a = row["declared"].upper()
    b = row["extracted"].upper()
    ratio = SequenceMatcher(None, a, b).ratio()
    row["match"] = f"{'MATCH' if ratio > 0.85 else 'MISMATCH'} ({ratio*100:.0f}%)"
except Exception as e:
    row["match"] = f"ERROR: {str(e)}"
"""
)
```

***

## 13. DIRECT API (No Tables)

### Chat Completions

```python
request = t.ChatRequest(
    model="openai/gpt-5.2",
    messages=[
        t.ChatEntry.system("You are helpful."),
        t.ChatEntry.user("Hello"),
    ],
    max_tokens=100,
    stream=False
)
completion = jamai.generate_chat_completions(request)
print(completion.text)
```

### Embeddings

```python
embeddings = jamai.generate_embeddings(
    t.EmbeddingRequest(
        model="ellm/BAAI/bge-m3",
        input=["Hello world"]
    )
)
print(len(embeddings.data[0].embedding))  # 1024
```

### Model Info

```python
# All models
models = jamai.model_info()

# By capability
chat_models = jamai.model_ids(capabilities=["chat"])
embed_models = jamai.model_ids(capabilities=["embed"])
```

***

## 14. VALUE EXTRACTION (CRITICAL)

Row values are WRAPPED. Always extract:

```python
# WRONG
name = row['name']  # Returns {'value': 'Alice'}

# CORRECT
name = row['name']['value']  # Returns 'Alice'

# HELPER FUNCTION (recommended)
def get_value(field):
    if isinstance(field, dict) and 'value' in field:
        return field['value']
    return field

name = get_value(row.get('name'))
```

### JSON with Confidence Scores

```python
icr_string = get_value(row.get('ocr_result'))
icr_data = json.loads(icr_string)

# Structure: {"field": {"value": "...", "confidence": 95}}
name = icr_data.get('name', {}).get('value')
confidence = icr_data.get('name', {}).get('confidence', 0)
```

***

## 15. ERROR HANDLING

### Common Errors Table

| Error              | Solution                                   |
| ------------------ | ------------------------------------------ |
| `limit > 100`      | Use pagination, max is 100                 |
| `Table not found`  | Check table name, create if needed         |
| `Value is dict`    | Use `get_value()` helper                   |
| `JSONDecodeError`  | Wrap in try-except                         |
| `LLM empty`        | Wait 15-30s or use streaming               |
| `WHERE syntax`     | Use `"col" = 'val'` (double/single quotes) |
| `Model overloaded` | Retry with exponential backoff             |

### Error Handling Pattern

```python
import time
from jamaibase import JamAI
from jamaibase.exceptions import JamAIError  # Base exception class

def safe_add_row(jamai, table_id, data, max_retries=3):
    """Add row with retry logic for transient failures."""
    for attempt in range(max_retries):
        try:
            response = jamai.table.add_table_rows(
                'action',
                t.MultiRowAddRequest(
                    table_id=table_id,
                    data=[data],
                    stream=False
                )
            )
            return response.rows[0]
        except Exception as e:
            error_msg = str(e).lower()
            if 'overloaded' in error_msg or 'rate' in error_msg:
                wait_time = (2 ** attempt) * 5  # 5s, 10s, 20s
                print(f"Retry {attempt+1}/{max_retries} in {wait_time}s...")
                time.sleep(wait_time)
            else:
                raise  # Re-raise non-retryable errors
    raise Exception(f"Failed after {max_retries} retries")
```

### Handle Empty LLM Response

```python
def get_llm_output(response, column_name, timeout=30):
    """Get LLM output, waiting if generation is slow."""
    output = response.rows[0].columns.get(column_name)
    if output and output.text:
        return output.text

    # If empty, wait and re-fetch
    import time
    time.sleep(timeout)
    row_id = response.rows[0].row_id
    row = jamai.table.get_table_row('action', 'my_table', row_id)
    return get_value(row.get(column_name))
```

### Streaming Error Handling

```python
def safe_stream(completion):
    """Handle streaming with error recovery."""
    try:
        for chunk in completion:
            if hasattr(chunk, 'text'):
                yield chunk.text
    except Exception as e:
        print(f"Stream error: {e}")
        yield "[Stream interrupted]"
```

***

## 16. ASYNC OPERATIONS

### Async Client

```python
import asyncio
from jamaibase import JamAIAsync
import jamaibase.types as t

async def main():
    jamai = JamAIAsync(token="...", project_id="...")

    # Async list tables
    tables = await jamai.table.list_tables('action')

    # Async add row
    response = await jamai.table.add_table_rows(
        'action',
        t.MultiRowAddRequest(
            table_id="my_table",
            data=[{"input": "Hello"}],
            stream=False
        )
    )
    print(response.rows[0].columns["output"].text)

asyncio.run(main())
```

### Parallel Processing with Async

```python
async def process_batch(jamai, table_id, items):
    """Process multiple items in parallel."""
    tasks = []
    for item in items:
        task = jamai.table.add_table_rows(
            'action',
            t.MultiRowAddRequest(
                table_id=table_id,
                data=[item],
                stream=False
            )
        )
        tasks.append(task)

    results = await asyncio.gather(*tasks, return_exceptions=True)
    return results
```

***

## 17. REQUEST TYPES

### Current vs Deprecated Methods

| Current (Use This)        | Deprecated (Avoid)    |
| ------------------------- | --------------------- |
| `t.MultiRowAddRequest`    | `t.RowAddRequest`     |
| `t.MultiRowUpdateRequest` | `t.RowUpdateRequest`  |
| `t.MultiRowDeleteRequest` | `t.RowDeleteRequest`  |
| `jamai.model_ids()`       | `jamai.model_names()` |

{% hint style="warning" %}
Deprecated methods still work but will show warnings. Update your code to use the current methods.
{% endhint %}

***

## 18. COMPLETE EXAMPLE

End-to-end example: Create table, add row, read result, cleanup.

```python
from jamaibase import JamAI
import jamaibase.types as t
from dotenv import load_dotenv

load_dotenv()

# Initialize (auto-loads JAMAI_TOKEN, JAMAI_PROJECT_ID from env)
jamai = JamAI()

# Helper function (use throughout your code)
def get_value(field):
    if isinstance(field, dict) and 'value' in field:
        return field['value']
    return field

# 1. Create table with LLM column
TABLE_ID = "qa_demo"

# Delete if exists
try:
    jamai.table.delete_table('action', TABLE_ID)
except:
    pass

jamai.table.create_action_table(
    t.ActionTableSchemaCreate(
        id=TABLE_ID,
        cols=[
            t.ColumnSchemaCreate(id="question", dtype="str"),
            t.ColumnSchemaCreate(
                id="answer",
                dtype="str",
                gen_config=t.LLMGenConfig(
                    model="openai/gpt-5.2",
                    system_prompt="You are a helpful assistant. Be concise.",
                    prompt="Question: ${question}",
                    max_tokens=100
                )
            )
        ]
    )
)
print(f"Created table: {TABLE_ID}")

# 2. Add row (non-streaming)
response = jamai.table.add_table_rows(
    'action',
    t.MultiRowAddRequest(
        table_id=TABLE_ID,
        data=[{"question": "What is Python?"}],
        stream=False
    )
)
row_id = response.rows[0].row_id
answer = response.rows[0].columns["answer"].text
print(f"Row ID: {row_id}")
print(f"Answer: {answer}")

# 3. Read rows
rows = jamai.table.list_table_rows('action', TABLE_ID, limit=100)
for row in rows.items:
    q = get_value(row.get('question'))
    a = get_value(row.get('answer'))
    print(f"Q: {q}\nA: {a}\n")

# 4. Cleanup
jamai.table.delete_table('action', TABLE_ID)
print("Cleanup complete")
```

***

## QUICK COPY-PASTE

```python
# === IMPORTS ===
from jamaibase import JamAI
import jamaibase.types as t

# === INIT ===
jamai = JamAI()  # Auto-loads from JAMAI_TOKEN, JAMAI_PROJECT_ID env vars

# === HELPER (ALWAYS USE) ===
def get_value(field):
    if isinstance(field, dict) and 'value' in field:
        return field['value']
    return field

# === ADD ROWS ===
response = jamai.table.add_table_rows(
    'action',
    t.MultiRowAddRequest(table_id="TABLE", data=[{"col": "val"}], stream=False)
)
row_id = response.rows[0].row_id
output = response.rows[0].columns["output_col"].text

# === LIST ROWS (MAX 100!) ===
rows = jamai.table.list_table_rows('action', 'TABLE', limit=100)
for row in rows.items:
    value = get_value(row.get('column'))

# === WITH WHERE FILTER ===
rows = jamai.table.list_table_rows('action', 'TABLE', where='"status" = \'active\'')

# === UPDATE ROWS ===
jamai.table.update_table_rows('action', t.MultiRowUpdateRequest(
    table_id="TABLE", data={"row-id": {"col": "new_val"}}
))

# === DELETE ROWS ===
jamai.table.delete_table_rows('action', t.MultiRowDeleteRequest(
    table_id="TABLE", row_ids=["row-id-1", "row-id-2"]
))

# === FILE UPLOAD ===
file_resp = jamai.file.upload_file("/path/to/file.png")
uri = file_resp.uri  # Use in row data

# === STREAMING ===
completion = jamai.table.add_table_rows(
    'action',
    t.MultiRowAddRequest(table_id="TABLE", data=[{"col": "val"}], stream=True)
)
for chunk in completion:
    if chunk.output_column_name == "output_col":
        print(chunk.text, end="", flush=True)
```


# Quick Start: Action Table

What are Multimodal Action Tables?

Action tables can process various types of input (text, images, audio) and generate structured outputs using VLM/multimodal LLM. They're particularly useful for:

* Document Processing
* Text / Image / Audio Content Analysis
* Data Extraction
* Automated Content Generation


# Action Table - Image

Build a Smart Receipt Scanner with JamAI Base

## 1. Introduction

This tutorial will guide you through using the JamAI Base SDK to create a simple receipt information extraction system.

### What We'll Build

Snap a photo of your receipt and let AI do the expense report for you!

In this tutorial, we'll create a receipt processing system that:

1. Takes a receipt image as input
2. Uploads it to JamAI Base action table
3. Extracts key information like:
   * Shop name
   * Total amount

### Prerequisites

Before starting, you'll need:

* Python 3.11 or higher installed
* Project ID and Personal Access Token (PAT)

## 2. Installation and Setup

### Installing the SDK

```bash
pip install jamaibase
```

### Basic Configuration

Get your Personal Access Token (PAT) here:

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FKSAVd8tlm3hnNQrY78Xx%2FSlide1.PNG?alt=media&amp;token=f8132eaf-7b1a-4f66-9361-10b1ac07e955" alt="" width="563"><figcaption><p>How to generate PAT</p></figcaption></figure>

Get your Project ID here:

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FuWoF11Sho8o6NVAAgJvj%2FProjectID.png?alt=media&amp;token=6354535b-0638-4237-8228-7f4d8393b78d" alt="" width="563"><figcaption><p>How to get Project ID</p></figcaption></figure>

```python
from jamaibase import JamAI, types as t

PROJECT_ID = "your_project_id"
PAT = "your_PAT"

client = JamAI(
    project_id=PROJECT_ID,
    token=PAT
)
```

## 3. Creating Your Action Table

For simplicity, you can set up your action table in the JamAI Base platform:

1. Navigate to your JamAI Base action table tab
2. Create a new action table named `"receipt"`

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FI0UwMOEctMaLdWoqcFE2%2Fimage.png?alt=media&amp;token=4c580263-b842-4d6a-a14f-8402b96c1e11" alt="" width="563"><figcaption></figcaption></figure>

3. Configure the following columns:

```
Input Column:
- Name: "Image"
- Type: FILE

Output Columns:
- Name: "Shop Name"
  Type: str
  ☑️ Output checkbox enabled

- Name: "Total"
  Type: str
  ☑️ Output checkbox enabled
```

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FRrWHksKdD2rvtQ7V1lLy%2Fimage.png?alt=media&amp;token=aed51899-dcb3-4aff-b288-7e6775d52040" alt="" width="563"><figcaption><p>Create new action table</p></figcaption></figure>

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FQY3v6aOmcvQXxzKUl0Dg%2Fimage.png?alt=media&amp;token=ee6607e6-6fba-42b4-894b-d4300bd81246" alt="" width="563"><figcaption><p>Created table</p></figcaption></figure>

## 4. Basic Implementation

### 4.1 Simple Receipt Processor

```python
def process_single_receipt(image_path):
    # Upload image file
    file_response = client.file.upload_file(image_path)
    
    # Process in action table
    response = client.table.add_table_rows(
        table_type=t.TableType.ACTION,
        request=t.RowAddRequest(
            table_id="receipt",
            data=[{"Image": file_response.uri}],
            stream=False,
        ),
    )
    
    # Extract results
    return {
        "shop_name": response.rows[0].columns["Shop Name"].text,
        "total": response.rows[0].columns["Total"].text
    }
```

### 4.2 Complete Implementation with Error Handling

```python
import os
from typing import Dict, Optional

class ReceiptProcessor:
    def __init__(self, project_id: str, pat: str):
        self.client = JamAI(
            project_id=project_id,
            token=pat
        )
    
    def validate_image(self, image_path: str) -> bool:
        """Validate if file exists and has correct extension"""
        if not os.path.exists(image_path):
            raise FileNotFoundError(f"Image not found: {image_path}")
            
        valid_extensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif']
        file_ext = os.path.splitext(image_path)[1].lower()
        if file_ext not in valid_extensions:
            raise ValueError(f"Unsupported file format. Use: {valid_extensions}")
            
        return True

    def process_receipt(self, image_path: str) -> Optional[Dict[str, str]]:
        """Process a single receipt image"""
        try:
            # Validate image
            self.validate_image(image_path)
            
            # Upload file
            print("Uploading image...")
            file_response = self.client.file.upload_file(image_path)
            print(f"Upload successful: {file_response.uri}")
            
            # Process in action table
            print("Processing receipt...")
            response = self.client.table.add_table_rows(
                table_type=t.TableType.ACTION,
                request=t.RowAddRequest(
                    table_id="receipt",
                    data=[{"Image": file_response.uri}],
                    stream=False,
                ),
            )
            
            # Extract and return results
            results = {
                "shop_name": response.rows[0].columns["Shop Name"].text,
                "total": response.rows[0].columns["Total"].text
            }
            print("Processing complete!")
            return results
            
        except Exception as e:
            print(f"Error processing receipt: {str(e)}")
            return None
```

## 5. Usage Examples

### 5.1 Basic Usage

```python
# Initialize processor
processor = ReceiptProcessor(PROJECT_ID, PAT)

# Process single receipt
result = processor.process_receipt("path/to/receipt.jpg")

if result:
    print(f"Shop Name: {result['shop_name']}")
    print(f"Total: {result['total']}")
```

### 5.2 Batch Processing

```python
def process_receipt_batch(receipt_folder: str):
    processor = ReceiptProcessor(PROJECT_ID, PAT)
    results = []
    
    for filename in os.listdir(receipt_folder):
        if filename.lower().endswith(('.jpg', '.jpeg', '.png', '.webp', '.gif')):
            image_path = os.path.join(receipt_folder, filename)
            result = processor.process_receipt(image_path)
            if result:
                results.append({
                    "filename": filename,
                    **result
                })
    
    return results

# Usage
results = process_receipt_batch("path/to/receipt/folder")
for result in results:
    print(f"File: {result['filename']}")
    print(f"Shop: {result['shop_name']}")
    print(f"Total: {result['total']}")
    print("---")
```

## 6. Best Practices

1. **Error Handling**
   * Always validate input files
   * Handle network errors gracefully
2. **Performance**
   * Reuse the client instance
   * Consider batch processing for multiple files
   * Implement rate limiting for large batches
3. **Security**
   * Use environment variables for credentials

## Complete Standalone Example

Save this as `receipt_processor.py`:

```python
import os
import argparse
from jamaibase import JamAI, types as t 
from typing import Dict, Optional

class ReceiptProcessor:
    def __init__(self, project_id: str, pat: str):
        self.client = JamAI(
            project_id=project_id,
            token=pat
        )
    
    def validate_image(self, image_path: str) -> bool:
        if not os.path.exists(image_path):
            raise FileNotFoundError(f"Image not found: {image_path}")
            
        valid_extensions = ['.jpg', '.jpeg', '.png']
        file_ext = os.path.splitext(image_path)[1].lower()
        if file_ext not in valid_extensions:
            raise ValueError(f"Unsupported file format. Use: {valid_extensions}")
            
        return True

    def process_receipt(self, image_path: str) -> Optional[Dict[str, str]]:
        try:
            self.validate_image(image_path)
            
            print(f"Processing receipt: {image_path}")
            print("Uploading image...")
            file_response = self.client.file.upload_file(image_path)
            print(f"Upload successful!")
            
            print("Extracting information...")
            response = self.client.add_table_rows(
                table_type=t.TableType.ACTION,
                request=t.RowAddRequest(
                    table_id="receipt",
                    data=[{"Image": file_response.uri}],
                    stream=False,
                ),
            )
            
            results = {
                "shop_name": response.rows[0].columns["Shop Name"].text,
                "total": response.rows[0].columns["Total"].text
            }
            return results
            
        except Exception as e:
            print(f"Error: {str(e)}")
            return None

def process_folder(folder_path: str, processor: ReceiptProcessor) -> None:
    """Process all receipts in a folder"""
    if not os.path.exists(folder_path):
        print(f"Folder not found: {folder_path}")
        return

    results = []
    for filename in os.listdir(folder_path):
        if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
            image_path = os.path.join(folder_path, filename)
            result = processor.process_receipt(image_path)
            if result:
                results.append({
                    "filename": filename,
                    **result
                })
    
    # Print results in a formatted way
    print("\nProcessing Results:")
    print("-" * 50)
    for result in results:
        print(f"File: {result['filename']}")
        print(f"Shop Name: {result['shop_name']}")
        print(f"Total: {result['total']}")
        print("-" * 50)

def main():
    # Set up argument parser
    parser = argparse.ArgumentParser(description='Process receipt images using JamAIBase')
    parser.add_argument('--project-id', required=True, help='Your JamAIBase project ID')
    parser.add_argument('--pat', required=True, help='Your Personal Access Token')
    parser.add_argument('--input', required=True, help='Path to image file or folder')
    
    args = parser.parse_args()

    # Initialize processor
    processor = ReceiptProcessor(args.project_id, args.pat)

    # Process input
    if os.path.isfile(args.input):
        # Single file processing
        result = processor.process_receipt(args.input)
        if result:
            print("\nResults:")
            print("-" * 50)
            print(f"Shop Name: {result['shop_name']}")
            print(f"Total: {result['total']}")
            print("-" * 50)
    else:
        # Folder processing
        process_folder(args.input, processor)

if __name__ == "__main__":
    main()
```

### How to Run

1. Save the code above as `receipt_processor.py`
2. Install required package:

```bash
pip install jamaibase
```

3. Run for a single receipt:

```bash
python receipt_processor.py --project-id "your_project_id" --pat "your_pat" --input "path/to/receipt.jpg"
```

4. Run for a folder of receipts:

```bash
python receipt_processor.py --project-id "your_project_id" --pat "your_pat" --input "path/to/receipt/folder"
```

### Example Output

```
Processing receipt: 20240920_033000.jpg
Uploading image...
Upload successful!
Extracting information...

Results:
--------------------------------------------------
Shop Name: Burger King
Total: 523
--------------------------------------------------
```

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FOqaWMyl49k18O3RXW0Ai%2Fimage.png?alt=media&amp;token=620fc9c1-e6fe-4f54-b0b2-3016399b1c91" alt=""><figcaption><p>The receipt table with the uploaded image.</p></figcaption></figure>

### Features

* Processes single images or entire folders
* Validates file types and existence
* Provides clear progress feedback
* Formats results in an easy-to-read way
* Includes error handling
* Command-line argument support

This standalone example provides a complete, working implementation that you can use as a starting point for your own projects or modify according to your needs.


# Action Table - Audio

Build a Smart Audio Processor with JamAI Base

## **1. Introduction**

This tutorial will guide you through using the JamAI Base SDK to create a simple audio information processing system.

**What We'll Build**

Automate tasks with audio! Imagine processing meeting recordings or voice memos automatically.

In this tutorial, we'll create an audio processing system that:

* Takes an audio file as input (e.g., `.mp3`, `.wav`).
* Uploads it to a JamAI Base action table.
* Extracts and generates key information such as:
  * Transcription of the audio content.
  * Summary of the audio.

**Prerequisites**

Before starting, you'll need:

* Python 3.11 or higher installed
* Project ID and Personal Access Token (PAT)

## **2. Installation and Setup**

**Installing the SDK**

```bash
pip install jamaibase
```

**Basic Configuration**

Get your Personal Access Token (PAT) here:

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fu07PCWBszll8IUiBQiRO%2Fimage.png?alt=media&amp;token=bf72c471-4ceb-4e05-ae6e-147c88f3a802" alt=""><figcaption><p>How to generate PAT</p></figcaption></figure>

Get your Project ID here:

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FyI9GB2jzjk25r2M4XYcO%2Fimage.png?alt=media&amp;token=c20c34b3-eaa8-4b40-b0b5-63facef32423" alt=""><figcaption><p>How to get Project ID</p></figcaption></figure>

```python
from jamaibase import JamAI, types as t 

PROJECT_ID = "your_project_id"
PAT = "your_PAT"

client = JamAI(
    project_id=PROJECT_ID,
    token=PAT
)
```

## **3. Creating Your Action Table**

For simplicity, you can set up your action table in the JamAI Base platform:

1. Navigate to your JamAI Base action table tab.
2. Create a new action table named "AudioProcessor".

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2F2OBcvCBxXAJIGbAlrhSc%2Fimage.png?alt=media&amp;token=4b61454f-bd3d-43bb-bf08-bb36ee3f4b34" alt=""><figcaption></figcaption></figure>

3. Configure the following columns:

```
Input Column:
- Name: "Audio"
- Type: Audio

Output Columns:
- Name: "Transcription"
  Type: Text
  ☑️ Output checkbox enabled

- Name: "Summary"
  Type: Text
  ☑️ Output checkbox enabled
```

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FxVtglX2kWYUydyEEGEjm%2Fimage.png?alt=media&amp;token=153c26d7-b6b9-4f72-bd26-1ad71afada88" alt=""><figcaption><p>Create a new action table</p></figcaption></figure>

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fc8DHkE5lVpZlDiU6SndU%2Fimage.png?alt=media&amp;token=4014c5ce-e68c-4981-aec2-e37b726fe7f3" alt=""><figcaption><p>Created action table</p></figcaption></figure>

## **4. Basic Implementation**

### **4.1 Simple Audio Processor**

```python
def process_single_audio(audio_path):
    # Upload audio file
    file_response = client.file.upload_file(audio_path)

    # Process in action table
    response = client.table.add_table_rows(
        table_type=t.TableType.ACTION,
        request=t.RowAddRequest(
            table_id="AudioProcessor",
            data=[{"Audio": file_response.uri}],
            stream=False,
        ),
    )

    # Extract results
    return {
        "transcription": response.rows[0].columns["Transcription"].text,
        "summary": response.rows[0].columns["Summary"].text
    }
```

### **4.2 Complete Implementation with Error Handling**

```python
import os
from typing import Dict, Optional

class AudioProcessor:
    def __init__(self, project_id: str, pat: str):
        self.client = JamAI(
            project_id=project_id,
            token=pat
        )

    def validate_audio(self, audio_path: str) -> bool:
        """Validate if file exists and has correct extension"""
        if not os.path.exists(audio_path):
            raise FileNotFoundError(f"Audio file not found: {audio_path}")

        valid_extensions = ['.mp3', '.wav']
        file_ext = os.path.splitext(audio_path)[1].lower()
        if file_ext not in valid_extensions:
            raise ValueError(f"Unsupported file format. Use: {valid_extensions}")

        return True

    def process_audio(self, audio_path: str) -> Optional[Dict[str, str]]:
        """Process a single audio file"""
        try:
            # Validate audio
            self.validate_audio(audio_path)

            # Upload file
            print("Uploading audio...")
            file_response = self.client.file.upload_file(audio_path)
            print(f"Upload successful: {file_response.uri}")

            # Process in action table
            print("Processing audio...")
            response = self.client.table.add_table_rows(
                table_type=t.TableType.ACTION,
                request=t.RowAddRequest(
                    table_id="AudioProcessor",
                    data=[{"Audio": file_response.uri}],
                    stream=False,
                ),
            )

            # Extract and return results
            results = {
                "transcription": response.rows[0].columns["Transcription"].text,
                "summary": response.rows[0].columns["Summary"].text
            }
            print("Processing complete!")
            return results

        except Exception as e:
            print(f"Error processing audio: {str(e)}")
            return None
```

## **5. Usage Examples**

### **5.1 Basic Usage**

```python
# Initialize processor
processor = AudioProcessor(PROJECT_ID, PAT)

# Process single audio
result = processor.process_audio("path/to/audio.mp3")

if result:
    print(f"Transcription: {result['transcription']}")
    print(f"Summary: {result['summary']}")
```

### **5.2 Batch Processing**

```python
def process_audio_batch(audio_folder: str):
    processor = AudioProcessor(PROJECT_ID, PAT)
    results = []

    for filename in os.listdir(audio_folder):
        if filename.lower().endswith(('.mp3', '.wav')):
            audio_path = os.path.join(audio_folder, filename)
            result = processor.process_audio(audio_path)
            if result:
                results.append({
                    "filename": filename,
                    **result
                })

    return results

# Usage
results = process_audio_batch("path/to/audio/folder")
for result in results:
    print(f"File: {result['filename']}")
    print(f"Transcription: {result['transcription']}")
    print(f"Summary: {result['summary']}")
    print("---")
```

## **6. Best Practices**

**Error Handling**

* Always validate input audio files.
* Handle network errors gracefully.
* Consider adding specific error handling for audio processing failures (e.g., silence, corrupted files).

**Performance**

* Reuse the client instance.
* Consider batch processing for multiple files.
* Implement rate limiting for large batches if needed.

**Security**

* Use environment variables for credentials.

**Complete Standalone Example**

Save this as `audio_processor.py`:

```python
import os
import argparse
from jamaibase import JamAI, protocol as p
from typing import Dict, Optional

class AudioProcessor:
    def __init__(self, project_id: str, pat: str):
        self.client = JamAI(
            project_id=project_id,
            token=pat
        )

    def validate_audio(self, audio_path: str) -> bool:
        if not os.path.exists(audio_path):
            raise FileNotFoundError(f"Audio file not found: {audio_path}")

        valid_extensions = ['.mp3', '.wav'] 
        file_ext = os.path.splitext(audio_path)[1].lower()
        if file_ext not in valid_extensions:
            raise ValueError(f"Unsupported file format. Use: {valid_extensions}")

        return True

    def process_audio(self, audio_path: str) -> Optional[Dict[str, str]]:
        try:
            self.validate_audio(audio_path)

            print(f"Processing audio: {audio_path}")
            print("Uploading audio...")
            file_response = self.client.file.upload_file(audio_path)
            print(f"Upload successful!")

            print("Extracting information...")
            response = self.client.table.add_table_rows(
                table_type=t.TableType.ACTION,
                request=t.RowAddRequest(
                    table_id="AudioProcessor",
                    data=[{"Audio": file_response.uri}],
                    stream=False,
                ),
            )

            results = {
                "transcription": response.rows[0].columns["Transcription"].text,
                "summary": response.rows[0].columns["Summary"].text
            }
            return results

        except Exception as e:
            print(f"Error: {str(e)}")
            return None

def process_folder(folder_path: str, processor: AudioProcessor) -> None:
    """Process all audio files in a folder"""
    if not os.path.exists(folder_path):
        print(f"Folder not found: {folder_path}")
        return

    results = []
    for filename in os.listdir(folder_path):
        if filename.lower().endswith(('.mp3', '.wav')):
            audio_path = os.path.join(folder_path, filename)
            result = processor.process_audio(audio_path)
            if result:
                results.append({
                    "filename": filename,
                    **result
                })

    # Print results in a formatted way
    print("\nProcessing Results:")
    print("-" * 50)
    for result in results:
        print(f"File: {result['filename']}")
        print(f"Transcription: {result['transcription']}")
        print(f"Summary: {result['summary']}")
        print("-" * 50)

def main():
    # Set up argument parser
    parser = argparse.ArgumentParser(description='Process audio files using JamAIBase')
    parser.add_argument('--project-id', required=True, help='Your JamAIBase project ID')
    parser.add_argument('--pat', required=True, help='Your Personal Access Token')
    parser.add_argument('--input', required=True, help='Path to audio file or folder')

    args = parser.parse_args()

    # Initialize processor
    processor = AudioProcessor(args.project_id, args.pat)

    # Process input
    if os.path.isfile(args.input):
        # Single file processing
        result = processor.process_audio(args.input)
        if result:
            print("\nResults:")
            print("-" * 50)
            print(f"Transcription: {result['transcription']}")
            print(f"Summary: {result['summary']}")
            print("-" * 50)
    else:
        # Folder processing
        process_folder(args.input, processor)

if __name__ == "__main__":
    main()
```

**How to Run**

1. Save the code above as `audio_processor.py`
2. Install required package:

   ```bash
   pip install jamaibase
   ```
3. Run for a single audio file:

   ```bash
   python audio_processor.py --project-id "your_project_id" --pat "your_pat" --input "path/to/audio.mp3"
   ```
4. Run for a folder of audio files:

   ```bash
   python audio_processor.py --project-id "your_project_id" --pat "your_pat" --input "path/to/audio/folder"
   ```

**Example Output**

```
Processing audio: discussion_recording.mp3
Uploading audio...
Upload successful!
Extracting information...

Results:
--------------------------------------------------
Transcription: ... [The full transcription of the audio] ...
Summary: This audio discusses the cost of AI model training. Stanford researchers...
--------------------------------------------------
```

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FKSzO3yTD9lirTkUzIMts%2Fimage.png?alt=media&amp;token=a50554a6-1a34-4925-8382-90571c02e4fe" alt=""><figcaption><p>The AudioProcessor table with the uploaded audio.</p></figcaption></figure>

**Features**

* Processes single audio files or entire folders.
* Validates audio file types and existence.
* Provides clear progress feedback.
* Formats results (transcription and summary) in an easy-to-read way.
* Includes error handling.
* Command-line argument support.


# Action Table - Document

Summarize news with JamAI Base

## 1. Introduction

This tutorial will guide you through using the JamAI Base Python SDK to create a simple news summarization system.

### What We'll Build

Get a news and let AI tells you the main message!

In this tutorial, we'll create a news summarization system that:

1. Takes a news document as input. The supported formats include: .csv, .tsv, .txt, .md, .doc, .docx, .pdf, .ppt, .pptx, .xls, .xlsx, .xml, .html, .json, .jsonl.
2. Upload the document to JamAI Base action table.
3. Obtain the key information like:
   * Summary
   * Tag

### Prerequisites

Before starting, you'll need:

* Python 3.11 or higher installed
* Project ID and Personal Access Token (PAT)&#x20;

## 2. Installation and Setup

### Installing the Python SDK

```bash
pip install jamaibase
```

### Basic Configuration

Get your Personal Access Token (PAT) here:

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FKSAVd8tlm3hnNQrY78Xx%2FSlide1.PNG?alt=media&amp;token=f8132eaf-7b1a-4f66-9361-10b1ac07e955" alt=""><figcaption><p>How to generate PAT</p></figcaption></figure>

Get your Project ID here:

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FyI9GB2jzjk25r2M4XYcO%2Fimage.png?alt=media&amp;token=c20c34b3-eaa8-4b40-b0b5-63facef32423" alt=""><figcaption><p>How to get Project ID</p></figcaption></figure>

```python
from jamaibase import JamAI, types as t

PROJECT_ID = "your_project_id"
PAT = "your_PAT"

client = JamAI(
    project_id=PROJECT_ID,
    token=PAT
)
```

{% hint style="info" %}
You can use a .env file to manage your PROJECT\_ID and PAT.
{% endhint %}

## 3. Creating Your Action Table

For simplicity, you can set up your action table in the JamAI Base platform:

1. Navigate to your JamAI Base action table tab
2. Create a new action table named `"news_summarization"`

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2F2OBcvCBxXAJIGbAlrhSc%2Fimage.png?alt=media&amp;token=4b61454f-bd3d-43bb-bf08-bb36ee3f4b34" alt=""><figcaption></figcaption></figure>

3. Create the following columns:&#x20;

| Name     | Column Type | Data Type |
| -------- | ----------- | --------- |
| document | Input       | Document  |
| summary  | LLM Output  | Text      |
| tag      | LLM Output  | Text      |

4. Update the Prompt as LLM Output columns as follows:

<table><thead><tr><th width="113.199951171875">Column name</th><th>Prompt</th></tr></thead><tbody><tr><td>summary</td><td><p>Table name: "news_summarization"</p><p></p><p>document: ${document}</p><p></p><p>Summarize the article in not more than three sentences and not more than 50 words.</p><p>Provide the summary in the number form.</p><p>Be factual and do not hallucinate. Remember to act as a cell in a spreadsheet and provide concise, relevant information without explanations unless specifically requested.</p></td></tr><tr><td>tag</td><td><p>Table name: "news_summarization"</p><p></p><p>document: ${document}</p><p></p><p>Provide at most three tags that well represent the document. Each tag should not have more than three words.</p><p>Provide the tag in the number form.</p><p>Be factual and do not hallucinate. Remember to act as a cell in a spreadsheet and provide concise, relevant information without explanations unless specifically requested.</p></td></tr></tbody></table>

## 4. Basic Implementation

### 4.1 Simple Document Processor

```python
def process_single_news(document_path):
    # Upload document file
    file_response = client.file.upload_file(document_path)

    # Process in action table
    response = client.table.add_table_rows(
        table_type=t.TableType.ACTION,
        request=t.MultiRowAddRequest(
            table_id="news_summarization",
            data=[{"document": file_response.uri}],
            stream=False,
        ),
    )

    # Extract results
    return {
        "summary": response.rows[0].columns["summary"].text,
        "tag": response.rows[0].columns["tag"].text
    }
```

### 4.2 Complete Implementation with Error Handling

```python
class DocumentProcessor:
    def __init__(self, project_id: str, pat: str):
        self.client = JamAI(
            project_id=project_id,
            token=pat
        )

    def validate_document(self, document_path: str) -> bool:
        """Validate if file exists and has correct extension"""
        if not os.path.exists(document_path):
            raise FileNotFoundError(f"Document not found: {document_path}")

        valid_extensions = [
            '.csv', '.tsv', '.txt', '.md', '.doc', '.docx', '.pdf', 
            '.ppt', '.pptx', '.xls', '.xlsx', '.xml', '.html', 
            '.json', '.jsonl'
        ]
        
        file_ext = os.path.splitext(document_path)[1].lower()
        if file_ext not in valid_extensions:
            raise ValueError(f"Unsupported file format. Use: {valid_extensions}")

        return True

    def process_document(self, document_path: str) -> Optional[Dict[str, str]]:
        """Process a single document file"""
        try:
            # Validate document
            self.validate_document(document_path)

            # Upload file
            print("Uploading document...")
            file_response = self.client.file.upload_file(document_path)
            print(f"Upload successful: {file_response.uri}")

            # Process in action table
            print("Processing document...")
            response = self.client.table.add_table_rows(
                table_type=t.TableType.ACTION,
                request=t.MultiRowAddRequest(
                    table_id="news_summarization",
                    data=[{"document": file_response.uri}],
                    stream=False,
                ),
            )

            # Extract and return results
            results = {
                "summary": response.rows[0].columns["summary"].text,
                "tag": response.rows[0].columns["tag"].text
            }
            print("Processing complete!")
            return results

        except Exception as e:
            print(f"Error processing document: {str(e)}")
            return None
```

## 5. Usage Examples

### 5.1 Basic Usage

```python
# Initialize processor
processor = DocumentProcessor(PROJECT_ID, PAT)

# Process single receipt
result = processor.process_document("path/to/news.txt")

if result:
    print(f"Summary: {result['summary']}")
    print(f"Tag: {result['tag']}")
```

### 5.2 Batch Processing

```python
def process_document_batch(document_folder: str):
    processor = DocumentProcessor(PROJECT_ID, PAT)
    results = []

    for filename in os.listdir(document_folder):
        # Check if file has a supported document extension
        valid_extensions = [
            '.csv', '.tsv', '.txt', '.md', '.doc', '.docx', '.pdf', 
            '.ppt', '.pptx', '.xls', '.xlsx', '.xml', '.html', 
            '.json', '.jsonl'
        ]
        
        if any(filename.lower().endswith(ext) for ext in valid_extensions):
            document_path = os.path.join(document_folder, filename)
            result = processor.process_document(document_path)
            if result:
                results.append({
                    "filename": filename,
                    **result
                })

    return results

# Usage
results = process_document_batch("path/to/documents/folder")
for result in results:
    print(f"File: {result['filename']}")
    print(f"Summary: {result['summary']}")
    print(f"Tag: {result['tag']}")
    print("---")
```

## 6. Best Practices

1. **Error Handling**
   * Always validate input files
   * Handle network errors gracefully
2. **Performance**
   * Reuse the client instance
   * Consider batch processing for multiple files
   * Implement rate limiting for large batches
3. **Security**
   * Use environment variables for credentials

## Complete Standalone Example

```python
import os
import argparse
from jamaibase import JamAI, types as t
from typing import Dict, Optional

class DocumentProcessor:
    def __init__(self, project_id: str, pat: str):
        self.client = JamAI(
            project_id=project_id,
            token=pat
        )

    def validate_document(self, document_path: str) -> bool:
        if not os.path.exists(document_path):
            raise FileNotFoundError(f"Document not found: {document_path}")

        valid_extensions = [
            '.csv', '.tsv', '.txt', '.md', '.doc', '.docx', '.pdf', 
            '.ppt', '.pptx', '.xls', '.xlsx', '.xml', '.html', 
            '.json', '.jsonl'
        ]
        
        file_ext = os.path.splitext(document_path)[1].lower()
        if file_ext not in valid_extensions:
            raise ValueError(f"Unsupported file format. Use: {valid_extensions}")

        return True

    def process_document(self, document_path: str) -> Optional[Dict[str, str]]:
        try:
            self.validate_document(document_path)

            print(f"Processing document: {document_path}")
            print("Uploading document...")
            file_response = self.client.file.upload_file(document_path)
            print(f"Upload successful!")

            print("Extracting information...")
            response = self.client.add_table_rows(
                table_type=t.TableType.ACTION,
                request=t.MultiRowAddRequest(
                    table_id="news_summarization",
                    data=[{"document": file_response.uri}],
                    stream=False,
                ),
            )

            results = {
                "summary": response.rows[0].columns["summary"].text,
                "tag": response.rows[0].columns["tag"].text
            }
            return results

        except Exception as e:
            print(f"Error: {str(e)}")
            return None


def process_folder(folder_path: str, processor: DocumentProcessor) -> None:
    """Process all documents in a folder"""
    if not os.path.exists(folder_path):
        print(f"Folder not found: {folder_path}")
        return

    valid_extensions = [
        '.csv', '.tsv', '.txt', '.md', '.doc', '.docx', '.pdf', 
        '.ppt', '.pptx', '.xls', '.xlsx', '.xml', '.html', 
        '.json', '.jsonl'
    ]
    
    results = []
    for filename in os.listdir(folder_path):
        if any(filename.lower().endswith(ext) for ext in valid_extensions):
            document_path = os.path.join(folder_path, filename)
            result = processor.process_document(document_path)
            if result:
                results.append({
                    "filename": filename,
                    **result
                })

    # Print results in a formatted way
    print("\nProcessing Results:")
    print("-" * 50)
    for result in results:
        print(f"File: {result['filename']}")
        print(f"Summary: {result['summary']}")
        print(f"Tag: {result['tag']}")
        print("-" * 50)


def main():
    # Set up argument parser
    parser = argparse.ArgumentParser(description='Process documents using JamAIBase')
    parser.add_argument('--project-id', required=True, help='Your JamAIBase project ID')
    parser.add_argument('--pat', required=True, help='Your Personal Access Token')
    parser.add_argument('--input', required=True, help='Path to document file or folder')

    args = parser.parse_args()

    # Initialize processor
    processor = DocumentProcessor(args.project_id, args.pat)

    # Process input
    if os.path.isfile(args.input):
        # Single file processing
        result = processor.process_document(args.input)
        if result:
            print("\nResults:")
            print("-" * 50)
            print(f"Summary: {result['summary']}")
            print(f"Tag: {result['tag']}")
            print("-" * 50)
    else:
        # Folder processing
        process_folder(args.input, processor)


if __name__ == "__main__":
    main()
```

### How to Run

1. Save the code as `news_processor.py`&#x20;
2. Install the required package:&#x20;

```bash
pip install jamaibase
```

3. Run for a single news:

```bash
python news_processor.py --project-id "your-project-id" --pat "your-pat" --input "path/to/news.txt"
```

4. Run for a folder of news:

```bash
python news_processor.py --project-id "your-project-id" --pat "your-pat" --input "path/to/news/folder"
```

### Example Output

```
Processing document: ./jamai_test/test_doc.docx
Uploading document...
Upload successful!
Extracting information...

Results:
--------------------------------------------------
Summary: 1. President Trump announced plans to visit Malaysia, Japan, and South Korea, aiming for a fair deal with China and a meeting with President Xi Jinping during an Asia-Pacific summit.  
2. The trip marks his first to the region since his second term began, with Japan expected from Oct 27 after a regional conference in Malaysia.  
3. Trump emphasized positive relations with Xi and expressed optimism despite ongoing trade tensions.
Tag: 1. Trump Asia Trip  
2. US China Relations  
3. Presidential Visit
--------------------------------------------------
```

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FVWGp3BDhp52ZoNj50EQY%2Fnews_summarization_screenshot.png?alt=media&amp;token=aecb40ac-3d29-40a4-97a4-e757c1d89960" alt=""><figcaption><p>The news summarization table with the uploaded documents.</p></figcaption></figure>

This standalone example provides a complete, working implementation that you can use as a starting point for your own projects or modify according to your needs.


# Quick Start with Chat Table

Build Intelligent Conversational Applications in Minutes with JamAI Base Chat Tables

## 1. Introduction

This guide will help you quickly get started with JamAI Base chat tables using the Python SDK. You'll learn how to create a simple chat application similar to the example provided.

## 2. Prerequisites

* Python 3.11 or above
* JamAI Base account
* Personal Access Token (PAT)
* Project ID

## 3. Installation

```bash
pip install jamaibase
```

## 4. Basic Setup

Get your Personal Access Token (PAT) here:

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FKSAVd8tlm3hnNQrY78Xx%2FSlide1.PNG?alt=media&amp;token=f8132eaf-7b1a-4f66-9361-10b1ac07e955" alt=""><figcaption><p>How to generate PAT</p></figcaption></figure>

Get your Project ID here:

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FuWoF11Sho8o6NVAAgJvj%2FProjectID.png?alt=media&amp;token=6354535b-0638-4237-8228-7f4d8393b78d" alt=""><figcaption><p>How to get Project ID</p></figcaption></figure>

```python
from jamaibase import JamAI, types as t
import os

# Initialize JamAI client
jamai = JamAI(
    project_id="your_project_id",  # Replace with your project ID
    token="your_pat_token"         # Replace with your PAT
)
```

## 5. Creating a Chat Agent (UI First)

Go to JamAI Base web interface chat table tab

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FqNXf1675B1Y0w87xa0nm%2Fimage.png?alt=media&amp;token=ec208ffc-7493-4e42-93c6-617907de2c7a" alt="" width="375"><figcaption></figcaption></figure>

Navigate to "Agents" section, click **`+`** to create new agent.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FyKujFZwQxBwszA0GiDvs%2Fimage.png?alt=media&amp;token=88950fe6-967d-4e18-9264-cf00821f861d" alt="" width="375"><figcaption></figcaption></figure>

Name the new agent `"example_agent"`and select LLM model.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fz8RydKAbBPzfWcr0igYz%2Fimage.png?alt=media&amp;token=67b5fe26-2740-46ce-85cb-60214a02d320" alt="" width="375"><figcaption></figcaption></figure>

You can further configure your agent in UI but it will not be covered in this tutorial.

Example of some configuration you can do:

* Set system prompt
* Set multiple parallel agent
* Configure RAG settings
* Set up knowledge base

## 6. Creating Chat Sessions

Each chat session is created by duplicating your base agent table. Here's how to do it:

```python
def create_new_chat():
    timestamp = int(time.time())
    new_table_id = f"Chat_{timestamp}"
    
    try:
        jamai.table.duplicate_table(
            table_type=t.TableType.CHAT,
            table_id_src="example_agent",    # Your base agent ID
            table_id_dst=new_table_id,
            include_data=True,
            create_as_child=True
        )
        return new_table_id
    except Exception as e:
        print(f"Error creating new chat: {str(e)}")
        return None
```

## 7. Basic Chat Interaction

```python
# Send a message and get response
def chat_interaction(table_id, user_message):
    response = jamai.table.add_table_rows(
        table_type=t.TableType.CHAT,
        request=p.RowAddRequest(
            table_id=table_id,
            data=[{"User": user_message}],
            stream=True  # Set to False for non-streaming response
        )
    )
    
    # For streaming response
    full_response = ""
    for chunk in response:
        if chunk.output_column_name == 'AI':
            full_response += chunk.content
    
    return full_response
```

## 8. Simple Implementation Example

```python
# Create a new chat session
chat_id = create_new_chat()

if chat_id:
    # Send a message
    response = chat_interaction(chat_id, "Hello, how are you?")
    print("AI:", response)
```

## 9. Key Features

* **Streaming Responses**: Use `stream=True` for real-time responses
* **Session Management**: Each chat creates a new table instance
* **Inheritance**: New chats inherit settings from the base agent
* **History Preservation**: Chat history is maintained in the table

## 10. Best Practices

* Create a new chat session for each conversation
* Handle exceptions during table creation
* Store chat IDs for session management
* Clean up unused chat tables periodically

## 11. Complete Runnable Example

Here's a complete, standalone example that you can copy, paste, and run:

```python
import streamlit as st
from jamaibase import JamAI, types as t
import time
import os
from dotenv import load_dotenv

# Load environment variables (optional)
load_dotenv()

# Constants
PROJECT_ID = "your_project_id"  # Replace with your project ID
PAT = "your_pat_token"          # Replace with your PAT
TABLE_TYPE = t.TableType.CHAT
OPENER = "Hello! How can I help you today?"

# Initialize JamAI
jamai = JamAI(project_id=PROJECT_ID, token=PAT)

def create_new_chat():
    timestamp = int(time.time())
    new_table_id = f"Chat_{timestamp}"
    try:
        jamai.table.duplicate_table(
            table_type=TABLE_TYPE,
            table_id_src="example_agent",  # Replace with your agent ID
            table_id_dst=new_table_id,
            include_data=True,
            create_as_child=True
        )
        return new_table_id
    except Exception as e:
        st.error(f"Error creating new chat: {str(e)}")
        return None

def main():
    st.title("Simple Chat Demo")

    # Initialize session state
    if "table_id" not in st.session_state:
        new_table_id = create_new_chat()
        st.session_state.table_id = new_table_id
        st.session_state.messages = [{"role": "assistant", "content": OPENER}]

    # Create New Chat button
    if st.button("New Chat"):
        new_table_id = create_new_chat()
        if new_table_id:
            st.session_state.table_id = new_table_id
            st.session_state.messages = [{"role": "assistant", "content": OPENER}]
            st.rerun()

    # Display chat messages
    for message in st.session_state.messages:
        with st.chat_message(message["role"]):
            st.write(message["content"])

    # Chat input
    if prompt := st.chat_input("Type your message here"):
        # Display user message
        with st.chat_message("user"):
            st.write(prompt)
        st.session_state.messages.append({"role": "user", "content": prompt})

        # Get AI response
        with st.chat_message("assistant"):
            message_placeholder = st.empty()
            full_response = ""

            # Stream the response
            for chunk in jamai.table.add_table_rows(
                table_type=TABLE_TYPE,
                request=t.MultiRowAddRequest(
                    table_id=st.session_state.table_id,
                    data=[{"User": prompt}],
                    stream=True
                )
            ):                
                if chunk.output_column_name == 'AI':
                    full_response += chunk.content
                    message_placeholder.write(full_response + "▌")
            
            message_placeholder.write(full_response)
            st.session_state.messages.append({"role": "assistant", "content": full_response})

if __name__ == "__main__":
    main()
```

### How to Run:

1. Save the code in a file (e.g., `chat_demo.py`)
2. Install required packages:

```bash
pip install streamlit jamaibase python-dotenv
```

3. Replace the following values:
   * `your_project_id` with your actual Project ID
   * `your_pat_token` with your Personal Access Token
   * `example_agent` with your agent ID created in the UI
4. Run the application:

```bash
streamlit run chat_demo.py
```

This example provides:

* A simple chat interface
* Streaming responses
* New chat session creation
* Basic error handling

Note: Make sure you have created your agent in the JamAI Base UI before running this code, as it relies on duplicating an existing agent table.

This example serves as a great starting point for building more complex chat applications with JamAI Base.


# Quick Start: Knowledge Table File Upload

Prepare your file for RAG

## 1. Introduction

This guide demonstrates how to use JamAI Base SDK to upload and embed files into Knowledge Tables for AI-powered document processing and retrieval.

### What are Knowledge Tables?

Knowledge Tables are specialized tables in JamAI Base that provide hybrid-search capabilities through both full-text search (FTS) and vector embeddings:

1. **Search Capabilities**:
   * **Full-Text Search (FTS)**: Traditional keyword-based search for exact and partial matches
   * **Semantic Search**: Vector embedding-based search for meaning and context
2. **Document Processing**:
   * Automatically chunks documents into manageable segments
   * Generates vector embeddings for semantic understanding
   * Indexes content for full-text search
   * Preserves document structure(tables, layouts, etc) and metadata
3. **Use Cases**:
   * Document retrieval using both keywords and semantic meaning
   * Question-answering agent
   * Content recommendation
   * Knowledge base search and discovery

### Supported File Types

The following file formats are supported:

* Text files: `.txt`, `.md`, `.csv`, `.tsv`
* Documents: `.doc`, `.docx`, `.pdf`
* Presentations: `.ppt`, `.pptx`
* Spreadsheets: `.xls`, `.xlsx`
* Markup/Data: `.xml`, `.html`, `.json`, `.jsonl`

### Prerequisites

Before starting, you'll need:

* Python 3.10 or higher
* Project ID and Personal Access Token (PAT)
* Documents to process

## 2. Installation and Setup

### Installing Required Packages

```bash
pip install jamaibase python-dotenv
```

### Basic Configuration

```python
from jamaibase import JamAI, protocol as p
from dotenv import load_dotenv
import os

# Load environment variables
load_dotenv()

PROJECT_ID = "your_project_id"
PAT = os.getenv("PAT")

client = JamAI(
    project_id=PROJECT_ID,
    token=PAT
)
```

## 3. Creating Your Knowledge Table

1. Navigate to your JamAI Base knowledge tables tab
2. Create a new knowledge table
3. Note down the table ID for later use

## 4. Implementation

### 4.1 Complete Document Uploader Class

```python
from typing import Optional, Dict, List
import os

class DocumentUploader:
    def __init__(self, project_id: str, pat: str):
        """Initialize the document uploader"""
        self.client = JamAI(
            project_id=project_id,
            token=pat
        )
        
    def validate_file(self, file_path: str) -> bool:
        """Validate if file exists and has supported format"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"File not found: {file_path}")
            
        supported_types = [
            '.csv', '.tsv', '.txt', '.md',
            '.doc', '.docx', '.pdf',
            '.ppt', '.pptx',
            '.xls', '.xlsx',
            '.xml', '.html',
            '.json', '.jsonl'
        ]
        file_ext = os.path.splitext(file_path)[1].lower()
        
        if file_ext not in supported_types:
            raise ValueError(
                f"Unsupported file format. Supported formats: {', '.join(supported_types)}"
            )
            
        return True
    
    def get_mime_type(self, file_path: str) -> str:
        """Get MIME type of the file"""
        mime_types = {
            '.csv': 'text/csv',
            '.tsv': 'text/tab-separated-values',
            '.txt': 'text/plain',
            '.md': 'text/markdown',
            '.doc': 'application/msword',
            '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            '.pdf': 'application/pdf',
            '.ppt': 'application/vnd.ms-powerpoint',
            '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            '.xls': 'application/vnd.ms-excel',
            '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            '.xml': 'application/xml',
            '.html': 'text/html',
            '.json': 'application/json',
            '.jsonl': 'application/jsonl'
        }
        
        file_ext = os.path.splitext(file_path)[1].lower()
        return mime_types.get(file_ext, 'application/octet-stream')
    
    def get_optimal_chunk_settings(self, file_path: str) -> Dict[str, int]:
        """Determine optimal chunk settings based on file type"""
        file_ext = os.path.splitext(file_path)[1].lower()
        
        settings = {
            # Text-based documents
            '.txt': {'size': 1000, 'overlap': 200},
            '.md': {'size': 1000, 'overlap': 200},
            '.csv': {'size': 800, 'overlap': 150},
            '.tsv': {'size': 800, 'overlap': 150},
            
            # Rich text documents
            '.doc': {'size': 1200, 'overlap': 250},
            '.docx': {'size': 1200, 'overlap': 250},
            '.pdf': {'size': 1500, 'overlap': 300},
            
            # Presentations
            '.ppt': {'size': 1000, 'overlap': 200},
            '.pptx': {'size': 1000, 'overlap': 200},
            
            # Spreadsheets
            '.xls': {'size': 800, 'overlap': 150},
            '.xlsx': {'size': 800, 'overlap': 150},
            
            # Markup/structured documents
            '.xml': {'size': 1000, 'overlap': 200},
            '.html': {'size': 1000, 'overlap': 200},
            '.json': {'size': 800, 'overlap': 150},
            '.jsonl': {'size': 800, 'overlap': 150},
            
            # Default settings
            'default': {'size': 1000, 'overlap': 200}
        }
        
        return settings.get(file_ext, settings['default'])
    
    def upload_document(self, file_path: str, table_id: str, 
                       custom_chunk_size: Optional[int] = None,
                       custom_chunk_overlap: Optional[int] = None) -> bool:
        """Upload single document with optimized settings"""
        try:
            # Validate file
            self.validate_file(file_path)
            
            # Get file information
            file_name = os.path.basename(file_path)
            file_size = os.path.getsize(file_path)
            mime_type = self.get_mime_type(file_path)
            
            # Get chunk settings
            settings = self.get_optimal_chunk_settings(file_path)
            chunk_size = custom_chunk_size or settings['size']
            chunk_overlap = custom_chunk_overlap or settings['overlap']
            
            print(f"Uploading: {file_name}")
            print(f"File type: {mime_type}")
            print(f"File size: {file_size / 1024:.2f} KB")
            print(f"Chunk size: {chunk_size}, Overlap: {chunk_overlap}")
            
            # Upload and embed file
            response = self.client.table.embed_file(
                file_path=file_path,
                table_id=table_id,
                chunk_size=chunk_size,
                chunk_overlap=chunk_overlap,
            )
            
            print(f"Upload successful: {file_name}")
            return True
            
        except Exception as e:
            print(f"Error uploading {file_path}: {str(e)}")
            return False
```

## 5. Complete Standalone Script

Save this as `knowledge_uploader.py`:

```python
import os
import argparse
from jamaibase import JamAI, protocol as p
from typing import Optional, Dict, List
from dotenv import load_dotenv

class DocumentUploader:
    def __init__(self, project_id: str, pat: str):
        """Initialize the document uploader"""
        self.client = JamAI(
            project_id=project_id,
            token=pat
        )
        
    def validate_file(self, file_path: str) -> bool:
        """Validate if file exists and has supported format"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"File not found: {file_path}")
            
        supported_types = [
            '.csv', '.tsv', '.txt', '.md',
            '.doc', '.docx', '.pdf',
            '.ppt', '.pptx',
            '.xls', '.xlsx',
            '.xml', '.html',
            '.json', '.jsonl'
        ]
        file_ext = os.path.splitext(file_path)[1].lower()
        
        if file_ext not in supported_types:
            raise ValueError(
                f"Unsupported file format. Supported formats: {', '.join(supported_types)}"
            )
            
        return True
    
    def get_mime_type(self, file_path: str) -> str:
        """Get MIME type of the file"""
        mime_types = {
            '.csv': 'text/csv',
            '.tsv': 'text/tab-separated-values',
            '.txt': 'text/plain',
            '.md': 'text/markdown',
            '.doc': 'application/msword',
            '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            '.pdf': 'application/pdf',
            '.ppt': 'application/vnd.ms-powerpoint',
            '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            '.xls': 'application/vnd.ms-excel',
            '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            '.xml': 'application/xml',
            '.html': 'text/html',
            '.json': 'application/json',
            '.jsonl': 'application/jsonl'
        }
        
        file_ext = os.path.splitext(file_path)[1].lower()
        return mime_types.get(file_ext, 'application/octet-stream')
    
    def get_optimal_chunk_settings(self, file_path: str) -> Dict[str, int]:
        """Determine optimal chunk settings based on file type"""
        file_ext = os.path.splitext(file_path)[1].lower()
        
        settings = {
            # Text-based documents
            '.txt': {'size': 1000, 'overlap': 200},
            '.md': {'size': 1000, 'overlap': 200},
            '.csv': {'size': 800, 'overlap': 150},
            '.tsv': {'size': 800, 'overlap': 150},
            
            # Rich text documents
            '.doc': {'size': 1200, 'overlap': 250},
            '.docx': {'size': 1200, 'overlap': 250},
            '.pdf': {'size': 1500, 'overlap': 300},
            
            # Presentations
            '.ppt': {'size': 1000, 'overlap': 200},
            '.pptx': {'size': 1000, 'overlap': 200},
            
            # Spreadsheets
            '.xls': {'size': 800, 'overlap': 150},
            '.xlsx': {'size': 800, 'overlap': 150},
            
            # Markup/structured documents
            '.xml': {'size': 1000, 'overlap': 200},
            '.html': {'size': 1000, 'overlap': 200},
            '.json': {'size': 800, 'overlap': 150},
            '.jsonl': {'size': 800, 'overlap': 150},
            
            # Default settings
            'default': {'size': 1000, 'overlap': 200}
        }
        
        return settings.get(file_ext, settings['default'])
    
    def upload_document(self, file_path: str, table_id: str, 
                       custom_chunk_size: Optional[int] = None,
                       custom_chunk_overlap: Optional[int] = None) -> bool:
        """Upload single document with optimized settings"""
        try:
            # Validate file
            self.validate_file(file_path)
            
            # Get file information
            file_name = os.path.basename(file_path)
            file_size = os.path.getsize(file_path)
            mime_type = self.get_mime_type(file_path)
            
            # Get chunk settings
            settings = self.get_optimal_chunk_settings(file_path)
            chunk_size = custom_chunk_size or settings['size']
            chunk_overlap = custom_chunk_overlap or settings['overlap']
            
            print(f"Uploading: {file_name}")
            print(f"File type: {mime_type}")
            print(f"File size: {file_size / 1024:.2f} KB")
            print(f"Chunk size: {chunk_size}, Overlap: {chunk_overlap}")
            
            # Upload and embed file
            response = self.client.table.embed_file(
                file_path=file_path,
                table_id=table_id,
                chunk_size=chunk_size,
                chunk_overlap=chunk_overlap,
            )
            
            print(f"Upload successful: {file_name}")
            return True
            
        except Exception as e:
            print(f"Error uploading {file_path}: {str(e)}")
            return False

def process_folder(folder_path: str, uploader: DocumentUploader, 
                  table_id: str, chunk_size: Optional[int] = None,
                  chunk_overlap: Optional[int] = None) -> tuple[List[str], List[str]]:
    """Process all documents in a folder"""
    successful = []
    failed = []
    
    for filename in os.listdir(folder_path):
        file_path = os.path.join(folder_path, filename)
        if os.path.isfile(file_path):
            if uploader.upload_document(
                file_path,
                table_id,
                chunk_size,
                chunk_overlap
            ):
                successful.append(filename)
            else:
                failed.append(filename)
    
    return successful, failed

def main():
    # Set up argument parser
    parser = argparse.ArgumentParser(
        description='Upload documents to JamAI Base Knowledge Table'
    )
    parser.add_argument('--project-id', required=True, 
                       help='Your JamAI Base project ID')
    parser.add_argument('--pat', required=True, 
                       help='Your Personal Access Token')
    parser.add_argument('--table-id', required=True, 
                       help='Knowledge Table ID')
    parser.add_argument('--input', required=True, 
                       help='Path to file or folder')
    parser.add_argument('--chunk-size', type=int, 
                       help='Custom chunk size')
    parser.add_argument('--chunk-overlap', type=int, 
                       help='Custom chunk overlap')
    
    args = parser.parse_args()

    # Initialize uploader
    uploader = DocumentUploader(args.project_id, args.pat)

    # Process input
    if os.path.isfile(args.input):
        # Single file processing
        success = uploader.upload_document(
            args.input,
            args.table_id,
            args.chunk_size,
            args.chunk_overlap
        )
        print(f"\nFinal Status: Upload {'successful' if success else 'failed'}")
    else:
        # Folder processing
        successful, failed = process_folder(
            args.input,
            uploader,
            args.table_id,
            args.chunk_size,
            args.chunk_overlap
        )
        
        print("\nUpload Summary:")
        print(f"Successful: {len(successful)} files")
        print(f"Failed: {len(failed)} files")
        
        if successful:
            print("\nSuccessfully uploaded files:")
            for file in successful:
                print(f"- {file}")
                
        if failed:
            print("\nFailed uploads:")
            for file in failed:
                print(f"- {file}")

if __name__ == "__main__":
    main()
```

## 6. Usage Examples

### Single File Upload

```bash
python knowledge_uploader.py \
    --project-id "your_project_id" \
    --pat "your_pat" \
    --table-id "your_table_id" \
    --input "path/to/document.pdf"
```

### Folder Upload

```bash
python knowledge_uploader.py \
    --project-id "your_project_id" \
    --pat "your_pat" \
    --table-id "your_table_id" \
    --input "path/to/documents/folder"
```

### Custom Chunk Settings

```bash
python knowledge_uploader.py \
    --project-id "your_project_id" \
    --pat "your_pat" \
    --table-id "your_table_id" \
    --input "path/to/document.pdf" \
    --chunk-size 2000 \
    --chunk-overlap 400
```

## 7. Example Output

```
Uploading: document.pdf
File type: application/pdf
File size: 1024.50 KB
Chunk size: 1500, Overlap: 300
Upload successful: document.pdf

Final Status: Upload successful
```

For folder processing:

```
Upload Summary:
Successful: 3 files
Failed: 1 files

Successfully uploaded files:
- document1.pdf
- document2.docx
- presentation.pptx

Failed uploads:
- invalid_format.xyz
```

## 8. Best Practices

1. **File Handling**
   * Always validate files before upload
   * Use appropriate chunk sizes for different document types
   * Handle large files appropriately
2. **Performance**
   * Reuse the client instance
   * Process files in batches
   * Consider implementing rate limiting for large batches
3. **Error Handling**
   * Validate input files
   * Handle network errors gracefully
   * Provide meaningful error messages
4. **Security**
   * Use environment variables for credentials
   * Validate file content when necessary
   * Implement proper access controls

This implementation provides a robust foundation for uploading documents to JamAI Base Knowledge Tables, with support for all official file types and optimal processing settings for each format.


# TypeScript SDK Documentation

JamAI Base is a **backend-as-a-service for AI applications**. You define tables with columns that automatically process data through AI pipelines.

### Column Types

| Type      | Purpose              | Example                                        |
| --------- | -------------------- | ---------------------------------------------- |
| **Input** | Your data            | `dtype: "str"`, `"file"`, `"image"`, `"audio"` |
| **LLM**   | AI generates content | `gen_config: { model: "...", prompt: "..." }`  |

**Data Types**: `str` (text), `file` (generic), `image` (.jpeg/.jpg/.png/.gif/.webp), `audio` (.mp3/.wav)

### How It Works

{% stepper %}
{% step %}
Define a table with input + AI columns
{% endstep %}

{% step %}
Add a row with input data
{% endstep %}

{% step %}
AI columns auto-generate based on your prompts
{% endstep %}

{% step %}
Read the completed row with all outputs
{% endstep %}
{% endstepper %}

### Available Models

```typescript
// List all chat models
const chatModels = await jamai.llm.modelNames({
  capabilities: ["chat"],
});
console.log(chatModels.slice(0, 5)); // ['openai/gpt-4o', 'openai/gpt-4o-mini', ...]

// List embedding models
const embedModels = await jamai.llm.modelNames({
  capabilities: ["embed"],
});
console.log(embedModels); // ['ellm/BAAI/bge-m3', ...]

// Get full model info
const models = await jamai.llm.modelInfo();
for (const m of models.data.slice(0, 3)) {
  console.log(`${m.id}: context=${m.context_length || "N/A"}`);
}
```

### Row Structure

Every row returned from `listRows` contains:

```typescript
{
    'ID': 'uuid-string',           // Use for updates/deletes
    'Updated at': '2026-01-07...',  // Timestamp
    'column_name': { value: 'actual_data' },  // WRAPPED!
    ...
}
```

{% hint style="warning" %}
**Value Wrapping Context:**

* **SDK reads** (`listRows`, `getRow`): Values ARE wrapped → use `row['col'].value`
* Access values using: `row['column_name'].value` or the helper function below
  {% endhint %}

***

## QUICK REFERENCE

```typescript
// INSTALL
npm install jamaibase

// INITIALIZE
import JamAI from "jamaibase";

const jamai = new JamAI({
    token: "YOUR_PAT",
    projectId: "YOUR_PROJECT_ID"
});

// CRITICAL: Values are wrapped - ALWAYS extract
function getValue(field: any): any {
    if (field && typeof field === 'object' && 'value' in field) {
        return field.value;
    }
    return field;
}

// MAX LIMIT IS 100 - use pagination for more
```

***

## 1. SETUP

### Install

Node.js Version (>= 16.x)

```bash
npm install jamaibase
```

### Get Credentials

{% stepper %}
{% step %}
Sign up: <https://cloud.jamaibase.com/>
{% endstep %}

{% step %}
Create project
{% endstep %}

{% step %}
Get PAT key: Click on your user name on top right corner > [⚙ Account Settings](https://cloud.jamaibase.com/settings/account) > Create a Personal Access Token
{% endstep %}

{% step %}
Get Project ID: from project URL
{% endstep %}
{% endstepper %}

### Initialize Client

To start using the JamAI SDK, you need to create an instance of the client by passing a configuration object. Below are the configurable parameters:

| Parameter                 | Type          | Required | Description                                                                                                           |
| ------------------------- | ------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
| `token`                   | string        | Yes\*    | Your Personal Access Token (PAT) to authenticate API requests.                                                        |
| `projectId`               | string        | Yes\*    | The ID of the Jamaibase project you want to interact with.                                                            |
| `baseURL`                 | string        | No       | Set a custom API endpoint (useful for self-hosted/OSS instances).                                                     |
| `maxRetries`              | number        | No       | Maximum number of times to retry a failed request (default: 0, i.e., no retries).                                     |
| `timeout`                 | number        | No       | Request timeout in milliseconds.                                                                                      |
| `httpClient`              | AxiosInstance | No       | Provide a custom Axios instance if you need advanced request handling.                                                |
| `dangerouslyAllowBrowser` | boolean       | No       | If `true`, allows use in browser environments (only for advanced/OSS use). **Not recommended due to security risks.** |
| `userId`                  | string        | No       | Optionally set a user ID for multi-user or impersonation scenarios.                                                   |

**Note**: Both `token` and `projectId` are required unless `baseURL` is specified for OSS/self-hosted use, in which case you may override authentication per your server's configuration.

#### Example Usage

```typescript
import JamAI from "jamaibase";

// Standard usage with required credentials
const jamai = new JamAI({
  token: "YOUR_PAT",
  projectId: "YOUR_PROJECT_ID",
});

// Specify a custom API endpoint (for OSS/self-hosted setups)
const jamaiCustom = new JamAI({
  baseURL: "http://localhost:6969",
  token: "YOUR_PAT",
  projectId: "YOUR_PROJECT_ID",
});

// With optional advanced settings
const jamaiWithOptions = new JamAI({
  token: "YOUR_PAT",
  projectId: "YOUR_PROJECT_ID",
  maxRetries: 3,
  timeout: 10000,
});
```

#### Example .env File

You may wish to keep your credentials out of your source code by using environment variables:

```env
JAMAI_API_KEY=your_PAT
JAMAI_PROJECT_ID=your_project_id

# Optional
JAMAI_API_BASE=https://api.jamaibase.com
```

> ⚠️ You must ensure you pass the config to the SDK in your code when using environment variables:
>
> ```typescript
> const jamai = new JamAI({
>   token: process.env.JAMAI_API_KEY!,
>   projectId: process.env.JAMAI_PROJECT_ID!,
> });
> ```

***

## 2. TABLE TYPES

| Type        | Use Case                       | Create Method            |
| ----------- | ------------------------------ | ------------------------ |
| `action`    | AI chains, document processing | `createActionTable()`    |
| `knowledge` | RAG, embeddings, vector search | `createKnowledgeTable()` |
| `chat`      | Conversational AI with context | `createChatTable()`      |

***

## 3. ACTION TABLES (Most Common)

### Create

```typescript
const table = await jamai.table.createActionTable({
  id: "my_table",
  cols: [
    // Input column
    {
      id: "input",
      dtype: "str",
    },

    // File input
    {
      id: "image",
      dtype: "file",
    },

    // LLM output column
    {
      id: "output",
      dtype: "str",
      gen_config: {
        model: "openai/gpt-4o-mini",
        system_prompt: "You are helpful.",
        prompt: "Process: ${input}\nImage: ${image}",
        temperature: 0.7,
        max_tokens: 500,
      },
    },
  ],
});
```

### Column Reference Syntax

Use `${column_name}` in prompts to reference other columns. At runtime, each reference is replaced with the corresponding cell value from the current row.

```typescript
// Example prompt
prompt: 'Translate "${input}" into Italian:';

// If input column contains "Good morning", actual prompt sent to LLM:
// "Translate \"Good morning\" into Italian:"
```

### How LLM Columns Work

{% stepper %}
{% step %}
Gather prompts — System Prompt and Prompt (which can reference upstream columns).
{% endstep %}

{% step %}
Optional RAG — Augment prompt with references from a Knowledge Table.
{% endstep %}

{% step %}
Send to LLM — With your chosen generation settings (model, temperature, max\_tokens).
{% endstep %}

{% step %}
Write response — Model's response becomes the cell value.
{% endstep %}
{% endstepper %}

### LLM Generation Settings

| Parameter       | Description                                                             |
| --------------- | ----------------------------------------------------------------------- |
| `model`         | LLM model to use (e.g., `openai/gpt-4o-mini`)                           |
| `system_prompt` | Passed as-is as system message. Define role, style, global instructions |
| `prompt`        | Main user message with `${column}` references                           |
| `temperature`   | Controls randomness (0.0-2.0)                                           |
| `max_tokens`    | Maximum output length                                                   |

### RAG (Retrieval Augmented Generation)

Link an LLM column to a Knowledge Table for grounded responses. See [Section 9](file:///#9-knowledge-tables-rag) for full Knowledge Table setup.

{% stepper %}
{% step %}
Formulate query — LLM generates retrieval query from your Prompt
{% endstep %}

{% step %}
Retrieve — Fetch relevant rows from Knowledge Table
{% endstep %}

{% step %}
Rerank — Optional reranking model (RRF Ranker by default)
{% endstep %}

{% step %}
Inject — Top-k references added to prompt
{% endstep %}

{% step %}
Cite — Optional inline citations: `[@ref0; @ref1; @ref2]`
{% endstep %}
{% endstepper %}

```typescript
{
    id: "answer",
    dtype: "str",
    gen_config: {
        model: "openai/gpt-4o-mini",
        prompt: "${question}",
        rag_params: {
            table_id: "my_knowledge_table",  // Must exist (see Section 9)
            k: 3,  // Number of references to inject
            // reranking_model: "...",  // Optional
        },
        max_tokens: 500
    }
}
```

### Prompting Tips

Separate column references using XML tags or Markdown headings:

```typescript
// XML tags (recommended)
prompt: `
<user-query>
\${input}
</user-query>

Translate user query into Italian.
`;

// Markdown headings
prompt: `

# User Query
\${input}

# Instruction
Translate user query into Italian.
`;
```

***

## 4. ADD ROWS

### Non-Streaming (Wait for Complete Response)

```typescript
const response = await jamai.table.addRow({
  table_type: "action",
  table_id: "my_table",
  data: [{ input: "Hello world" }, { input: "Goodbye world" }],
  concurrent: false,
});

// Get LLM output
console.log(
  response?.rows?.[0]?.columns["output"]?.choices[0]?.message?.content
);
```

### Streaming (Real-time Output)

```typescript
const stream = await jamai.table.addRowStream({
  table_type: "action",
  table_id: "my_table",
  data: [{ input: "Hello" }],
});

// Format: Async iterator-style for cleaner usage
for await (const value of stream) {
  if (value.object === "gen_table.completion.chunk") {
    console.log(value.choices?.[0]?.message?.content);
  }
}
```

### With File Upload

```typescript
// Upload file first
const fileResponse = await jamai.file.uploadFile({
  file_path: "/path/to/image.png",
});

// Use URI in row
await jamai.table.addRow({
  table_type: "action",
  table_id: "my_table",
  data: [
    {
      image: fileResponse.uri,
      input: "Describe this",
    },
  ],
  concurrent: false,
});
```

### Get Row ID After Adding (Non-Streaming)

```typescript
const response = await jamai.table.addRow({
  table_type: "action",
  table_id: "my_table",
  data: [{ input: "Hello" }],
  concurrent: false,
});

// Get the row ID for later updates/deletes
const rowId = response.rows[0]?.row_id;
console.log(`Created row: ${rowId}`);

// Get LLM output
const output =
  response.rows[0]?.columns["output"]?.choices[0]?.message?.content;
```

### Get Row ID After Adding (Streaming)

```typescript
const stream = await jamai.table.addRowStream({
  table_type: "action",
  table_id: "my_table",
  data: [{ input: "Hello" }],
});

let rowId: string | null = null;

for await (const value of stream) {
  if (value && "row_id" in value && value.row_id) {
    rowId = value.row_id;
  }

  if (
    value.object === "gen_table.completion.chunk" &&
    value.choices?.[0]?.message?.content
  ) {
    console.log(value.choices[0].message.content);
  }
}

console.log(`\nRow ID: ${rowId}`);
```

***

## 5. READ ROWS

### Basic List

```typescript
const rows = await jamai.table.listRows({
  table_type: "action",
  table_id: "my_table",
  offset: 0,
  limit: 100, // MAX IS 100!
});

for (const row of rows.items) {
  // IMPORTANT: Extract value from wrapper
  const value = row["input"].value; // or use getValue()
  console.log(value);
}
```

### With WHERE Filter

```typescript
// Syntax: "column" (double quotes) = 'value' (single quotes)
const rows = await jamai.table.listRows({
  table_type: "action",
  table_id: "my_table",
  where: "\"status\" = 'active'",
});

// LIKE pattern
// where: '"name" LIKE \'%Smith%\''

// AND conditions
// where: '"status" = \'active\' AND "type" = \'premium\''
```

### Select Specific Columns

```typescript
const rows = await jamai.table.listRows({
  table_type: "action",
  table_id: "my_table",
  columns: ["input", "output"], // ID, Updated at always included
});
```

### Full-Text Search

```typescript
const rows = await jamai.table.listRows({
  table_type: "action",
  table_id: "my_table",
  search_query: "machine learning",
});
```

### Pagination (REQUIRED for >100 rows)

```typescript
import { TableTypes } from "@/resources/gen_tables/tables";
async function getAllRows(tableId: string, tableType: TableTypes = "action") {
  const allRows: any[] = [];
  let offset = 0;

  while (true) {
    const response = await jamai.table.listRows({
      table_type: tableType,
      table_id: tableId,
      offset: offset,
      limit: 100,
    });

    allRows.push(...response.items);

    if (response.items.length < 100) {
      break;
    }

    offset += 100;
  }

  return allRows;
}
```

### Get Single Row

```typescript
const row = await jamai.table.getRow({
  table_type: "action",
  table_id: "my_table",
  row_id: "row-uuid",
});
```

***

## 6. UPDATE ROWS

### Basic Update

```typescript
await jamai.table.updateRows({
  table_type: "action",
  table_id: "my_table",
  data: {
    "row-uuid-1": { column1: "new_value" },
    "row-uuid-2": { column1: "value", column2: "value2" },
  },
});
```

### Regenerate Rows (Non-Streaming)

```typescript
// List rows to get row IDs
const rows = await jamai.table.listRows({
  table_type: "action",
  table_id: "my_table",
});

const rowIds = rows.items.map((item) => item.ID);

// Regenerate rows
const response = await jamai.table.regenRow({
  table_type: "action",
  table_id: "my_table",
  row_ids: rowIds,
  concurrent: true,
});
```

### Regenerate Rows (Streaming)

```typescript
const stream = await jamai.table.regenRowStream({
  table_type: "action",
  table_id: "my_table",
  row_ids: rowIds,
  concurrent: true,
});

for await (const value of stream) {
  if (
    value.object === "gen_table.completion.chunk" &&
    value.choices?.[0]?.message?.content
  ) {
    console.log(value.choices[0].message.content);
  }
}
```

***

## 7. DELETE ROWS

```typescript
await jamai.table.deleteRows({
  table_type: "action",
  table_id: "my_table",
  row_ids: ["uuid1", "uuid2"],
});
```

***

## 8. TABLE OPERATIONS

### List Tables

```typescript
const tables = await jamai.table.listTables({
  table_type: "action",
  count_rows: true,
});

for (const t of tables.items) {
  console.log(`${t.id}: ${t.num_rows} rows`);
}
```

### Get Schema

```typescript
const table = await jamai.table.getTable({
  table_type: "action",
  table_id: "my_table",
});

for (const col of table.cols) {
  console.log(`${col.id}: ${col.dtype}`);
}
```

### Delete Table

```typescript
await jamai.table.deleteTable({
  table_type: "action",
  table_id: "my_table",
});
```

### Duplicate Table

```typescript
// With data
const newTable = await jamai.table.duplicateTable({
  table_type: "action",
  table_id_src: "source",
  table_id_dst: "copy",
  include_data: true,
});

// Schema only
const schemaOnly = await jamai.table.duplicateTable({
  table_type: "action",
  table_id_src: "source",
  table_id_dst: "copy",
  include_data: false,
});
```

### Rename Table

```typescript
const renamedTable = await jamai.table.renameTable({
  table_type: "action",
  table_id_src: "old_name",
  table_id_dst: "new_name",
});
```

### Column Management

```typescript
// Add columns
const updatedTable = await jamai.table.addActionColumns({
  id: "my_table",
  cols: [{ id: "new_column", dtype: "str" }],
});

// Rename columns
await jamai.table.renameColumns({
  table_type: "action",
  table_id: "my_table",
  column_map: {
    old_name: "new_name",
  },
});

// Reorder columns
await jamai.table.reorderColumns({
  table_type: "action",
  table_id: "my_table",
  column_names: ["col1", "col2", "col3"],
});

// Drop columns
await jamai.table.dropColumns({
  table_type: "action",
  table_id: "my_table",
  column_names: ["col_to_remove"],
});
```

### Hybrid Search

```typescript
const results = await jamai.table.hybridSearch({
  table_type: "action",
  table_id: "my_table",
  query: "search term",
  limit: 10,
  metric: "dot",
  reranking_model: null,
});

for (const result of results) {
  console.log(result);
}
```

### Import/Export Data

```typescript
// Export table data to CSV
const csvData = await jamai.table.exportTableData({
  table_type: "action",
  table_id: "my_table",
});
// csvData is a Uint8Array containing CSV data

// Export table data to TSV
const tsvData = await jamai.table.exportTableData({
  table_type: "action",
  table_id: "my_table",
  delimiter: "\t",
});

// Import table data from CSV file
const importResponse = await jamai.table.importTableData({
  file_path: "/path/to/data.csv",
  table_id: "my_table",
  table_type: "action",
});
console.log(importResponse.rows.length);

// Import with streaming
const importStream = await jamai.table.importTableDataStream({
  file_path: "/path/to/data.csv",
  table_id: "my_table",
  table_type: "action",
});

const reader = importStream.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log(value);
}
```

### Progress Tracking for Long Operations

Track progress of long-running operations like table imports:

```typescript
import { PROGRESS_STATES } from "jamaibase";

// Get current progress
const progress = await jamai.tasks.getProgress("key");
console.log(`Progress: ${progress.state}`);

// Poll for completion
const result = await jamai.tasks.pollProgress("key", {
  initialWait: 0.5, // Initial wait in seconds
  maxWait: 60, // Maximum wait time in seconds
  verbose: true, // Log progress updates
});

if (result?.state === PROGRESS_STATES.COMPLETED) {
  console.log("Import completed successfully!");
} else if (result?.state === PROGRESS_STATES.FAILED) {
  console.error(`Import failed: ${result.error}`);
} else {
  console.log("Import timed out");
}
```

**Available Progress States:**

* `PROGRESS_STATES.PENDING` - Task is queued
* `PROGRESS_STATES.RUNNING` - Task is in progress
* `PROGRESS_STATES.COMPLETED` - Task completed successfully
* `PROGRESS_STATES.FAILED` - Task failed with error

***

## 9. KNOWLEDGE TABLES (RAG)

### Create

```typescript
const table = await jamai.table.createKnowledgeTable({
  id: "my_kb",
  cols: [], // Title, Text auto-created
  embedding_model: "ellm/BAAI/bge-m3",
});
```

### Add Data

```typescript
await jamai.table.addRow({
  table_type: "knowledge",
  table_id: "my_kb",
  data: [
    {
      Title: "Doc1",
      Text: "Content here...",
    },
  ],
  concurrent: false,
});
```

### Embed File

```typescript
import { File } from "formdata-node";

// Create a File object
const file = new File(
  ["I bought a Mofusand book in 2024.\n\nI went to Italy in 2018."],
  "text.txt",
  { type: "text/plain" }
);

// Embed the file
const response = await jamai.table.embedFile({
  file: file,
  table_id: "my_kb",
});

console.log(response.ok); // true
```

### Create RAG Action Table

```typescript
const table = await jamai.table.createActionTable({
  id: "rag_qa",
  cols: [
    { id: "question", dtype: "str" },
    {
      id: "answer",
      dtype: "str",
      gen_config: {
        model: "openai/gpt-4o-mini",
        prompt: "${question}",
        rag_params: {
          table_id: "my_kb", // Link to Knowledge Table
          k: 3, // Top k chunks
        },
        max_tokens: 200,
      },
    },
  ],
});
```

***

## 10. CHAT TABLES

### Create

```typescript
const table = await jamai.table.createChatTable({
  id: "my_chatbot",
  cols: [
    { id: "User", dtype: "str" },
    {
      id: "AI",
      dtype: "str",
      gen_config: {
        model: "openai/gpt-4o-mini",
        system_prompt: "You are helpful.",
        max_tokens: 500,
      },
    },
  ],
});
```

### Chat (Streaming)

```typescript
const stream = await jamai.table.addRowStream({
  table_type: "chat",
  table_id: "my_chatbot",
  data: [{ User: "Hello!" }],
});

for await (const value of stream) {
  if (
    value.object === "gen_table.completion.chunk" &&
    value.choices?.[0]?.message?.content
  ) {
    console.log(value.choices[0].message.content);
  }
}
```

### How Chat History Works

Chat tables **automatically maintain conversation history**. Each row added becomes part of the context for subsequent rows.

```typescript
// Turn 1
await jamai.table.addRow({
  table_type: "chat",
  table_id: "my_chatbot",
  data: [{ User: "My name is Alice" }],
  concurrent: false,
});

// Turn 2 - AI remembers the name from Turn 1
await jamai.table.addRow({
  table_type: "chat",
  table_id: "my_chatbot",
  data: [{ User: "What's my name?" }],
  concurrent: false,
});
// AI will respond: "Your name is Alice"

// View conversation history (getValue defined in Quick Reference)
const rows = await jamai.table.listRows({
  table_type: "chat",
  table_id: "my_chatbot",
  limit: 100,
});

for (const row of rows.items) {
  console.log(`User: ${getValue(row["User"])}`);
  console.log(`AI: ${getValue(row["AI"])}\n`);
}
```

**Note**: Each chat table is a separate conversation. Create multiple tables for multiple users/sessions.

***

## 11. FILE OPERATIONS

### Upload

```typescript
// Upload by file path
const fileResponse = await jamai.file.uploadFile({
  file_path: "/path/to/file.png",
});
const s3Uri = fileResponse.uri; // s3://devcloud-file/...

// Upload audio file
const audioUpload = await jamai.file.uploadFile({
  file_path: "/path/to/audio.mp3",
});
console.log(audioUpload.uri);
```

### Get Presigned URLs

```typescript
// Get raw URLs for uploaded files
const rawUrls = await jamai.file.getRawUrls({
  uris: [fileResponse.uri],
});
console.log(rawUrls.urls[0]);

// Get thumbnail URLs for uploaded images
const thumbUrls = await jamai.file.getThumbUrls({
  uris: [fileResponse.uri],
});
console.log(thumbUrls.urls[0]);
```

***

## 12. DIRECT API (No Tables)

### Chat Completions (Non-Streaming)

```typescript
const completion = await jamai.llm.generateChatCompletions({
  model: "openai/gpt-4o-mini",
  messages: [
    { role: "system", content: "You are helpful." },
    { role: "user", content: "Hello" },
  ],
  max_tokens: 100,
  temperature: 0.1,
  top_p: 0.1,
});

console.log(completion.choices[0]?.message?.content);
```

### Chat Completions (Streaming)

```typescript
const stream = await jamai.llm.generateChatCompletionsStream({
  model: "openai/gpt-4o-mini",
  messages: [
    { role: "system", content: "You are a concise assistant." },
    { role: "user", content: "What is a llama?" },
  ],
  temperature: 0.001,
  top_p: 0.001,
  max_tokens: 10,
});

for await (const value of stream) {
  if (
    value.object === "chat.completion.chunk" &&
    value?.choices[0]?.delta?.content
  ) {
    console.log(value.choices[0].delta.content);
  }
}
```

### Embeddings

```typescript
const texts = ["What is love?", "What is a llama?"];

const embeddings = await jamai.llm.generateEmbeddings({
  model: "ellm/BAAI/bge-m3",
  input: texts,
});

// Inspect one of the embeddings
console.log(embeddings.data[0]?.embedding.slice(0, 3));

// Print the text and its embedding
for (let i = 0; i < texts.length; i++) {
  console.log(texts[i], embeddings.data[i]?.embedding.slice(0, 3));
}
```

### Model Info

```typescript
// Get all model info
const models = await jamai.llm.modelInfo();
const model = models.data[0];
console.log(`Model: ${model?.id}  Context length: ${model?.context_length}`);

// Get specific model info
const specificModel = await jamai.llm.modelInfo({
  model: "openai/gpt-4o",
});
console.log(specificModel.data[0]);

// Filter based on capability: "chat", "embed", "rerank", "image"
const chatModels = await jamai.llm.modelInfo({
  capabilities: ["chat"],
});
for (const model of chatModels.data) {
  console.log(model);
}

const embedModels = await jamai.llm.modelInfo({
  capabilities: ["embed"],
});
for (const model of embedModels.data) {
  console.log(model);
}

const rerankModels = await jamai.llm.modelInfo({
  capabilities: ["rerank"],
});
for (const model of rerankModels.data) {
  console.log(model);
}

// Get model IDs/names
const modelNames = await jamai.llm.modelNames();
console.log(modelNames);

// Model IDs with the preferred model at the top if available
const preferredModels = await jamai.llm.modelNames({
  prefer: "openai/gpt-4o",
});
console.log(preferredModels[0]);

// Filter based on capability
const chatModelNames = await jamai.llm.modelNames({
  capabilities: ["chat"],
});
console.log(chatModelNames);
```

***

## 13. ORGANIZATIONS & PROJECTS

### Organizations

```typescript
// Create organization
const org = await jamai.organizations.createOrganization({
  name: "My Organization",
});
console.log(org.id, org.name);

// List organizations
const orgs = await jamai.organizations.listOrganizations({});
for (const org of orgs.items) {
  console.log(org.id, org.name);
}

// List organizations with pagination
const paginatedOrgs = await jamai.organizations.listOrganizations({
  limit: 10,
  offset: 0,
});

// Get specific organization
const specificOrg = await jamai.organizations.getOrganization(org.id);
console.log(specificOrg);

// Update organization
const updatedOrg = await jamai.organizations.updateOrganization(org.id, {
  name: "Updated Organization Name",
});
console.log(updatedOrg.name);

// Delete organization
const deleteResponse = await jamai.organizations.deleteOrganization(org.id);
console.log(deleteResponse.ok); // true

// List organization members
const members = await jamai.organizations.listMembers(org.id, {});
for (const member of members.items) {
  console.log(member.user_id, member.role);
}

// Get specific member
const currentUser = await jamai.users.getUser();
const member = await jamai.organizations.getMember(currentUser.id, org.id);
console.log(member.role);

// Get model catalogue for organization
const catalogue = await jamai.organizations.modelCatalogue(org.id, {});
for (const model of catalogue.items) {
  console.log(model.id);
}

// Create organization invite
const invite = await jamai.organizations.createInvite({
  user_email: "user@example.com",
  organization_id: org.id,
  role: "MEMBER",
  valid_days: 7,
});
console.log(invite.id);

// List organization invites
const invites = await jamai.organizations.listInvites(org.id, {});
for (const invite of invites.items) {
  console.log(invite.user_email, invite.role);
}

// Revoke organization invite
const revokeResponse = await jamai.organizations.revokeInvite(invite.id);
console.log(revokeResponse.ok); // true

// Refresh organization quota
const refreshedOrg = await jamai.organizations.refreshQuota(org.id);
console.log(refreshedOrg.quota_reset_at);
```

### Usage & Billing Metrics

Monitor usage and costs for your organization:

```typescript
// Get LLM usage metrics
const fromDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); // 7 days ago
const toDate = new Date().toISOString();

const llmMetrics = await jamai.meters.getUsageMetrics({
  type: "llm",
  from: fromDate,
  to: toDate,
  windowSize: "1d", // Options: "1h", "1d", "7d"
  orgIds: [org.id],
  groupBy: ["org_id"],
});
console.log("LLM Usage:", llmMetrics.data);

// Get embedding usage metrics
const embeddingMetrics = await jamai.meters.getUsageMetrics({
  type: "embedding",
  from: fromDate,
  to: toDate,
  windowSize: "1d",
  orgIds: [org.id],
});

// Get reranking usage metrics
const rerankingMetrics = await jamai.meters.getUsageMetrics({
  type: "reranking",
  from: fromDate,
  to: toDate,
  windowSize: "1d",
  orgIds: [org.id],
});

// Get billing metrics (cost information)
const billingMetrics = await jamai.meters.getBillingMetrics({
  from: fromDate,
  to: toDate,
  windowSize: "1d",
  orgIds: [org.id],
});
console.log("Billing:", billingMetrics.data);

// Get bandwidth metrics
const bandwidthMetrics = await jamai.meters.getBandwidthMetrics({
  from: fromDate,
  to: toDate,
  windowSize: "1d",
  orgIds: [org.id],
});

// Get storage metrics
const storageMetrics = await jamai.meters.getStorageMetrics({
  from: fromDate,
  to: toDate,
  windowSize: "1d",
  orgIds: [org.id],
});
```

**Window Size Options:**

* `"1h"` - Hourly metrics
* `"1d"` - Daily metrics
* `"7d"` - Weekly metrics

**Group By Options:**

* `["org_id"]` - Group by organization
* `["project_id"]` - Group by project
* `["model"]` - Group by model (for LLM metrics)

### Projects

```typescript
// Create project
const project = await jamai.projects.createProject({
  organization_id: org.id,
  name: "My Project",
});
console.log(project.id, project.name);

// List projects
const projects = await jamai.projects.listProjects(org.id, {});
for (const project of projects.items) {
  console.log(project.id, project.name);
}

// List projects with pagination
const paginatedProjects = await jamai.projects.listProjects(org.id, {
  limit: 10,
  offset: 0,
});

// Get specific project
const specificProject = await jamai.projects.getProject(project.id);
console.log(specificProject);

// Update project
const updatedProject = await jamai.projects.updateProject(project.id, {
  name: "Updated Project Name",
  description: "Updated description",
});
console.log(updatedProject.name, updatedProject.description);

// Delete project
const deleteProjectResponse = await jamai.projects.deleteProject(project.id);
console.log(deleteProjectResponse.ok); // true

// List project members
const projectMembers = await jamai.projects.listMembers(project.id, {});
for (const member of projectMembers.items) {
  console.log(member.user_id, member.role);
}

// Get specific project member
const projectMember = await jamai.projects.getMember(
  currentUser.id,
  project.id
);
console.log(projectMember.role);

// Create project invite
const projectInvite = await jamai.projects.createInvite({
  user_email: "user@example.com",
  project_id: project.id,
  role: "MEMBER",
  valid_days: 7,
});
console.log(projectInvite.id);

// List project invites
const projectInvites = await jamai.projects.listInvites(project.id, {});
for (const invite of projectInvites.items) {
  console.log(invite.user_email, invite.role);
}

// Revoke project invite
const revokeProjectInvite = await jamai.projects.revokeInvite(projectInvite.id);
console.log(revokeProjectInvite.ok); // true

// Export project
const exportData = await jamai.projects.exportProject(project.id);
// exportData is a Uint8Array containing the exported project data
console.log(exportData.length);
```

***

## 14. USERS & AUTHENTICATION

```typescript
// Get current user
const currentUser = await jamai.users.getUser();
console.log(currentUser.id, currentUser.name, currentUser.email);

// List users
const users = await jamai.users.listUsers({});
for (const user of users.items) {
  console.log(user.id, user.name);
}

// List users with pagination
const paginatedUsers = await jamai.users.listUsers({
  limit: 10,
  offset: 0,
});

// Update current user
const updatedUser = await jamai.users.updateUser({
  name: "Updated Name",
});
console.log(updatedUser.name);

// Create personal access token (PAT)
const pat = await jamai.users.createPat({
  name: "My API Token",
});
console.log(pat.id, pat.name);

// List personal access tokens
const pats = await jamai.users.listPats({});
for (const token of pats.items) {
  console.log(token.id, token.name);
}

// Update personal access token
const updatedPat = await jamai.users.updatePat(pat.id, {
  name: "Updated Token Name",
});
console.log(updatedPat.name);

// Delete personal access token
const deletePat = await jamai.users.deletePat(pat.id);
console.log(deletePat.ok); // true

// Create email verification code
const verificationCode = await jamai.users.createEmailVerificationCode();
console.log(verificationCode.id);

// List email verification codes
const codes = await jamai.users.listEmailVerificationCodes({});
for (const code of codes.items) {
  console.log(code.id);
}

// Get specific email verification code
const specificCode = await jamai.users.getEmailVerificationCode(
  verificationCode.id
);
console.log(specificCode);

// Revoke email verification code
const revokeCode = await jamai.users.revokeEmailVerificationCode(
  verificationCode.id
);
console.log(revokeCode.ok); // true
```

***

## 15. SECRETS MANAGEMENT

Manage secrets and API keys at the organization level.

```typescript
// Create secret
const secret = await jamai.secrets.createSecret(
  {
    name: "MY_API_KEY",
    value: "secret-value-here",
  },
  org.id
);
console.log(secret.name, secret.organization_id);

// Create secret with allowed projects
const secretWithProjects = await jamai.secrets.createSecret(
  {
    name: "PROJECT_API_KEY",
    value: "secret-value",
    allowed_projects: [project.id],
  },
  org.id
);

// List secrets
const secrets = await jamai.secrets.listSecrets(org.id, {});
for (const secret of secrets.items) {
  console.log(secret.name, secret.value); // value is masked
}

// List secrets with pagination
const paginatedSecrets = await jamai.secrets.listSecrets(org.id, {
  limit: 10,
  offset: 0,
});

// Get specific secret
const specificSecret = await jamai.secrets.getSecret(org.id, "MY_API_KEY");
console.log(specificSecret.value); // value is masked

// Update secret
const updatedSecret = await jamai.secrets.updateSecret(org.id, "MY_API_KEY", {
  value: "new-secret-value",
});
console.log(updatedSecret.value); // value is unmasked on update

// Delete secret
const deleteSecret = await jamai.secrets.deleteSecret(org.id, "MY_API_KEY");
console.log(deleteSecret.ok); // true
```

{% hint style="info" %}
**Secret names must:**

* Start with a letter or underscore
* Contain only alphanumeric characters and underscores
* Be uppercase (automatically converted)
  {% endhint %}

***

## 16. TEMPLATES

Templates provide pre-built table configurations that you can use as starting points:

```typescript
// List available templates
const templates = await jamai.template.listTemplates();
for (const template of templates.items) {
  console.log(`Template: ${template.id} - ${template.name}`);
}

// Get template details
const template = await jamai.template.getTemplate({
  template_id: "template_id",
});
console.log(template);

// List tables in a template
const tables = await jamai.template.listTables({
  template_id: "template_id",
  table_type: "action",
});
for (const table of tables.items) {
  console.log(`Table: ${table.id}`);
}

// Get specific table from template
const table = await jamai.template.getTable({
  template_id: "template_id",
  table_type: "action",
  table_id: "table_id",
});
console.log(table);

// List rows in a template table
const rows = await jamai.template.listTableRows({
  template_id: "template_id",
  table_type: "action",
  table_id: "table_id",
});
for (const row of rows.items) {
  console.log(row);
}
```

**Use Cases:**

* Explore pre-built table configurations
* Learn best practices for table design
* Quick-start with proven table structures
* Browse example data and prompts

## 17. CONVERSATIONS

Manage conversations with AI agents (Chat Tables).

```typescript
// Create a chat table (agent) first
const agent = await jamai.table.createChatTable({
  id: "my-agent",
  cols: [
    { id: "User", dtype: "str" },
    {
      id: "AI",
      dtype: "str",
      gen_config: {
        model: "openai/gpt-4o-mini",
        system_prompt: "You are a helpful assistant.",
      },
    },
  ],
});

// List agents
const agents = await jamai.conversations.listAgents({});
for (const agent of agents.items) {
  console.log(agent.conversation_id, agent.title);
}

// Get specific agent
const agentDetails = await jamai.conversations.getAgent("my-agent");
console.log(agentDetails.agent_id, agentDetails.title);

// Create a conversation
const conversationStream = await jamai.conversations.createConversation({
  agent_id: "my-agent",
  title: "My Conversation",
  data: {},
});

// Read the stream to get conversation ID
const reader = conversationStream.getReader();
let conversationId: string | null = null;

try {
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    if (value && typeof value === "object" && "conversation_id" in value) {
      conversationId = value.conversation_id as string;
      break;
    }
  }
} finally {
  reader.releaseLock();
}

// List conversations
const conversations = await jamai.conversations.listConversations({});
for (const conv of conversations.items) {
  console.log(conv.conversation_id, conv.title);
}

// List conversations with limit
const limitedConversations = await jamai.conversations.listConversations({
  limit: 10,
});

// Get specific conversation
const conversation = await jamai.conversations.getConversation(conversationId!);
console.log(conversation.title);

// Rename conversation title
const updatedConv = await jamai.conversations.renameConversationTitle(
  conversationId!,
  "Updated Title"
);
console.log(updatedConv.title);

// Send message to conversation
const messageStream = await jamai.conversations.sendMessage({
  conversation_id: conversationId!,
  data: { User: "Hello, how are you?" },
});

const messageReader = messageStream.getReader();
try {
  while (true) {
    const { done, value } = await messageReader.read();
    if (done) break;
    console.log(value);
  }
} finally {
  messageReader.releaseLock();
}

// List messages in a conversation
const messages = await jamai.conversations.listMessages(conversationId!, {});
console.log(messages);

// Get conversation threads
const threads = await jamai.conversations.getThreads(conversationId!);
console.log(threads.threads);

// Delete conversation
const deleteConv = await jamai.conversations.deleteConversation(
  conversationId!
);
console.log(deleteConv.ok); // true
```

***

## 18. COMPLETE EXAMPLE

End-to-end example: Create table, add row, read result, cleanup.

```typescript
import JamAI from "jamaibase";

// Initialize (auto-loads JAMAI_API_KEY, JAMAI_PROJECT_ID from env)
const jamai = new JamAI();

// Helper function (use throughout your code)
function getValue(field: any): any {
  if (field && typeof field === "object" && "value" in field) {
    return field.value;
  }
  return field;
}

async function main() {
  // 1. Create table with LLM column
  const TABLE_ID = "qa_demo";

  // Delete if exists
  try {
    await jamai.table.deleteTable({
      table_type: "action",
      table_id: TABLE_ID,
    });
  } catch (error) {
    // Table didn't exist, continue
  }

  await jamai.table.createActionTable({
    id: TABLE_ID,
    cols: [
      { id: "question", dtype: "str" },
      {
        id: "answer",
        dtype: "str",
        gen_config: {
          model: "openai/gpt-4o-mini",
          system_prompt: "You are a helpful assistant. Be concise.",
          prompt: "Question: ${question}",
          max_tokens: 100,
        },
      },
    ],
  });
  console.log(`Created table: ${TABLE_ID}`);

  // 2. Add row (non-streaming)
  const response = await jamai.table.addRow({
    table_type: "action",
    table_id: TABLE_ID,
    data: [{ question: "What is TypeScript?" }],
    concurrent: false,
  });

  const rowId = response.rows[0]?.row_id;
  const answer =
    response.rows[0]?.columns["answer"]?.choices[0]?.message?.content;
  console.log(`Row ID: ${rowId}`);
  console.log(`Answer: ${answer}`);

  // 3. Read rows
  const rows = await jamai.table.listRows({
    table_type: "action",
    table_id: TABLE_ID,
    limit: 100,
  });

  for (const row of rows.items) {
    const q = getValue(row["question"]);
    const a = getValue(row["answer"]);
    console.log(`Q: ${q}\nA: ${a}\n`);
  }

  // 4. Cleanup
  await jamai.table.deleteTable({
    table_type: "action",
    table_id: TABLE_ID,
  });
  console.log("Cleanup complete");
}

main().catch(console.error);
```

***

## QUICK COPY-PASTE

```typescript
// === IMPORTS ===
import JamAI from "jamaibase";

// === INIT ===
const jamai = new JamAI(); // Auto-loads from JAMAI_API_KEY, JAMAI_PROJECT_ID env vars

// === HELPER (ALWAYS USE) ===
function getValue(field: any): any {
  if (field && typeof field === "object" && "value" in field) {
    return field.value;
  }
  return field;
}

// === ADD ROWS ===
const response = await jamai.table.addRow({
  table_type: "action",
  table_id: "TABLE",
  data: [{ col: "val" }],
  concurrent: false,
});
const rowId = response.rows[0]?.row_id;
const output =
  response.rows[0]?.columns["output_col"]?.choices[0]?.message?.content;

// === LIST ROWS (MAX 100!) ===
const rows = await jamai.table.listRows({
  table_type: "action",
  table_id: "TABLE",
  limit: 100,
});
for (const row of rows.items) {
  const value = getValue(row["column"]);
}

// === WITH WHERE FILTER ===
const filteredRows = await jamai.table.listRows({
  table_type: "action",
  table_id: "TABLE",
  where: "\"status\" = 'active'",
});

// === UPDATE ROWS ===
await jamai.table.updateRows({
  table_type: "action",
  table_id: "TABLE",
  data: {
    "row-id": { col: "new_val" },
  },
});

// === DELETE ROWS ===
await jamai.table.deleteRows({
  table_type: "action",
  table_id: "TABLE",
  row_ids: ["row-id-1", "row-id-2"],
});

// === FILE UPLOAD ===
const fileResp = await jamai.file.uploadFile({
  file_path: "/path/to/file.png",
});
const uri = fileResp.uri; // Use in row data

// === STREAMING ===
const stream = await jamai.table.addRowStream({
  table_type: "action",
  table_id: "TABLE",
  data: [{ col: "val" }],
});

for await (const value of stream) {
  if (
    value.object === "gen_table.completion.chunk" &&
    value.choices?.[0]?.message?.content
  ) {
    console.log(value.choices[0].message.content);
  }
}
```


# Quick Start: Knowledge Table

## 1. Introduction

This guide demonstrates how to use JamAI Base TypeScript SDK to upload and embed files into Knowledge Tables for AI-powered document processing and retrieval.

### What are Knowledge Tables?

Knowledge Tables are specialized tables in JamAI Base that provide hybrid-search capabilities through both full-text search (FTS) and vector embeddings:

* **Search Capabilities**:
  * **Full-Text Search (FTS)**: Traditional keyword-based search for exact and partial matches
  * **Semantic Search**: Vector embedding-based search for meaning and context
* **Document Processing**:
  * Automatically chunks documents into manageable segments
  * Generates vector embeddings for semantic understanding
  * Indexes content for full-text search
  * Preserves document structure (tables, layouts, etc) and metadata
* **Use Cases**:
  * Document retrieval using both keywords and semantic meaning
  * Question-answering agent
  * Content recommendation
  * Knowledge base search and discovery

### Supported File Types

The following file formats are supported:

* Text files: `.txt`, `.md`, `.csv`, `.tsv`
* Documents: `.doc`, `.docx`, `.pdf`
* Presentations: `.ppt`, `.pptx`
* Spreadsheets: `.xls`, `.xlsx`
* Markup/Data: `.xml`, `.html`, `.json`, `.jsonl`

### Prerequisites

Before starting, you'll need:

* Node.js 16.x or higher
* Project ID and Personal Access Token (PAT)
* Documents to process

## 2. Installation and Setup

### Installing Required Packages

```bash
npm install jamaibase dotenv
```

### Basic Configuration

```typescript
import JamAI from "jamaibase";
import * as dotenv from "dotenv";

// Load environment variables
dotenv.config();

const PROJECT_ID = "your_project_id";
const PAT = process.env.PAT || "your_PAT";

const client = new JamAI({
  projectId: PROJECT_ID,
  token: PAT,
});
```

## 3. Creating Your Knowledge Table

{% stepper %}
{% step %}

### Create a new table

Navigate to your JamAI Base knowledge tables tab and create a new knowledge table.
{% endstep %}

{% step %}

### Note table ID

After creating the table, note down the table ID for later use.
{% endstep %}
{% endstepper %}

## 4. Implementation

### 4.1 Complete Document Uploader Class

```typescript
import JamAI from "jamaibase";
import { File } from "formdata-node";
import * as fs from "fs";
import * as path from "path";

class DocumentUploader {
  private client: JamAI;

  constructor(projectId: string, pat: string) {
    // Initialize the document uploader
    this.client = new JamAI({
      projectId: projectId,
      token: pat,
    });
  }

  validateFile(filePath: string): boolean {
    // Validate if file exists and has supported format
    if (!fs.existsSync(filePath)) {
      throw new Error(`File not found: ${filePath}`);
    }

    const supportedTypes = [
      ".csv",
      ".tsv",
      ".txt",
      ".md",
      ".doc",
      ".docx",
      ".pdf",
      ".ppt",
      ".pptx",
      ".xls",
      ".xlsx",
      ".xml",
      ".html",
      ".json",
      ".jsonl",
    ];
    const fileExt = path.extname(filePath).toLowerCase();

    if (!supportedTypes.includes(fileExt)) {
      throw new Error(
        `Unsupported file format. Supported formats: ${supportedTypes.join(
          ", "
        )}`
      );
    }

    return true;
  }

  getMimeType(filePath: string): string {
    // Get MIME type of the file
    const mimeTypes: { [key: string]: string } = {
      ".csv": "text/csv",
      ".tsv": "text/tab-separated-values",
      ".txt": "text/plain",
      ".md": "text/markdown",
      ".doc": "application/msword",
      ".docx":
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      ".pdf": "application/pdf",
      ".ppt": "application/vnd.ms-powerpoint",
      ".pptx":
        "application/vnd.openxmlformats-officedocument.presentationml.presentation",
      ".xls": "application/vnd.ms-excel",
      ".xlsx":
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      ".xml": "application/xml",
      ".html": "text/html",
      ".json": "application/json",
      ".jsonl": "application/jsonl",
    };

    const fileExt = path.extname(filePath).toLowerCase();
    return mimeTypes[fileExt] || "application/octet-stream";
  }

  getOptimalChunkSettings(filePath: string): { size: number; overlap: number } {
    // Determine optimal chunk settings based on file type
    const fileExt = path.extname(filePath).toLowerCase();

    const settings: Record<string, { size: number; overlap: number }> = {
      // Text-based documents
      ".txt": { size: 1000, overlap: 200 },
      ".md": { size: 1000, overlap: 200 },
      ".csv": { size: 800, overlap: 150 },
      ".tsv": { size: 800, overlap: 150 },

      // Rich text documents
      ".doc": { size: 1200, overlap: 250 },
      ".docx": { size: 1200, overlap: 250 },
      ".pdf": { size: 1500, overlap: 300 },

      // Presentations
      ".ppt": { size: 1000, overlap: 200 },
      ".pptx": { size: 1000, overlap: 200 },

      // Spreadsheets
      ".xls": { size: 800, overlap: 150 },
      ".xlsx": { size: 800, overlap: 150 },

      // Markup/structured documents
      ".xml": { size: 1000, overlap: 200 },
      ".html": { size: 1000, overlap: 200 },
      ".json": { size: 800, overlap: 150 },
      ".jsonl": { size: 800, overlap: 150 },
    };

    return settings[fileExt] || { size: 1000, overlap: 200 };
  }

  async uploadDocument(
    filePath: string,
    tableId: string,
    customChunkSize?: number,
    customChunkOverlap?: number
  ): Promise<boolean> {
    try {
      // Validate file
      this.validateFile(filePath);

      // Get file information
      const fileName = path.basename(filePath);
      const fileStats = fs.statSync(filePath);
      const fileSize = fileStats.size;
      const mimeType = this.getMimeType(filePath);

      // Get chunk settings
      const settings = this.getOptimalChunkSettings(filePath);
      const chunkSize = customChunkSize || settings.size;
      const chunkOverlap = customChunkOverlap || settings.overlap;

      console.log(`Uploading: ${fileName}`);
      console.log(`File type: ${mimeType}`);
      console.log(`File size: ${(fileSize / 1024).toFixed(2)} KB`);
      console.log(`Chunk size: ${chunkSize}, Overlap: ${chunkOverlap}`);

      // Read file and create File object
      const fileBuffer = fs.readFileSync(filePath);
      const file = new File([fileBuffer], fileName, { type: mimeType });

      // Upload and embed file
      const response = await this.client.table.embedFile({
        file: file,
        table_id: tableId,
        chunk_size: chunkSize,
        chunk_overlap: chunkOverlap,
      });

      console.log(`Upload successful: ${fileName}`);
      return true;
    } catch (error) {
      console.error(`Error uploading ${filePath}: ${error}`);
      return false;
    }
  }
}
```

## 5. Complete Standalone Script

Save this as `knowledge_uploader.ts`:

```typescript
import JamAI from "jamaibase";
import { File } from "formdata-node";
import * as fs from "fs";
import * as path from "path";
import * as dotenv from "dotenv";

class DocumentUploader {
  private client: JamAI;

  constructor(projectId: string, pat: string) {
    this.client = new JamAI({
      projectId: projectId,
      token: pat,
    });
  }

  validateFile(filePath: string): boolean {
    if (!fs.existsSync(filePath)) {
      throw new Error(`File not found: ${filePath}`);
    }

    const supportedTypes = [
      ".csv",
      ".tsv",
      ".txt",
      ".md",
      ".doc",
      ".docx",
      ".pdf",
      ".ppt",
      ".pptx",
      ".xls",
      ".xlsx",
      ".xml",
      ".html",
      ".json",
      ".jsonl",
    ];
    const fileExt = path.extname(filePath).toLowerCase();

    if (!supportedTypes.includes(fileExt)) {
      throw new Error(
        `Unsupported file format. Supported formats: ${supportedTypes.join(
          ", "
        )}`
      );
    }

    return true;
  }

  getMimeType(filePath: string): string {
    const mimeTypes: { [key: string]: string } = {
      ".csv": "text/csv",
      ".tsv": "text/tab-separated-values",
      ".txt": "text/plain",
      ".md": "text/markdown",
      ".doc": "application/msword",
      ".docx":
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      ".pdf": "application/pdf",
      ".ppt": "application/vnd.ms-powerpoint",
      ".pptx":
        "application/vnd.openxmlformats-officedocument.presentationml.presentation",
      ".xls": "application/vnd.ms-excel",
      ".xlsx":
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
      ".xml": "application/xml",
      ".html": "text/html",
      ".json": "application/json",
      ".jsonl": "application/jsonl",
    };

    const fileExt = path.extname(filePath).toLowerCase();
    return mimeTypes[fileExt] || "application/octet-stream";
  }

  getOptimalChunkSettings(filePath: string): { size: number; overlap: number } {
    const fileExt = path.extname(filePath).toLowerCase();

    const settings: { [key: string]: { size: number; overlap: number } } = {
      ".txt": { size: 1000, overlap: 200 },
      ".md": { size: 1000, overlap: 200 },
      ".csv": { size: 800, overlap: 150 },
      ".tsv": { size: 800, overlap: 150 },
      ".doc": { size: 1200, overlap: 250 },
      ".docx": { size: 1200, overlap: 250 },
      ".pdf": { size: 1500, overlap: 300 },
      ".ppt": { size: 1000, overlap: 200 },
      ".pptx": { size: 1000, overlap: 200 },
      ".xls": { size: 800, overlap: 150 },
      ".xlsx": { size: 800, overlap: 150 },
      ".xml": { size: 1000, overlap: 200 },
      ".html": { size: 1000, overlap: 200 },
      ".json": { size: 800, overlap: 150 },
      ".jsonl": { size: 800, overlap: 150 },
    };

    return settings[fileExt] || { size: 1000, overlap: 200 };
  }

  async uploadDocument(
    filePath: string,
    tableId: string,
    customChunkSize?: number,
    customChunkOverlap?: number
  ): Promise<boolean> {
    try {
      this.validateFile(filePath);

      const fileName = path.basename(filePath);
      const fileStats = fs.statSync(filePath);
      const fileSize = fileStats.size;
      const mimeType = this.getMimeType(filePath);

      const settings = this.getOptimalChunkSettings(filePath);
      const chunkSize = customChunkSize || settings.size;
      const chunkOverlap = customChunkOverlap || settings.overlap;

      console.log(`Uploading: ${fileName}`);
      console.log(`File type: ${mimeType}`);
      console.log(`File size: ${(fileSize / 1024).toFixed(2)} KB`);
      console.log(`Chunk size: ${chunkSize}, Overlap: ${chunkOverlap}`);

      const fileBuffer = fs.readFileSync(filePath);
      const file = new File([fileBuffer], fileName, { type: mimeType });

      const response = await this.client.table.embedFile({
        file: file,
        table_id: tableId,
        chunk_size: chunkSize,
        chunk_overlap: chunkOverlap,
      });

      console.log(`Upload successful: ${fileName}`);
      return true;
    } catch (error) {
      console.error(`Error uploading ${filePath}: ${error}`);
      return false;
    }
  }
}

async function processFolder(
  folderPath: string,
  uploader: DocumentUploader,
  tableId: string,
  chunkSize?: number,
  chunkOverlap?: number
): Promise<{ successful: string[]; failed: string[] }> {
  const successful: string[] = [];
  const failed: string[] = [];

  const files = fs.readdirSync(folderPath);

  for (const filename of files) {
    const filePath = path.join(folderPath, filename);
    if (fs.statSync(filePath).isFile()) {
      const success = await uploader.uploadDocument(
        filePath,
        tableId,
        chunkSize,
        chunkOverlap
      );
      if (success) {
        successful.push(filename);
      } else {
        failed.push(filename);
      }
    }
  }

  return { successful, failed };
}

// Main execution
async function main() {
  // Load environment variables
  dotenv.config();

  // Configuration (use environment variables or command-line args)
  const PROJECT_ID = process.env.JAMAI_PROJECT_ID || "your_project_id";
  const PAT = process.env.JAMAI_API_KEY || "your_PAT";
  const TABLE_ID = process.env.TABLE_ID || "your_table_id";
  const INPUT_PATH = process.env.INPUT_PATH || "path/to/file_or_folder";

  // Initialize uploader
  const uploader = new DocumentUploader(PROJECT_ID, PAT);

  // Process input
  if (fs.statSync(INPUT_PATH).isFile()) {
    // Single file upload
    console.log("Processing single file...\n");
    const success = await uploader.uploadDocument(INPUT_PATH, TABLE_ID);

    if (success) {
      console.log("\nFile uploaded successfully!");
    } else {
      console.log("\nFile upload failed.");
    }
  } else if (fs.statSync(INPUT_PATH).isDirectory()) {
    // Batch folder upload
    console.log("Processing folder...\n");
    const { successful, failed } = await processFolder(
      INPUT_PATH,
      uploader,
      TABLE_ID
    );

    console.log("\n=== Upload Summary ===");
    console.log(`Successful: ${successful.length}`);
    console.log(`Failed: ${failed.length}`);

    if (successful.length > 0) {
      console.log("\nSuccessful uploads:");
      successful.forEach((file) => console.log(`  ✓ ${file}`));
    }

    if (failed.length > 0) {
      console.log("\nFailed uploads:");
      failed.forEach((file) => console.log(`  ✗ ${file}`));
    }
  } else {
    console.log("Invalid input path");
  }
}

main().catch(console.error);
```

## 6. Usage Examples

### 6.1 Single File Upload

```typescript
// Initialize uploader
const uploader = new DocumentUploader(PROJECT_ID, PAT);

// Upload single document
const success = await uploader.uploadDocument(
  "path/to/document.pdf",
  "your_table_id"
);

if (success) {
  console.log("Upload successful!");
}
```

### 6.2 Batch Folder Upload

```typescript
const { successful, failed } = await processFolder(
  "path/to/documents/folder",
  uploader,
  "your_table_id"
);

console.log(`Successfully uploaded: ${successful.length} files`);
console.log(`Failed uploads: ${failed.length} files`);
```

### 6.3 Custom Chunk Settings

```typescript
// Upload with custom chunk size and overlap
await uploader.uploadDocument(
  "path/to/document.pdf",
  "your_table_id",
  2000, // Custom chunk size
  400 // Custom chunk overlap
);
```

## 7. Best Practices

### 7.1 Chunking Strategy

**Recommended Settings by Document Type:**

| Document Type | Chunk Size | Overlap | Reasoning                   |
| ------------- | ---------- | ------- | --------------------------- |
| PDFs          | 1500       | 300     | Preserves paragraph context |
| Text Files    | 1000       | 200     | Balanced for general text   |
| Spreadsheets  | 800        | 150     | Smaller for structured data |
| JSON/JSONL    | 800        | 150     | Maintains object boundaries |

### 7.2 Error Handling

```typescript
async function robustUpload(
  filePath: string,
  tableId: string,
  maxRetries: number = 3
) {
  const uploader = new DocumentUploader(PROJECT_ID, PAT);

  for (let i = 0; i < maxRetries; i++) {
    try {
      const success = await uploader.uploadDocument(filePath, tableId);
      if (success) return true;
    } catch (error) {
      console.log(`Attempt ${i + 1} failed: ${error}`);
      if (i === maxRetries - 1) throw error;
      await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
  return false;
}
```

### 7.3 Batch Processing with Rate Limiting

```typescript
async function batchUploadWithLimit(
  files: string[],
  tableId: string,
  concurrentLimit: number = 3
) {
  const uploader = new DocumentUploader(PROJECT_ID, PAT);
  const results = [];

  for (let i = 0; i < files.length; i += concurrentLimit) {
    const batch = files.slice(i, i + concurrentLimit);
    const batchResults = await Promise.all(
      batch.map((file) => uploader.uploadDocument(file, tableId))
    );
    results.push(...batchResults);

    // Rate limiting delay
    if (i + concurrentLimit < files.length) {
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }

  return results;
}
```

## 8. Running the Script

### Using Environment Variables

1. Create `.env` file:

   ```
   JAMAI_PROJECT_ID=your_project_id
   JAMAI_API_KEY=your_PAT
   TABLE_ID=your_table_id
   INPUT_PATH=path/to/documents
   ```
2. Run the script:

   ```bash
   ts-node knowledge_uploader.ts
   ```

### Using Command Line (modify script to accept args)

```bash
ts-node knowledge_uploader.ts \
    --project-id your_project_id \
    --pat your_PAT \
    --table-id your_table_id \
    --input path/to/documents
```

## 9. Troubleshooting

### Common Issues

1. **File not found error**
   * Verify file path is correct
   * Use absolute paths for reliability
2. **Unsupported file format**
   * Check file extension against supported types
   * Verify file is not corrupted
3. **Upload timeout**
   * Check internet connection
   * For large files, consider increasing timeout settings
   * Verify file size is reasonable
4. **Chunk size errors**
   * Ensure chunk size > chunk overlap
   * Try default settings first
   * Adjust based on document structure

## 10. Next Steps

{% stepper %}
{% step %}

### Query Your Knowledge Base

* Use hybrid search to find relevant documents.
* Combine with Action Tables for RAG applications.
  {% endstep %}

{% step %}

### Create RAG Applications

* Link Knowledge Tables to Action Tables.
* Build question-answering systems.
* Develop intelligent document assistants.
  {% endstep %}

{% step %}

### Monitor and Optimize

* Review search quality.
* Adjust chunk settings if needed.
* Add more documents to improve coverage.
  {% endstep %}
  {% endstepper %}


# Quick Start: Chat Table

{% stepper %}
{% step %}

### Introduction

This guide will help you quickly get started with JamAI Base chat tables using the TypeScript SDK. You'll learn how to create a simple chat application similar to the example provided.
{% endstep %}

{% step %}

### Prerequisites

* Node.js 16.x or higher
* JamAI Base account
* Personal Access Token (PAT)
* Project ID
  {% endstep %}

{% step %}

### Installation

```bash
npm install jamaibase
```

{% endstep %}

{% step %}

### Basic Setup

Get your Personal Access Token (PAT) here:

Get your Project ID here:

```typescript
import JamAI from "jamaibase";

// Initialize JamAI client
const jamai = new JamAI({
  projectId: "your_project_id", // Replace with your project ID
  token: "your_pat_token", // Replace with your PAT
});
```

{% endstep %}

{% step %}

### Creating a Chat Agent (UI First)

Go to JamAI Base web interface chat table tab

Navigate to "Agents" section, click **`+`** to create new agent.

Name the new agent `"example_agent"` and select LLM model.

You can further configure your agent in UI but it will not be covered in this tutorial.

Example of some configuration you can do:

* Set system prompt
* Set multiple parallel agents
* Configure RAG settings
* Set up knowledge base
  {% endstep %}

{% step %}

### Creating Chat Sessions

Each chat session is created by duplicating your base agent table. Here's how to do it:

```typescript
async function createNewChat(): Promise<string | null> {
  const timestamp = Date.now();
  const newTableId = `Chat_${timestamp}`;

  try {
    await jamai.table.duplicateTable({
      table_type: "chat",
      table_id_src: "example_agent", // Your base agent ID
      table_id_dst: newTableId,
      include_data: true,
      create_as_child: true,
    });
    return newTableId;
  } catch (error) {
    console.error(`Error creating new chat: ${error}`);
    return null;
  }
}
```

{% endstep %}

{% step %}

### Basic Chat Interaction

```typescript
// Send a message and get response
async function chatInteraction(
  tableId: string,
  userMessage: string
): Promise<string> {
  const response = await jamai.table.addRowStream({
    table_type: "chat",
    table_id: tableId,
    data: [{ User: userMessage }],
  });

  // For streaming response
  let fullResponse = "";

  for await (const value of response) {
    if (
      value.object === "gen_table.completion.chunk" &&
      value.choices?.[0]?.message?.content
    ) {
      fullResponse += value.choices[0].message.content;
      console.log(value.choices[0].message.content);
    }
  }

  return fullResponse;
}

// For non-streaming response
async function chatInteractionNonStream(tableId: string, userMessage: string) {
  const response = await jamai.table.addRow({
    table_type: "chat",
    table_id: tableId,
    data: [{ User: userMessage }],
    concurrent: false,
  });

  return response.rows[0]?.columns["AI"]?.choices[0]?.message?.content;
}
```

{% endstep %}

{% step %}

### Simple Implementation Example

```typescript
async function main() {
  // Create a new chat session
  const chatId = await createNewChat();

  if (chatId) {
    // Send a message
    const response = await chatInteraction(chatId, "Hello, how are you?");
    console.log("\nAI:", response);
  }
}

main().catch(console.error);
```

{% endstep %}

{% step %}

### Key Features

* **Streaming Responses**: Use `addRowStream()` for real-time responses
* **Session Management**: Each chat creates a new table instance
* **Inheritance**: New chats inherit settings from the base agent
* **History Preservation**: Chat history is maintained in the table
  {% endstep %}

{% step %}

### Best Practices

* Create a new chat session for each conversation
* Handle exceptions during table creation
* Store chat IDs for session management
* Clean up unused chat tables periodically
  {% endstep %}

{% step %}

### Complete Runnable Example with CLI

Here's a complete, standalone example that you can copy, paste, and run in the terminal:

```typescript
import JamAI from "jamaibase";
import * as readline from "readline";
import * as dotenv from "dotenv";

// Load environment variables (optional)
dotenv.config();

// Constants
const PROJECT_ID = process.env.JAMAI_PROJECT_ID || "your_project_id";
const PAT = process.env.JAMAI_API_KEY || "your_pat_token";
const AGENT_ID = "example_agent"; // Replace with your agent ID
const OPENER = "Hello! How can I help you today?";

// Initialize JamAI
const jamai = new JamAI({
  projectId: PROJECT_ID,
  token: PAT,
});

interface Message {
  role: "user" | "assistant";
  content: string;
}

class ChatSession {
  private tableId: string | null = null;
  private messages: Message[] = [];
  private rl: readline.Interface;

  constructor() {
    this.rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
    });
  }

  async initialize(): Promise<boolean> {
    this.tableId = await this.createNewChat();
    if (this.tableId) {
      this.messages.push({ role: "assistant", content: OPENER });
      console.log(`\nAssistant: ${OPENER}\n`);
      return true;
    }
    return false;
  }

  async createNewChat(): Promise<string | null> {
    const timestamp = Date.now();
    const newTableId = `Chat_${timestamp}`;

    try {
      await jamai.table.duplicateTable({
        table_type: "chat",
        table_id_src: AGENT_ID,
        table_id_dst: newTableId,
        include_data: true,
        create_as_child: true,
      });
      console.log(`Created new chat session: ${newTableId}`);
      return newTableId;
    } catch (error) {
      console.error(`Error creating new chat: ${error}`);
      return null;
    }
  }

  async sendMessage(userMessage: string): Promise<void> {
    if (!this.tableId) {
      console.error("No active chat session");
      return;
    }

    // Add user message
    this.messages.push({ role: "user", content: userMessage });

    // Get AI response with streaming
    console.log("\nAssistant: ");

    const response = await jamai.table.addRowStream({
      table_type: "chat",
      table_id: this.tableId,
      data: [{ User: userMessage }],
    });

    let fullResponse = "";

    for await (const value of response) {
      if (
        value.object === "gen_table.completion.chunk" &&
        value.choices?.[0]?.message?.content
      ) {
        const content = value.choices[0].message.content;
        fullResponse += content;
        console.log(content);
      }
    }

    console.log("\n");
    this.messages.push({ role: "assistant", content: fullResponse });
  }

  async startChat(): Promise<void> {
    console.log(
      "\nChat started! Type 'exit' to quit, 'new' to start a new chat.\n"
    );

    const askQuestion = () => {
      this.rl.question("You: ", async (input) => {
        const userInput = input.trim();

        if (userInput.toLowerCase() === "exit") {
          console.log("Goodbye!");
          this.rl.close();
          process.exit(0);
        } else if (userInput.toLowerCase() === "new") {
          const success = await this.initialize();
          if (success) {
            askQuestion();
          } else {
            console.log("Failed to create new chat. Exiting...");
            this.rl.close();
            process.exit(1);
          }
        } else if (userInput) {
          await this.sendMessage(userInput);
          askQuestion();
        } else {
          askQuestion();
        }
      });
    };

    askQuestion();
  }
}

async function main() {
  console.log("=== JamAI Base Chat Demo ===\n");

  const chat = new ChatSession();
  const initialized = await chat.initialize();

  if (initialized) {
    await chat.startChat();
  } else {
    console.error("Failed to initialize chat session");
    process.exit(1);
  }
}

main().catch(console.error);
```

How to Run:

1. Save the code in a file (e.g., `chat_demo.ts`)
2. Create a `.env` file:

   ```
   JAMAI_PROJECT_ID=your_project_id
   JAMAI_API_KEY=your_pat_token
   ```
3. Install required packages:

   ```bash
   npm install jamaibase dotenv
   npm install --save-dev @types/node typescript ts-node
   ```
4. Replace the following values in the code:
   * `AGENT_ID` with your agent ID created in the UI (or use environment variable)
5. Run the application:

   ```bash
   ts-node chat_demo.ts
   ```

Or compile and run:

```bash
tsc chat_demo.ts
node chat_demo.js
```

Using the Chat Demo:

* Type your message and press Enter to chat
* Type `new` to start a new chat session
* Type `exit` to quit the application
  {% endstep %}

{% step %}

### Web-Based Chat Example (Express + HTML)

Server (TypeScript):

```typescript
import JamAI from "jamaibase";
import express from "express";
import * as dotenv from "dotenv";

dotenv.config();

const app = express();
const PORT = 3000;

const PROJECT_ID = process.env.JAMAI_PROJECT_ID || "your_project_id";
const PAT = process.env.JAMAI_API_KEY || "your_pat_token";
const AGENT_ID = "example_agent";

const jamai = new JamAI({
  projectId: PROJECT_ID,
  token: PAT,
});

app.use(express.json());
app.use(express.static("public"));

// Create new chat session
app.post("/api/chat/new", async (req, res) => {
  try {
    const timestamp = Date.now();
    const newTableId = `Chat_${timestamp}`;

    await jamai.table.duplicateTable({
      table_type: "chat",
      table_id_src: AGENT_ID,
      table_id_dst: newTableId,
      include_data: true,
      create_as_child: true,
    });

    res.json({ chatId: newTableId });
  } catch (error) {
    res.status(500).json({ error: String(error) });
  }
});

// Send message
app.post("/api/chat/message", async (req, res) => {
  try {
    const { chatId, message } = req.body;

    const response = await jamai.table.addRow({
      table_type: "chat",
      table_id: chatId,
      data: [{ User: message }],
      concurrent: false,
    });

    const aiResponse =
      response.rows[0]?.columns["AI"]?.choices[0]?.message?.content;
    res.json({ response: aiResponse });
  } catch (error) {
    res.status(500).json({ error: String(error) });
  }
});

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});
```

Save the following HTML in `public/index.html`:

```html
<!DOCTYPE html>
<html>
  <head>
    <title>JamAI Chat Demo</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 800px;
        margin: 50px auto;
      }
      #chat {
        border: 1px solid #ccc;
        height: 400px;
        overflow-y: scroll;
        padding: 10px;
        margin-bottom: 10px;
      }
      .message {
        margin: 10px 0;
        padding: 10px;
        border-radius: 5px;
      }
      .user {
        background: #e3f2fd;
        text-align: right;
      }
      .assistant {
        background: #f5f5f5;
      }
      #input {
        width: 80%;
        padding: 10px;
      }
      button {
        padding: 10px 20px;
      }
    </style>
  </head>
  <body>
    <h1>JamAI Chat Demo</h1>
    <div id="chat"></div>
    <input type="text" id="input" placeholder="Type your message..." />
    <button onclick="sendMessage()">Send</button>
    <button onclick="newChat()">New Chat</button>

    <script>
      let chatId = null;

      async function newChat() {
        const response = await fetch("/api/chat/new", { method: "POST" });
        const data = await response.json();
        chatId = data.chatId;
        document.getElementById("chat").innerHTML = "";
        addMessage("assistant", "Hello! How can I help you today?");
      }

      async function sendMessage() {
        const input = document.getElementById("input");
        const message = input.value.trim();
        if (!message || !chatId) return;

        addMessage("user", message);
        input.value = "";

        const response = await fetch("/api/chat/message", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ chatId, message }),
        });

        const data = await response.json();
        addMessage("assistant", data.response);
      }

      function addMessage(role, content) {
        const chat = document.getElementById("chat");
        const div = document.createElement("div");
        div.className = `message ${role}`;
        div.textContent = content;
        chat.appendChild(div);
        chat.scrollTop = chat.scrollHeight;
      }

      // Initialize with new chat
      newChat();
    </script>
  </body>
</html>
```

Run the server:

```bash
npm install express dotenv
ts-node server.ts
```

Then open `http://localhost:3000` in your browser.
{% endstep %}

{% step %}

### Summary

This example provides:

* A simple chat interface (CLI and Web-based)
* Streaming and non-streaming responses
* New chat session creation
* Basic error handling

Note: Make sure you have created your agent in the JamAI Base UI before running this code, as it relies on duplicating an existing agent table.

This example serves as a great starting point for building more complex chat applications with JamAI Base using TypeScript/JavaScript.
{% endstep %}
{% endstepper %}


# Quick Start: Action Table

What are Multimodal Action Tables?

Action tables can process various types of input (text, images, audio) and generate structured outputs using VLM/multimodal LLM. They're particularly useful for:

* Document Processing
* Text / Image / Audio Content Analysis
* Data Extraction
* Automated Content Generation


# Action Table - Image

## 1. Introduction

This tutorial will guide you through using the JamAI Base TypeScript SDK to create a simple receipt information extraction system.

### What We'll Build

Snap a photo of your receipt and let AI do the expense report for you!

In this tutorial, we'll create a receipt processing system that:

* Takes a receipt image as input
* Uploads it to JamAI Base action table
* Extracts key information like:
  * Shop name
  * Total amount

### Prerequisites

Before starting, you'll need:

* Node.js 16.x or higher installed
* Project ID and Personal Access Token (PAT)

## 2. Installation and Setup

### Installing the SDK

```bash
npm install jamaibase
```

### Basic Configuration

Get your Personal Access Token (PAT) here:

Get your Project ID here:

```typescript
import JamAI from "jamaibase";

const PROJECT_ID = "your_project_id";
const PAT = "your_PAT";

const client = new JamAI({
  projectId: PROJECT_ID,
  token: PAT,
});
```

## 3. Creating Your Action Table

{% stepper %}
{% step %}

### Create action table

* Navigate to your JamAI Base action table tab.
* Create a new action table named "receipt".
  {% endstep %}

{% step %}

### Configure columns

Configure the following columns:

```
Input Column:
- Name: "Image"
- Type: FILE
```

{% endstep %}

{% step %}

### Configure output columns

```
Output Columns:
- Name: "Shop Name"
  Type: str
  ☑️ Output checkbox enabled

- Name: "Total"
  Type: str
  ☑️ Output checkbox enabled
```

{% endstep %}
{% endstepper %}

## 4. Basic Implementation

### 4.1 Simple Receipt Processor

```typescript
async function processSingleReceipt(imagePath: string) {
  // Upload image file
  const fileResponse = await client.file.uploadFile({
    file_path: imagePath,
  });

  // Process in action table
  const response = await client.table.addRow({
    table_type: "action",
    table_id: "receipt",
    data: [{ Image: fileResponse.uri }],
    concurrent: false,
  });

  // Extract results
  return {
    shopName:
      response.rows[0]?.columns["Shop Name"]?.choices[0]?.message?.content,
    total: response.rows[0]?.columns["Total"]?.choices[0]?.message?.content,
  };
}
```

### 4.2 Complete Implementation with Error Handling

```typescript
import JamAI from "jamaibase";
import * as fs from "fs";
import * as path from "path";

class ReceiptProcessor {
  private client: JamAI;

  constructor(projectId: string, pat: string) {
    this.client = new JamAI({
      projectId: projectId,
      token: pat,
    });
  }

  validateImage(imagePath: string): boolean {
    // Validate if file exists and has correct extension
    if (!fs.existsSync(imagePath)) {
      throw new Error(`Image not found: ${imagePath}`);
    }

    const validExtensions = [".jpg", ".jpeg", ".png", ".webp", ".gif"];
    const fileExt = path.extname(imagePath).toLowerCase();
    if (!validExtensions.includes(fileExt)) {
      throw new Error(
        `Unsupported file format. Use: ${validExtensions.join(", ")}`
      );
    }

    return true;
  }

  async processReceipt(
    imagePath: string
  ): Promise<{ shopName: string; total: string } | null> {
    try {
      // Validate image
      this.validateImage(imagePath);

      // Upload file
      console.log("Uploading image...");
      const fileResponse = await this.client.file.uploadFile({
        file_path: imagePath,
      });
      console.log(`Upload successful: ${fileResponse.uri}`);

      // Process in action table
      console.log("Processing receipt...");
      const response = await this.client.table.addRow({
        table_type: "action",
        table_id: "receipt",
        data: [{ Image: fileResponse.uri }],
        concurrent: false,
      });

      // Extract and return results
      const results = {
        shopName: String(
          response.rows[0]?.columns["Shop Name"]?.choices[0]?.message
            ?.content ?? ""
        ),
        total: String(
          response.rows[0]?.columns["Total"]?.choices[0]?.message?.content ?? ""
        ),
      };
      console.log("Processing complete!");
      return results;
    } catch (error) {
      console.error(`Error processing receipt: ${error}`);
      return null;
    }
  }
}
```

## 5. Usage Examples

### 5.1 Basic Usage

```typescript
// Initialize processor
const processor = new ReceiptProcessor(PROJECT_ID, PAT);

// Process single receipt
const result = await processor.processReceipt("path/to/receipt.jpg");

if (result) {
  console.log(`Shop Name: ${result.shopName}`);
  console.log(`Total: ${result.total}`);
}
```

### 5.2 Batch Processing

```typescript
async function processReceiptBatch(receiptFolder: string) {
  const processor = new ReceiptProcessor(PROJECT_ID, PAT);
  const results = [];

  const files = fs.readdirSync(receiptFolder);

  for (const filename of files) {
    if (filename.toLowerCase().match(/\.(jpg|jpeg|png|webp|gif)$/)) {
      const imagePath = path.join(receiptFolder, filename);
      const result = await processor.processReceipt(imagePath);
      if (result) {
        results.push({
          filename: filename,
          ...result,
        });
      }
    }
  }

  return results;
}

// Usage
const results = await processReceiptBatch("path/to/receipt/folder");
for (const result of results) {
  console.log(`File: ${result.filename}`);
  console.log(`Shop: ${result.shopName}`);
  console.log(`Total: ${result.total}`);
  console.log("---");
}
```

## 6. Best Practices

* Error Handling
  * Always validate input files
  * Handle network errors gracefully
* Performance
  * Reuse the client instance
  * Consider batch processing for multiple files
  * Implement rate limiting for large batches
* Security
  * Use environment variables for credentials

## Complete Standalone Example

Save this as `receipt_processor.ts`:

```typescript
import JamAI from "jamaibase";
import * as fs from "fs";
import * as path from "path";

class ReceiptProcessor {
  private client: JamAI;

  constructor(projectId: string, pat: string) {
    this.client = new JamAI({
      projectId: projectId,
      token: pat,
    });
  }

  validateImage(imagePath: string): boolean {
    if (!fs.existsSync(imagePath)) {
      throw new Error(`Image not found: ${imagePath}`);
    }

    const validExtensions = [".jpg", ".jpeg", ".png"];
    const fileExt = path.extname(imagePath).toLowerCase();
    if (!validExtensions.includes(fileExt)) {
      throw new Error(
        `Unsupported file format. Use: ${validExtensions.join(", ")}`
      );
    }

    return true;
  }

  async processReceipt(
    imagePath: string
  ): Promise<{ shopName: string; total: string } | null> {
    try {
      this.validateImage(imagePath);

      console.log(`Processing receipt: ${imagePath}`);
      console.log("Uploading image...");
      const fileResponse = await this.client.file.uploadFile({
        file_path: imagePath,
      });
      console.log("Upload successful!");

      console.log("Extracting information...");
      const response = await this.client.table.addRow({
        table_type: "action",
        table_id: "receipt",
        data: [{ Image: fileResponse.uri }],
        concurrent: false,
      });

      const results = {
        shopName: String(
          response.rows[0]?.columns["Shop Name"]?.choices[0]?.message
            ?.content ?? ""
        ),
        total: String(
          response.rows[0]?.columns["Total"]?.choices[0]?.message?.content ?? ""
        ),
      };
      return results;
    } catch (error) {
      console.error(`Error: ${error}`);
      return null;
    }
  }
}

async function processFolder(
  folderPath: string,
  processor: ReceiptProcessor
): Promise<void> {
  // Process all receipts in a folder
  if (!fs.existsSync(folderPath)) {
    console.log(`Folder not found: ${folderPath}`);
    return;
  }

  const results = [];
  const files = fs.readdirSync(folderPath);

  for (const filename of files) {
    if (filename.toLowerCase().match(/\.(jpg|jpeg|png)$/)) {
      const imagePath = path.join(folderPath, filename);
      const result = await processor.processReceipt(imagePath);
      if (result) {
        results.push({
          filename: filename,
          ...result,
        });
      }
    }
  }

  console.log("\n=== Processing Results ===");
  for (const result of results) {
    console.log(`\nFile: ${result.filename}`);
    console.log(`Shop Name: ${result.shopName}`);
    console.log(`Total: ${result.total}`);
  }
}

// Main execution
async function main() {
  // Get credentials from environment variables
  const PROJECT_ID = process.env.JAMAI_PROJECT_ID || "your_project_id";
  const PAT = process.env.JAMAI_API_KEY || "your_PAT";

  const processor = new ReceiptProcessor(PROJECT_ID, PAT);

  // Process a single receipt
  const singleResult = await processor.processReceipt("path/to/receipt.jpg");
  if (singleResult) {
    console.log("\nSingle Receipt Result:");
    console.log(`Shop Name: ${singleResult.shopName}`);
    console.log(`Total: ${singleResult.total}`);
  }

  // Or process a folder of receipts
  // await processFolder("path/to/receipt/folder", processor);
}

main().catch(console.error);
```

## Running the Example

{% stepper %}
{% step %}
Save the code as `receipt_processor.ts`
{% endstep %}

{% step %}
Create a `.env` file:

```
JAMAI_PROJECT_ID=your_project_id
JAMAI_API_KEY=your_PAT
```

{% endstep %}

{% step %}
Install dependencies:

```bash
npm install jamaibase dotenv
npm install --save-dev @types/node typescript ts-node
```

{% endstep %}

{% step %}
Run with:

```bash
ts-node receipt_processor.ts
```

Or compile and run:

```bash
tsc receipt_processor.ts
node receipt_processor.js
```

{% endstep %}
{% endstepper %}

## Troubleshooting

<details>

<summary>File not found error</summary>

* Check that the file path is correct
* Use absolute paths or ensure relative paths are correct

</details>

<details>

<summary>Unsupported file format</summary>

* Ensure image is in JPG, PNG, WEBP, or GIF format
* Check file extension matches actual file type

</details>

<details>

<summary>Upload timeout</summary>

* Check internet connection
* Verify file size is reasonable (< 10MB recommended)

</details>

<details>

<summary>API errors</summary>

* Verify your PAT and Project ID are correct
* Check that the action table "receipt" exists in your project

</details>


# Action Table - Document

## 1. Introduction

This tutorial will guide you through using the JamAI Base TypeScript SDK to create a simple news summarization system.

### What We'll Build

Get a news document and let AI tell you the main message!

In this tutorial, we'll create a news summarization system that:

* Takes a news document as input. The supported formats include: .csv, .tsv, .txt, .md, .doc, .docx, .pdf, .ppt, .pptx, .xls, .xlsx, .xml, .html, .json, .jsonl.
* Upload the document to JamAI Base action table.
* Obtain the key information like:
  * Summary
  * Tag

### Prerequisites

Before starting, you'll need:

* Node.js 16.x or higher installed
* Project ID and Personal Access Token (PAT)

## 2. Installation and Setup

### Installing the TypeScript SDK

```bash
npm install jamaibase
```

### Basic Configuration

Get your Personal Access Token (PAT) here:

Get your Project ID here:

```typescript
import JamAI from "jamaibase";

const PROJECT_ID = "your_project_id";
const PAT = "your_PAT";

const client = new JamAI({
  projectId: PROJECT_ID,
  token: PAT,
});
```

{% hint style="info" %}
You can use a .env file to manage your PROJECT\_ID and PAT.
{% endhint %}

## 3. Creating Your Action Table

For simplicity, you can set up your action table in the JamAI Base platform:

{% stepper %}
{% step %}

### Create the action table

* Navigate to your JamAI Base action table tab.
* Create a new action table named "news\_summarization".
  {% endstep %}

{% step %}

### Add columns

Create the following columns:

| Name          | Column Type | Data Type |
| ------------- | ----------- | --------- |
| document      | Input       | Document  |
| summary       | LLM Output  | Text      |
| tag           | LLM Output  | Text      |
| {% endstep %} |             |           |

{% step %}

### Update prompts for LLM Output columns

Update the Prompt for the LLM Output columns as follows:

| Column name      | Prompt                                                                                                                                                                                                                                                                                                                                                                                           |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| summary          | Table name: "news\_summarization"\n\ndocument: ${document}\n\nSummarize the article in not more than three sentences and not more than 50 words.\n\nProvide the summary in the number form.\n\nBe factual and do not hallucinate. Remember to act as a cell in a spreadsheet and provide concise, relevant information without explanations unless specifically requested.                       |
| tag              | Table name: "news\_summarization"\n\ndocument: ${document}\n\nProvide at most three tags that well represent the document. Each tag should not have more than three words.\n\nProvide the tag in the number form.\n\nBe factual and do not hallucinate. Remember to act as a cell in a spreadsheet and provide concise, relevant information without explanations unless specifically requested. |
| {% endstep %}    |                                                                                                                                                                                                                                                                                                                                                                                                  |
| {% endstepper %} |                                                                                                                                                                                                                                                                                                                                                                                                  |

## 4. Basic Implementation

### 4.1 Simple Document Processor

```typescript
async function processSingleNews(documentPath: string) {
  // Upload document file
  const fileResponse = await client.file.uploadFile({
    file_path: documentPath,
  });

  // Process in action table
  const response = await client.table.addRow({
    table_type: "action",
    table_id: "news_summarization",
    data: [{ document: fileResponse.uri }],
    concurrent: false,
  });

  // Extract results
  return {
    summary: response.rows[0]?.columns["summary"]?.choices[0]?.message?.content,
    tag: response.rows[0]?.columns["tag"]?.choices[0]?.message?.content,
  };
}
```

### 4.2 Complete Implementation with Error Handling

```typescript
import JamAI from "jamaibase";
import * as fs from "fs";
import * as path from "path";

class DocumentProcessor {
  private client: JamAI;

  constructor(projectId: string, pat: string) {
    this.client = new JamAI({
      projectId: projectId,
      token: pat,
    });
  }

  validateDocument(documentPath: string): boolean {
    // Validate if file exists and has correct extension
    if (!fs.existsSync(documentPath)) {
      throw new Error(`Document not found: ${documentPath}`);
    }

    const validExtensions = [
      ".csv",
      ".tsv",
      ".txt",
      ".md",
      ".doc",
      ".docx",
      ".pdf",
      ".ppt",
      ".pptx",
      ".xls",
      ".xlsx",
      ".xml",
      ".html",
      ".json",
      ".jsonl",
    ];

    const fileExt = path.extname(documentPath).toLowerCase();
    if (!validExtensions.includes(fileExt)) {
      throw new Error(
        `Unsupported file format. Use: ${validExtensions.join(", ")}`
      );
    }

    return true;
  }

  async processDocument(
    documentPath: string
  ): Promise<{ summary: string; tag: string } | null> {
    try {
      // Validate document
      this.validateDocument(documentPath);

      // Upload file
      console.log("Uploading document...");
      const fileResponse = await this.client.file.uploadFile({
        file_path: documentPath,
      });
      console.log(`Upload successful: ${fileResponse.uri}`);

      // Process in action table
      console.log("Processing document...");
      const response = await this.client.table.addRow({
        table_type: "action",
        table_id: "news_summarization",
        data: [{ document: fileResponse.uri }],
        concurrent: false,
      });

      // Extract and return results
      const results = {
        summary: String(
          response.rows[0]?.columns["summary"]?.choices[0]?.message?.content ??
            ""
        ),
        tag: String(
          response.rows[0]?.columns["tag"]?.choices[0]?.message?.content ?? ""
        ),
      };
      console.log("Processing complete!");
      return results;
    } catch (error) {
      console.error(`Error processing document: ${error}`);
      return null;
    }
  }
}
```

## 5. Usage Examples

### 5.1 Basic Usage

```typescript
// Initialize processor
const processor = new DocumentProcessor(PROJECT_ID, PAT);

// Process single document
const result = await processor.processDocument("path/to/news.txt");

if (result) {
  console.log(`Summary: ${result.summary}`);
  console.log(`Tag: ${result.tag}`);
}
```

### 5.2 Batch Processing

```typescript
async function processDocumentBatch(documentFolder: string) {
  const processor = new DocumentProcessor(PROJECT_ID, PAT);
  const results = [];

  const files = fs.readdirSync(documentFolder);

  for (const filename of files) {
    // Check if file has a supported document extension
    const validExtensions = [
      ".csv",
      ".tsv",
      ".txt",
      ".md",
      ".doc",
      ".docx",
      ".pdf",
      ".ppt",
      ".pptx",
      ".xls",
      ".xlsx",
      ".xml",
      ".html",
      ".json",
      ".jsonl",
    ];

    if (validExtensions.some((ext) => filename.toLowerCase().endsWith(ext))) {
      const documentPath = path.join(documentFolder, filename);
      const result = await processor.processDocument(documentPath);
      if (result) {
        results.push({
          filename: filename,
          ...result,
        });
      }
    }
  }

  return results;
}

// Usage
const results = await processDocumentBatch("path/to/documents/folder");
for (const result of results) {
  console.log(`File: ${result.filename}`);
  console.log(`Summary: ${result.summary}`);
  console.log(`Tag: ${result.tag}`);
  console.log("---");
}
```

## 6. Best Practices

* Error Handling
  * Always validate input files
  * Handle network errors gracefully
* Performance
  * Reuse the client instance
  * Consider batch processing for multiple files
  * Implement rate limiting for large batches
* Security
  * Use environment variables for credentials

## Complete Standalone Example

```typescript
import JamAI from "jamaibase";
import * as fs from "fs";
import * as path from "path";

class DocumentProcessor {
  private client: JamAI;

  constructor(projectId: string, pat: string) {
    this.client = new JamAI({
      projectId: projectId,
      token: pat,
    });
  }

  validateDocument(documentPath: string): boolean {
    if (!fs.existsSync(documentPath)) {
      throw new Error(`Document not found: ${documentPath}`);
    }

    const validExtensions = [
      ".csv",
      ".tsv",
      ".txt",
      ".md",
      ".doc",
      ".docx",
      ".pdf",
      ".ppt",
      ".pptx",
      ".xls",
      ".xlsx",
      ".xml",
      ".html",
      ".json",
      ".jsonl",
    ];

    const fileExt = path.extname(documentPath).toLowerCase();
    if (!validExtensions.includes(fileExt)) {
      throw new Error(
        `Unsupported file format. Use: ${validExtensions.join(", ")}`
      );
    }

    return true;
  }

  async processDocument(
    documentPath: string
  ): Promise<{ summary: string; tag: string } | null> {
    try {
      this.validateDocument(documentPath);

      console.log(`Processing document: ${documentPath}`);
      console.log("Uploading document...");
      const fileResponse = await this.client.file.uploadFile({
        file_path: documentPath,
      });
      console.log("Upload successful!");

      console.log("Extracting information...");
      const response = await this.client.table.addRow({
        table_type: "action",
        table_id: "news_summarization",
        data: [{ document: fileResponse.uri }],
        concurrent: false,
      });

      const results = {
        summary: String(
          response.rows[0]?.columns["summary"]?.choices[0]?.message?.content ??
            ""
        ),
        tag: String(
          response.rows[0]?.columns["tag"]?.choices[0]?.message?.content ?? ""
        ),
      };
      return results;
    } catch (error) {
      console.error(`Error: ${error}`);
      return null;
    }
  }
}

async function processFolder(
  folderPath: string,
  processor: DocumentProcessor
): Promise<void> {
  // Process all documents in a folder
  if (!fs.existsSync(folderPath)) {
    console.log(`Folder not found: ${folderPath}`);
    return;
  }

  const results = [];
  const files = fs.readdirSync(folderPath);

  for (const filename of files) {
    const validExtensions = [
      ".csv",
      ".tsv",
      ".txt",
      ".md",
      ".doc",
      ".docx",
      ".pdf",
      ".ppt",
      ".pptx",
      ".xls",
      ".xlsx",
      ".xml",
      ".html",
      ".json",
      ".jsonl",
    ];

    if (validExtensions.some((ext) => filename.toLowerCase().endsWith(ext))) {
      const documentPath = path.join(folderPath, filename);
      const result = await processor.processDocument(documentPath);
      if (result) {
        results.push({
          filename: filename,
          ...result,
        });
      }
    }
  }

  console.log("\n=== Processing Results ===");
  for (const result of results) {
    console.log(`\nFile: ${result.filename}`);
    console.log(`Summary: ${result.summary}`);
    console.log(`Tag: ${result.tag}`);
  }
}

// Main execution
async function main() {
  // Get credentials from environment variables
  const PROJECT_ID = process.env.JAMAI_PROJECT_ID || "your_project_id";
  const PAT = process.env.JAMAI_API_KEY || "your_PAT";

  const processor = new DocumentProcessor(PROJECT_ID, PAT);

  // Process a single document
  const singleResult = await processor.processDocument("path/to/news.txt");
  if (singleResult) {
    console.log("\nSingle Document Result:");
    console.log(`Summary: ${singleResult.summary}`);
    console.log(`Tag: ${singleResult.tag}`);
  }

  // Or process a folder of documents
  // await processFolder("path/to/documents/folder", processor);
}

main().catch(console.error);
```

## Running the Example

1. Save the code as `document_processor.ts`
2. Create a `.env` file:

   ```
   JAMAI_PROJECT_ID=your_project_id
   JAMAI_API_KEY=your_PAT
   ```
3. Run with:

   ```bash
   npm install dotenv
   ts-node document_processor.ts
   ```

Or compile and run:

```bash
tsc document_processor.ts
node document_processor.js
```


# Action Table - Audio

## Introduction

This tutorial will guide you through using the JamAI Base TypeScript SDK to create a simple audio information processing system.

What We'll Build

Automate tasks with audio! Imagine processing meeting recordings or voice memos automatically.

In this tutorial, we'll create an audio processing system that:

* Takes an audio file as input (e.g., `.mp3`, `.wav`).
* Uploads it to a JamAI Base action table.
* Extracts and generates key information such as:
  * Transcription of the audio content.
  * Summary of the audio.

Prerequisites

Before starting, you'll need:

* Node.js 16.x or higher installed
* Project ID and Personal Access Token (PAT)

## Installation and Setup

Installing the SDK

```bash
npm install jamaibase
```

Basic Configuration

Get your Personal Access Token (PAT) here:

Get your Project ID here:

```typescript
import JamAI from "jamaibase";

const PROJECT_ID = "your_project_id";
const PAT = "your_PAT";

const client = new JamAI({
  projectId: PROJECT_ID,
  token: PAT,
});
```

## Creating Your Action Table

{% stepper %}
{% step %}

### Create the action table

Navigate to your JamAI Base action table tab and create a new action table named "AudioProcessor".
{% endstep %}

{% step %}

### Configure columns

Configure the following columns:

Input Column:

* Name: "Audio"
* Type: Audio

Output Columns:

* Name: "Transcription" Type: Text ☑️ Output checkbox enabled
* Name: "Summary" Type: Text ☑️ Output checkbox enabled
  {% endstep %}

{% step %}

### Verify table

Create and confirm the table:
{% endstep %}
{% endstepper %}

## Basic Implementation

### 4.1 Simple Audio Processor

```typescript
async function processSingleAudio(audioPath: string) {
  // Upload audio file
  const fileResponse = await client.file.uploadFile({
    file_path: audioPath,
  });

  // Process in action table
  const response = await client.table.addRow({
    table_type: "action",
    table_id: "AudioProcessor",
    data: [{ Audio: fileResponse.uri }],
    concurrent: false,
  });

  // Extract results
  return {
    transcription:
      response.rows[0]?.columns["Transcription"]?.choices[0]?.message?.content,
    summary: response.rows[0]?.columns["Summary"]?.choices[0]?.message?.content,
  };
}
```

### 4.2 Complete Implementation with Error Handling

```typescript
import JamAI from "jamaibase";
import * as fs from "fs";
import * as path from "path";

class AudioProcessor {
  private client: JamAI;

  constructor(projectId: string, pat: string) {
    this.client = new JamAI({
      projectId: projectId,
      token: pat,
    });
  }

  validateAudio(audioPath: string): boolean {
    // Validate if file exists and has correct extension
    if (!fs.existsSync(audioPath)) {
      throw new Error(`Audio file not found: ${audioPath}`);
    }

    const validExtensions = [".mp3", ".wav"];
    const fileExt = path.extname(audioPath).toLowerCase();
    if (!validExtensions.includes(fileExt)) {
      throw new Error(
        `Unsupported file format. Use: ${validExtensions.join(", ")}`
      );
    }

    return true;
  }

  async processAudio(
    audioPath: string
  ): Promise<{ transcription: string; summary: string } | null> {
    try {
      // Validate audio
      this.validateAudio(audioPath);

      // Upload file
      console.log("Uploading audio...");
      const fileResponse = await this.client.file.uploadFile({
        file_path: audioPath,
      });
      console.log(`Upload successful: ${fileResponse.uri}`);

      // Process in action table
      console.log("Processing audio...");
      const response = await this.client.table.addRow({
        table_type: "action",
        table_id: "AudioProcessor",
        data: [{ Audio: fileResponse.uri }],
        concurrent: false,
      });

      const results = {
        transcription: String(
          response.rows[0]?.columns["Transcription"]?.choices[0]?.message
            ?.content ?? ""
        ),
        summary: String(
          response.rows[0]?.columns["Summary"]?.choices[0]?.message?.content ??
            ""
        ),
      };

      console.log("Processing complete!");
      return results;
    } catch (error) {
      console.error(`Error processing audio: ${error}`);
      return null;
    }
  }
}
```

## Usage Examples

### Basic Usage

```typescript
// Initialize processor
const processor = new AudioProcessor(PROJECT_ID, PAT);

// Process single audio
const result = await processor.processAudio("path/to/audio.mp3");

if (result) {
  console.log(`Transcription: ${result.transcription}`);
  console.log(`Summary: ${result.summary}`);
}
```

### Batch Processing

```typescript
async function processAudioBatch(audioFolder: string) {
  const processor = new AudioProcessor(PROJECT_ID, PAT);
  const results = [];

  const files = fs.readdirSync(audioFolder);

  for (const filename of files) {
    if (filename.toLowerCase().match(/\.(mp3|wav)$/)) {
      const audioPath = path.join(audioFolder, filename);
      const result = await processor.processAudio(audioPath);
      if (result) {
        results.push({
          filename: filename,
          ...result,
        });
      }
    }
  }

  return results;
}

// Usage
const results = await processAudioBatch("path/to/audio/folder");
for (const result of results) {
  console.log(`File: ${result.filename}`);
  console.log(`Transcription: ${result.transcription}`);
  console.log(`Summary: ${result.summary}`);
  console.log("---");
}
```

## Best Practices

Error Handling

* Always validate input audio files.
* Handle network errors gracefully.
* Consider adding specific error handling for audio processing failures (e.g., silence, corrupted files).

Performance

* Reuse the client instance.
* Consider batch processing for multiple files.
* Implement rate limiting for large batches if needed.

Security

* Use environment variables for credentials.

Complete Standalone Example

Save this as `audio_processor.ts`:

```typescript
import JamAI from "jamaibase";
import * as fs from "fs";
import * as path from "path";

class AudioProcessor {
  private client: JamAI;

  constructor(projectId: string, pat: string) {
    this.client = new JamAI({
      projectId: projectId,
      token: pat,
    });
  }

  validateAudio(audioPath: string): boolean {
    if (!fs.existsSync(audioPath)) {
      throw new Error(`Audio file not found: ${audioPath}`);
    }

    const validExtensions = [".mp3", ".wav"];
    const fileExt = path.extname(audioPath).toLowerCase();
    if (!validExtensions.includes(fileExt)) {
      throw new Error(
        `Unsupported file format. Use: ${validExtensions.join(", ")}`
      );
    }

    return true;
  }

  async processAudio(
    audioPath: string
  ): Promise<{ transcription: string; summary: string } | null> {
    try {
      this.validateAudio(audioPath);

      console.log(`Processing audio: ${audioPath}`);
      console.log("Uploading audio...");
      const fileResponse = await this.client.file.uploadFile({
        file_path: audioPath,
      });
      console.log("Upload successful!");

      console.log("Extracting information...");
      const response = await this.client.table.addRow({
        table_type: "action",
        table_id: "AudioProcessor",
        data: [{ Audio: fileResponse.uri }],
        concurrent: false,
      });

      const results = {
        transcription: String(
          response.rows[0]?.columns["Transcription"]?.choices[0]?.message
            ?.content ?? ""
        ),
        summary: String(
          response.rows[0]?.columns["Summary"]?.choices[0]?.message?.content ??
            ""
        ),
      };
      return results;
    } catch (error) {
      console.error(`Error: ${error}`);
      return null;
    }
  }
}

async function processFolder(
  folderPath: string,
  processor: AudioProcessor
): Promise<void> {
  // Process all audio files in a folder
  if (!fs.existsSync(folderPath)) {
    console.log(`Folder not found: ${folderPath}`);
    return;
  }

  const results = [];
  const files = fs.readdirSync(folderPath);

  for (const filename of files) {
    if (filename.toLowerCase().match(/\.(mp3|wav)$/)) {
      const audioPath = path.join(folderPath, filename);
      const result = await processor.processAudio(audioPath);
      if (result) {
        results.push({
          filename: filename,
          ...result,
        });
      }
    }
  }

  console.log("\n=== Processing Results ===");
  for (const result of results) {
    console.log(`\nFile: ${result.filename}`);
    console.log(`Transcription: ${result.transcription}`);
    console.log(`Summary: ${result.summary}`);
  }
}

// Main execution
async function main() {
  // Get credentials from environment variables
  const PROJECT_ID = "your_project_id";
  const PAT = "your_PAT";

  const processor = new AudioProcessor(PROJECT_ID, PAT);

  // Process a single audio file
  const singleResult = await processor.processAudio("path/to/audio.mp3");
  if (singleResult) {
    console.log("\nSingle Audio Result:");
    console.log(`Transcription: ${singleResult.transcription}`);
    console.log(`Summary: ${singleResult.summary}`);
  }

  // Or process a folder of audio files
  // await processFolder("path/to/audio/folder", processor);
}

main().catch(console.error);
```

## Running the Example

1. Save the code as `audio_processor.ts`
2. Create a `.env` file:

   ```
   JAMAI_PROJECT_ID=your_project_id
   JAMAI_API_KEY=your_PAT
   ```
3. Install dependencies:

   ```bash
   npm install jamaibase dotenv
   npm install --save-dev @types/node typescript ts-node
   ```
4. Run with:

   ```bash
   ts-node audio_processor.ts
   ```

Or compile and run:

```bash
tsc audio_processor.ts
node audio_processor.js
```

## Troubleshooting

<details>

<summary>File not found error</summary>

* Check that the file path is correct
* Use absolute paths or ensure relative paths are correct

</details>

<details>

<summary>Unsupported file format</summary>

* Ensure audio is in MP3 or WAV format
* Check file extension matches actual file type

</details>

<details>

<summary>Upload timeout</summary>

* Check internet connection
* Verify file size is reasonable (< 25MB recommended)
* Large audio files may take longer to upload

</details>

<details>

<summary>Transcription errors</summary>

* Check audio quality (clear speech, minimal background noise)
* Ensure audio file is not corrupted
* Verify audio contains speech content

</details>

<details>

<summary>API errors</summary>

* Verify your PAT and Project ID are correct
* Check that the action table "AudioProcessor" exists in your project
* Ensure output columns "Transcription" and "Summary" are configured correctly

</details>


# Examples


# AI Assistant with SvelteKit

Build an AI-powered chat assistant with SvelteKit and JamAI Base

### 1. Introduction

This comprehensive tutorial will guide you through building a full-stack AI chat assistant application using SvelteKit and the JamAI Base TypeScript SDK. The application uses JamAI Base Chat Tables to create an intelligent conversational AI that can process text and images.

#### What We'll Build

An AI-powered chat application that allows users to:

1. Create and manage multiple chat conversations
2. Send text messages to an AI assistant
3. Upload and share images in conversations
4. Receive real-time streaming responses from AI
5. View and organize conversation history
6. Experience a modern, responsive chat interface

#### Key Features

* **Modern SvelteKit** with file-based routing and server-side rendering
* **Svelte 5** with the latest runes API
* **TypeScript** for type safety
* **Tailwind CSS 4** for styling with modern UI
* **Real-time Streaming** for AI responses
* **Image Upload** with preview and validation
* **Conversation Management** with create, rename, and delete
* **Responsive Design** for mobile and desktop
* **JamAI Base Chat Tables** for AI conversation management

#### Prerequisites

Before starting, you'll need:

* Node.js 18.x or higher installed
* Basic knowledge of Svelte and TypeScript
* Project ID and Personal Access Token (PAT) from JamAI Base
* A code editor (VS Code recommended)

### 2. Project Setup

#### 2.1 Create SvelteKit Project

First, create a new SvelteKit project:

```bash
npx sv create ai-chat-assistant
cd ai-chat-assistant
```

When prompted, select:

* Which Svelte app template? **SvelteKit demo app**
* Add type checking with TypeScript? **Yes, using TypeScript syntax**
* Add ESLint? **Yes**
* Add Prettier? **Yes**
* Add Vitest? **No** (optional)
* Add Playwright? **No** (optional)

#### 2.2 Install Dependencies

Install the required packages:

```bash
npm install
npm install jamaibase
```

**Dependencies:**

* `jamaibase` - JamAI Base TypeScript SDK for AI operations
* `@tailwindcss/vite` - Tailwind CSS 4 with Vite integration

#### 2.3 Configure Tailwind CSS

Create `src/routes/layout.css`:

```css
@import "tailwindcss";
@import "@fontsource/fira-mono";

@layer base {
  body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
      "Helvetica Neue", Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
}
```

#### 2.4 Project Structure

Your project structure should look like this:

```
ai-chat-assistant/
├── src/
│   ├── lib/
│   │   ├── components/
│   │   │   ├── ChatMessages.svelte
│   │   │   ├── MessageInput.svelte
│   │   │   └── ChatSidebar.svelte
│   │   ├── config/
│   │   │   └── constants.ts
│   │   ├── server/
│   │   │   └── jamai-client.ts
│   │   ├── stores/
│   │   │   └── chat-store.ts
│   │   └── types.ts
│   ├── routes/
│   │   ├── api/
│   │   │   ├── conversations/
│   │   │   │   ├── +server.ts
│   │   │   │   └── [id]/
│   │   │   │       ├── +server.ts
│   │   │   │       └── messages/
│   │   │   │           └── +server.ts
│   │   │   └── upload/
│   │   │       └── +server.ts
│   │   ├── chat/
│   │   │   ├── +page.svelte
│   │   │   └── +layout.svelte
│   │   ├── +page.svelte
│   │   ├── +layout.svelte
│   │   └── layout.css
│   ├── app.d.ts
│   └── app.html
├── .env
├── .env.example
├── package.json
├── svelte.config.js
├── tsconfig.json
└── vite.config.ts
```

### 3. Getting Your Credentials

#### 3.1 Get Your Personal Access Token (PAT)

1. Go to <https://cloud.jamaibase.com/>
2. Click on your username in the top right
3. Select **Account Settings**
4. Navigate to **Personal Access Token** section
5. Click **Create Personal Access Token**
6. Copy and save your token securely

#### 3.2 Get Your Project ID

1. In the JamAI Base dashboard, navigate to your project
2. Look at the browser URL: `https://cloud.jamaibase.com/project/{PROJECT_ID}`
3. Copy the Project ID from the URL

#### 3.3 Configure Environment Variables

Create a `.env` file in your project root:

```bash
# JamAI Base Configuration
# Get your credentials from https://cloud.jamaibase.com/

# Your Personal Access Token (PAT)
JAMAI_API_KEY=your_api_key_here

# Your Project ID
JAMAI_PROJECT_ID=your_project_id_here
```

{% hint style="info" %}
**Important:** Never commit `.env` to version control. Add it to your `.gitignore` file.
{% endhint %}

### 4. Core Configuration

#### 4.1 Create Type Definitions

Create `src/lib/types.ts`:

```typescript
// Message types
export interface Message {
  id: string;
  role: "user" | "assistant";
  content: string;
  imageUri?: string;
  thumbUri?: string;
  timestamp: string;
}
```

#### 4.2 Create Constants Configuration

Create `src/lib/config/constants.ts`:

```typescript
// Agent/Table Configuration
export const AGENT_CONFIG = {
  // The name of the chat agent (chat table)
  AGENT_ID: "simple-chat",

  // Table columns
  COLUMNS: {
    IMAGE: "Image",
    USER: "User",
    AI: "AI",
  },

  // LLM Configuration
  MODEL: "openai/gpt-5-mini",
  SYSTEM_PROMPT: "You are a helpful AI assistant.",
  MAX_TOKENS: 2000,
  TEMPERATURE: 0.7,
};

// File Upload Configuration
export const UPLOAD_CONFIG = {
  // Maximum file size in bytes (10MB)
  MAX_FILE_SIZE: 10 * 1024 * 1024,

  // Allowed image MIME types
  ALLOWED_MIME_TYPES: [
    "image/jpeg",
    "image/jpg",
    "image/png",
    "image/gif",
    "image/webp",
  ],

  // Allowed file extensions
  ALLOWED_EXTENSIONS: [".jpeg", ".jpg", ".png", ".gif", ".webp"],
};

// API Endpoint Paths
export const API_ROUTES = {
  CONVERSATIONS: "/api/conversations",
  UPLOAD: "/api/upload",
  getConversation: (id: string) => `/api/conversations/${id}`,
  getMessages: (id: string) => `/api/conversations/${id}/messages`,
};

// UI Constants
export const UI_CONFIG = {
  // Sidebar width
  SIDEBAR_WIDTH: "280px",

  // Max conversation title length
  MAX_TITLE_LENGTH: 50,

  // Messages per page
  MESSAGES_PER_PAGE: 50,

  // Auto-scroll delay (ms)
  AUTO_SCROLL_DELAY: 100,
};
```

**Key Configuration Points:**

* **AGENT\_ID**: The name of your Chat Table in JamAI Base
* **MODEL**: The LLM model to use (gpt-4o-mini is cost-effective and fast)
* **SYSTEM\_PROMPT**: Defines the AI assistant's personality and behavior
* **COLUMNS**: Maps to the Chat Table columns in JamAI Base

#### 4.3 Initialize JamAI Client

Create `src/lib/server/jamai-client.ts`:

```typescript
import JamAI from "jamaibase";
import { JAMAI_API_KEY, JAMAI_PROJECT_ID } from "$env/static/private";

// Singleton JamAI client instance
let jamaiClient: JamAI | null = null;

/**
 * Get the JamAI client instance (singleton pattern)
 * Initializes the client with environment variables on first call
 */
export function getJamaiClient(): JamAI {
  if (!jamaiClient) {
    if (!JAMAI_API_KEY || !JAMAI_PROJECT_ID) {
      throw new Error(
        "Missing required environment variables: JAMAI_API_KEY and JAMAI_PROJECT_ID must be set"
      );
    }

    jamaiClient = new JamAI({
      token: JAMAI_API_KEY,
      projectId: JAMAI_PROJECT_ID,
    });
  }

  return jamaiClient;
}

/**
 * Helper function to extract values from wrapped row data
 * JamAI SDK wraps values in { value: ... } objects
 */
export function getValue(field: any): any {
  if (field && typeof field === "object" && "value" in field) {
    return field.value;
  }
  return field;
}
```

**Key Points:**

* Uses singleton pattern for efficient client management
* Validates environment variables on initialization
* Uses SvelteKit's `$env/static/private` for secure server-side env vars
* Provides helper function to unwrap JamAI's data structure

### 5. Building the API Routes

#### 5.1 Conversations List & Create Route

Create `src/routes/api/conversations/+server.ts`:

```typescript
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { getJamaiClient } from "$lib/server/jamai-client";
import { AGENT_CONFIG } from "$lib/config/constants";

/**
 * Ensure agent exists before any operation
 */
async function ensureAgentExists() {
  const jamai = getJamaiClient();

  try {
    await jamai.table.getTable({
      table_type: "chat",
      table_id: AGENT_CONFIG.AGENT_ID,
    });
  } catch (_error) {
    // Agent doesn't exist, create it
    await jamai.table.createChatTable({
      id: AGENT_CONFIG.AGENT_ID,
      cols: [
        {
          id: AGENT_CONFIG.COLUMNS.IMAGE,
          dtype: "image",
        },
        {
          id: AGENT_CONFIG.COLUMNS.USER,
          dtype: "str",
        },
        {
          id: AGENT_CONFIG.COLUMNS.AI,
          dtype: "str",
          gen_config: {
            model: AGENT_CONFIG.MODEL,
            system_prompt: AGENT_CONFIG.SYSTEM_PROMPT,
            prompt: `User message: \${${AGENT_CONFIG.COLUMNS.USER}}\n\nImage: \${${AGENT_CONFIG.COLUMNS.IMAGE}}`,
            max_tokens: AGENT_CONFIG.MAX_TOKENS,
            temperature: AGENT_CONFIG.TEMPERATURE,
          },
        },
      ],
    });
  }
}

/**
 * GET /api/conversations
 * List all conversations
 */
export const GET: RequestHandler = async () => {
  try {
    await ensureAgentExists();

    const jamai = getJamaiClient();
    const conversations = await jamai.conversations.listConversations();

    const filteredConversation = conversations?.items?.filter(
      (c) => c.parent_id === AGENT_CONFIG.AGENT_ID
    );

    return json({ conversations: filteredConversation || [] });
  } catch (error) {
    console.error("GET /api/conversations error:", error);
    return json({ error: String(error) }, { status: 500 });
  }
};

/**
 * POST /api/conversations
 * Create a new conversation
 * Body: { title?: string }
 */
export const POST: RequestHandler = async ({ request }) => {
  try {
    await ensureAgentExists();

    const body = await request.json();
    const { title } = body;

    const jamai = getJamaiClient();
    const conversationStream = await jamai.conversations.createConversation({
      agent_id: AGENT_CONFIG.AGENT_ID,
      title: title || "New Chat",
      data: {},
    });

    // Read the stream to get conversation ID
    const reader = conversationStream.getReader();
    let conversationId: string | null = null;

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        if (value && typeof value === "object" && "conversation_id" in value) {
          conversationId = value.conversation_id as string;
          break;
        }
      }
    } finally {
      reader.releaseLock();
    }

    if (!conversationId) {
      throw new Error("Failed to get conversation ID from stream");
    }

    return json(
      {
        conversation: {
          conversation_id: conversationId,
          title: title || "New Chat",
        },
      },
      { status: 201 }
    );
  } catch (error) {
    console.error("POST /api/conversations error:", error);
    return json({ error: String(error) }, { status: 500 });
  }
};
```

**What this does:**

* **ensureAgentExists()**: Automatically creates the Chat Table if it doesn't exist
* **GET**: Lists all conversations associated with the agent
* **POST**: Creates a new conversation with an optional title
* Handles streaming response to extract conversation ID

#### 5.2 Single Conversation Route (Get, Delete & Rename)

Create `src/routes/api/conversations/[id]/+server.ts`:

```typescript
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { getJamaiClient } from "$lib/server/jamai-client";

/**
 * GET /api/conversations/[id]
 * Get a specific conversation
 */
export const GET: RequestHandler = async ({ params }) => {
  try {
    const { id } = params;
    const jamai = getJamaiClient();
    const conversation = await jamai.conversations.getConversation(id);
    return json({ conversation });
  } catch (error) {
    console.error("GET /api/conversations/[id] error:", error);
    return json({ error: String(error) }, { status: 500 });
  }
};

/**
 * DELETE /api/conversations/[id]
 * Delete a conversation
 */
export const DELETE: RequestHandler = async ({ params }) => {
  try {
    const { id } = params;
    const jamai = getJamaiClient();

    await jamai.conversations.deleteConversation(id);

    return json({ ok: true });
  } catch (error) {
    console.error("DELETE /api/conversations/[id] error:", error);
    return json({ error: String(error) }, { status: 500 });
  }
};

/**
 * PATCH /api/conversations/[id]
 * Rename a conversation
 * Body: { title: string }
 */
export const PATCH: RequestHandler = async ({ params, request }) => {
  try {
    const { id } = params;
    const body = await request.json();
    const { title } = body;

    if (!title) {
      return json({ error: "Title is required" }, { status: 400 });
    }

    const jamai = getJamaiClient();
    const conversation = await jamai.conversations.renameConversationTitle(
      id,
      title
    );
    return json({ conversation });
  } catch (error) {
    console.error("PATCH /api/conversations/[id] error:", error);
    return json({ error: String(error) }, { status: 500 });
  }
};
```

#### 5.3 Messages Route (List & Send)

Create `src/routes/api/conversations/[id]/messages/+server.ts`:

```typescript
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { getJamaiClient, getValue } from "$lib/server/jamai-client";
import { AGENT_CONFIG } from "$lib/config/constants";
import type { Message } from "$lib/types";

/**
 * GET /api/conversations/[id]/messages
 * List messages in a conversation
 */
export const GET: RequestHandler = async ({ params }) => {
  try {
    const { id } = params;
    const jamai = getJamaiClient();
    const response = await jamai.conversations.listMessages(id, {});

    const messages: Message[] = [];
    const imageUris: string[] = [];
    const imageUriToMessageIndex: Map<string, number> = new Map();

    // Process rows to extract user and AI messages
    if (response && response.items) {
      for (const row of response.items) {
        const rowId = getValue(row["ID"]) || row.ID;
        const timestamp = getValue(row["Updated at"]) || row["Updated at"];

        // User message
        const userContent = getValue(row[AGENT_CONFIG.COLUMNS.USER]);
        const imageUri = getValue(row[AGENT_CONFIG.COLUMNS.IMAGE]);

        // Only add user message if content is not empty
        if (userContent && userContent.trim()) {
          const messageIndex = messages.length;
          messages.push({
            id: `${rowId}-user`,
            role: "user",
            content: userContent,
            imageUri: imageUri || undefined,
            timestamp,
          });

          // Track image URI for batch thumbnail fetching
          if (imageUri && imageUri.trim()) {
            imageUris.push(imageUri);
            imageUriToMessageIndex.set(imageUri, messageIndex);
          }
        }

        // AI message
        const aiContent = getValue(row[AGENT_CONFIG.COLUMNS.AI]);
        // Only add AI message if content is not empty
        if (aiContent && aiContent.trim()) {
          messages.push({
            id: `${rowId}-ai`,
            role: "assistant",
            content: aiContent,
            timestamp,
          });
        }
      }
    }

    // Fetch thumbnail URLs in a single batch call if there are any images
    if (imageUris.length > 0) {
      try {
        const thumbUriResponse = await jamai.file.getThumbUrls({
          uris: imageUris,
        });

        // Map thumbnail URLs back to messages
        if (thumbUriResponse.urls) {
          imageUris.forEach((uri, index) => {
            const messageIndex = imageUriToMessageIndex.get(uri);
            if (messageIndex !== undefined && thumbUriResponse.urls[index]) {
              messages[messageIndex].thumbUri = thumbUriResponse.urls[index];
            }
          });
        }
      } catch (thumbError) {
        console.error("Failed to fetch thumbnail URLs:", thumbError);
        // Continue without thumbnail URLs - don't fail the entire request
      }
    }

    return json({ messages });
  } catch (error) {
    console.error("GET /api/conversations/[id]/messages error:", error);
    return json({ error: String(error) }, { status: 500 });
  }
};

/**
 * POST /api/conversations/[id]/messages
 * Send a message to a conversation (returns streaming response)
 * Body: { text: string, imageUri?: string }
 */
export const POST: RequestHandler = async ({ params, request }) => {
  try {
    const { id } = params;
    const body = await request.json();
    const { text, imageUri } = body;

    if (!text) {
      return json({ error: "Text is required" }, { status: 400 });
    }

    const jamai = getJamaiClient();

    // Prepare message data
    const messageData: any = {
      [AGENT_CONFIG.COLUMNS.USER]: text,
    };

    // Add image if provided
    if (imageUri) {
      messageData[AGENT_CONFIG.COLUMNS.IMAGE] = imageUri;
    }

    // Send message with streaming
    const stream = await jamai.conversations.sendMessage({
      conversation_id: id,
      data: messageData,
    });

    // Create a ReadableStream to pipe the JamAI response to the client
    const readableStream = new ReadableStream({
      async start(controller) {
        try {
          const reader = stream.getReader();

          while (true) {
            const { done, value } = await reader.read();

            if (done) {
              controller.close();
              break;
            }

            // Convert the chunk to JSON string and send it
            const chunk = JSON.stringify(value) + "\n";
            controller.enqueue(new TextEncoder().encode(chunk));
          }
        } catch (error) {
          console.error("Streaming error:", error);
          controller.error(error);
        }
      },
    });

    // Return the streaming response
    return new Response(readableStream, {
      headers: {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        Connection: "keep-alive",
      },
    });
  } catch (error) {
    console.error("POST /api/conversations/[id]/messages error:", error);
    return json({ error: String(error) }, { status: 500 });
  }
};
```

**What this does:**

* **GET**: Fetches all messages in a conversation, unwrapping JamAI's data structure
* Fetches thumbnail URLs for images in batch for performance
* **POST**: Sends a new message and returns a streaming response
* Handles both text and image inputs
* Streams AI responses in real-time

#### 5.4 File Upload Route

Create `src/routes/api/upload/+server.ts`:

```typescript
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { getJamaiClient } from "$lib/server/jamai-client";
import { UPLOAD_CONFIG } from "$lib/config/constants";

/**
 * POST /api/upload
 * Upload an image file
 * Form data: { file: File }
 */
export const POST: RequestHandler = async ({ request }) => {
  try {
    const formData = await request.formData();
    const file = formData.get("file");

    if (!file || !(file instanceof File)) {
      return json({ error: "File is required" }, { status: 400 });
    }

    // Validate file type
    const fileType = file.type.toLowerCase();
    if (!UPLOAD_CONFIG.ALLOWED_MIME_TYPES.includes(fileType)) {
      return json(
        {
          error: "Invalid file type",
          details: `Allowed types: ${UPLOAD_CONFIG.ALLOWED_MIME_TYPES.join(
            ", "
          )}`,
        },
        { status: 400 }
      );
    }

    // Validate file size
    if (file.size > UPLOAD_CONFIG.MAX_FILE_SIZE) {
      return json(
        {
          error: "File too large",
          details: `Maximum file size is ${
            UPLOAD_CONFIG.MAX_FILE_SIZE / 1024 / 1024
          }MB`,
        },
        { status: 413 }
      );
    }

    const jamai = getJamaiClient();

    // Upload to JamAI Base, passing the File directly
    const response = await jamai.file.uploadFile({
      file, // pass File object directly
    });

    const thumbUriResponse = await jamai.file.getThumbUrls({
      uris: [response.uri],
    });

    return json(
      { uri: response.uri, thumbUri: thumbUriResponse.urls?.[0] ?? "" },
      { status: 200 }
    );
  } catch (error) {
    console.error("POST /api/upload error:", error);

    if (error instanceof Error) {
      return json(
        {
          error: "Upload failed",
          details: error.message,
        },
        { status: 500 }
      );
    }

    return json({ error: "Internal server error" }, { status: 500 });
  }
};
```

**What this does:**

* Accepts multipart form data with an image file
* Validates file type and size
* Uploads to JamAI Base storage
* Generates and returns thumbnail URL for preview

### 6. Building the Store

#### 6.1 Create Chat Store

Create `src/lib/stores/chat-store.ts`:

```typescript
import { writable, derived, get } from "svelte/store";
import { API_ROUTES } from "$lib/config/constants";
import type { Message } from "$lib/types";

export interface Conversation {
  conversation_id: string;
  title: string;
  agent_id?: string;
  created_at?: string;
  updated_at?: string;
}

// Store for all conversations
export const conversations = writable<Conversation[]>([]);

// Store for current active conversation ID
export const currentConversationId = writable<string | null>(null);

// Store for messages in the current conversation
export const messages = writable<Message[]>([]);

// Loading state
export const isLoading = writable(false);

// Error state
export const error = writable<string | null>(null);

// Streaming state
export const isStreaming = writable(false);

/**
 * Load all conversations from the API
 */
export async function loadConversations() {
  try {
    isLoading.set(true);
    error.set(null);

    const response = await fetch(API_ROUTES.CONVERSATIONS);
    const data = await response.json();

    if (!response.ok) {
      throw new Error(data.error || "Failed to load conversations");
    }

    conversations.set(data.conversations || []);
  } catch (err) {
    console.error("Failed to load conversations:", err);
    error.set(String(err));
  } finally {
    isLoading.set(false);
  }
}

/**
 * Select a conversation and load its messages
 */
export async function selectConversation(conversationId: string) {
  try {
    isLoading.set(true);
    error.set(null);

    currentConversationId.set(conversationId);

    // Load messages for this conversation
    const response = await fetch(API_ROUTES.getMessages(conversationId));
    const data = await response.json();

    if (!response.ok) {
      throw new Error(data.error || "Failed to load messages");
    }

    messages.set(data.messages || []);
  } catch (err) {
    console.error("Failed to select conversation:", err);
    error.set(String(err));
  } finally {
    isLoading.set(false);
  }
}

/**
 * Create a new conversation
 */
export async function createNewConversation(title?: string) {
  try {
    isLoading.set(true);
    error.set(null);

    const response = await fetch(API_ROUTES.CONVERSATIONS, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title: title || "New Chat" }),
    });

    const data = await response.json();

    if (!response.ok) {
      throw new Error(data.error || "Failed to create conversation");
    }

    // Add to conversations list
    const newConversation: Conversation = {
      conversation_id: data.conversation.conversation_id,
      title: data.conversation.title || "New Chat",
    };

    // Refetch all conversations to ensure we have the latest state from backend
    await loadConversations();

    // Select the new conversation and load its messages
    await selectConversation(newConversation.conversation_id);

    // Wait a bit for backend to initialize the conversation, then refetch
    setTimeout(async () => {
      const currentId = get(currentConversationId);
      if (currentId === newConversation.conversation_id) {
        await selectConversation(currentId);
      }
    }, 1000);

    return newConversation;
  } catch (err) {
    console.error("Failed to create conversation:", err);
    error.set(String(err));
    throw err;
  } finally {
    isLoading.set(false);
  }
}

/**
 * Delete a conversation
 */
export async function deleteConversation(conversationId: string) {
  try {
    error.set(null);

    const response = await fetch(API_ROUTES.getConversation(conversationId), {
      method: "DELETE",
    });

    if (!response.ok) {
      const data = await response.json();
      throw new Error(data.error || "Failed to delete conversation");
    }

    // Remove from conversations list
    conversations.update((convs) =>
      convs.filter((c) => c.conversation_id !== conversationId)
    );

    // If this was the current conversation, clear it
    const current = get(currentConversationId);
    if (current === conversationId) {
      currentConversationId.set(null);
      messages.set([]);
    }
  } catch (err) {
    console.error("Failed to delete conversation:", err);
    error.set(String(err));
    throw err;
  }
}

/**
 * Rename a conversation
 */
export async function renameConversation(
  conversationId: string,
  newTitle: string
) {
  try {
    error.set(null);

    const response = await fetch(API_ROUTES.getConversation(conversationId), {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title: newTitle }),
    });

    const data = await response.json();

    if (!response.ok) {
      throw new Error(data.error || "Failed to rename conversation");
    }

    // Update in conversations list
    conversations.update((convs) =>
      convs.map((c) =>
        c.conversation_id === conversationId ? { ...c, title: newTitle } : c
      )
    );
  } catch (err) {
    console.error("Failed to rename conversation:", err);
    error.set(String(err));
    throw err;
  }
}

/**
 * Send a message to the current conversation
 */
export async function sendMessage(text: string, imageFile?: File) {
  const conversationId = get(currentConversationId);

  if (!conversationId) {
    error.set("No conversation selected");
    return;
  }

  try {
    isStreaming.set(true);
    error.set(null);

    // Upload image if provided
    let imageUri: string | undefined;
    let thumbUri: string | undefined;
    if (imageFile) {
      const formData = new FormData();
      formData.append("file", imageFile);

      const uploadResponse = await fetch(API_ROUTES.UPLOAD, {
        method: "POST",
        body: formData,
      });

      const uploadData = await uploadResponse.json();

      if (!uploadResponse.ok) {
        throw new Error(uploadData.error || "Failed to upload image");
      }

      imageUri = uploadData.uri;
      thumbUri = uploadData.thumbUri;
    }

    // Add user message to UI immediately
    const userMessage: Message = {
      id: `temp-${Date.now()}`,
      role: "user",
      content: text,
      imageUri: thumbUri,
      timestamp: new Date().toISOString(),
    };

    messages.update((msgs) => [...msgs, userMessage]);

    // Send message with streaming
    const response = await fetch(API_ROUTES.getMessages(conversationId), {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text, imageUri }),
    });

    if (!response.ok) {
      throw new Error("Failed to send message");
    }

    // Handle streaming response
    const reader = response.body?.getReader();
    if (!reader) {
      throw new Error("No response stream");
    }

    const decoder = new TextDecoder();
    let aiMessageContent = "";

    // Add placeholder for AI message
    const aiMessageId = `ai-${Date.now()}`;
    messages.update((msgs) => [
      ...msgs,
      {
        id: aiMessageId,
        role: "assistant",
        content: "",
        timestamp: new Date().toISOString(),
      },
    ]);

    while (true) {
      const { done, value } = await reader.read();

      if (done) {
        break;
      }

      const chunk = decoder.decode(value, { stream: true });
      const lines = chunk.split("\n").filter((line) => line.trim());

      for (const line of lines) {
        try {
          const data = JSON.parse(line);

          // Extract content from streaming chunk
          if (
            data.object === "gen_table.completion.chunk" &&
            data.choices?.[0]?.message?.content
          ) {
            aiMessageContent += data.choices[0].message.content;

            // Update AI message content in real-time
            messages.update((msgs) =>
              msgs.map((msg) =>
                msg.id === aiMessageId
                  ? { ...msg, content: aiMessageContent }
                  : msg
              )
            );
          }
        } catch (parseError) {
          console.error("Failed to parse chunk:", parseError);
        }
      }
    }

    // Reload messages to get the final state with proper IDs
    await selectConversation(conversationId);
  } catch (err) {
    console.error("Failed to send message:", err);
    error.set(String(err));
  } finally {
    isStreaming.set(false);
  }
}

/**
 * Derived store for current conversation details
 */
export const currentConversation = derived(
  [conversations, currentConversationId],
  ([$conversations, $currentConversationId]) => {
    if (!$currentConversationId) return null;
    return (
      $conversations.find(
        (c) => c.conversation_id === $currentConversationId
      ) || null
    );
  }
);
```

**Key Features:**

* Manages all application state in a centralized store
* Provides functions for all chat operations
* Handles real-time streaming with message updates
* Uses Svelte's reactive stores for automatic UI updates

### 7. Building the UI Components

#### 7.1 Chat Messages Component

Create `src/lib/components/ChatMessages.svelte`:

```svelte
<script lang="ts">
	import { messages, isStreaming } from '$lib/stores/chat-store';
	import { tick } from 'svelte';

	let messagesContainer: HTMLDivElement;

	// Auto-scroll to bottom when new messages arrive
	$effect(() => {
		if ($messages.length > 0) {
			tick().then(() => {
				scrollToBottom();
			});
		}
	});

	function scrollToBottom() {
		if (messagesContainer) {
			messagesContainer.scrollTop = messagesContainer.scrollHeight;
		}
	}

	function formatTime(timestamp: string) {
		const date = new Date(timestamp);
		return date.toLocaleTimeString('en-US', {
			hour: '2-digit',
			minute: '2-digit'
		});
	}

	function formatMessageContent(content: string) {
		// Basic markdown rendering
		return content
			.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
			.replace(/\*(.*?)\*/g, '<em>$1</em>')
			.replace(/`(.*?)`/g, '<code>$1</code>')
			.replace(/\n/g, '<br>');
	}
</script>

<div
	class="scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 flex-1 overflow-y-auto bg-white p-6"
	bind:this={messagesContainer}
>
	{#if $messages.length === 0}
		<div class="flex h-full flex-col items-center justify-center p-8 text-center text-gray-600">
			<div class="mb-6 text-gray-300">
				<svg width="64" height="64" viewBox="0 0 64 64" fill="none">
					<path
						d="M32 8C18.7452 8 8 18.7452 8 32C8 38.5097 10.5226 44.4839 14.6452 48.9677L12.129 54.5806C11.8387 55.1613 12.2903 55.8387 12.9355 55.7742L21.0323 54.9677C24.2581 56.6129 27.9677 57.5484 32 57.5484C45.2548 57.5484 56 46.8032 56 33.5484C56 20.2935 45.2548 9.54839 32 9.54839V8Z"
						stroke="currentColor"
						stroke-width="2"
					/>
					<circle cx="24" cy="32" r="2" fill="currentColor" />
					<circle cx="32" cy="32" r="2" fill="currentColor" />
					<circle cx="40" cy="32" r="2" fill="currentColor" />
				</svg>
			</div>
			<h3 class="m-0 mb-2 text-2xl font-semibold text-gray-900">Start a Conversation</h3>
			<p class="m-0 text-base">Send a message to begin chatting with the AI assistant</p>
		</div>
	{:else}
		<div class="mx-auto max-w-3xl">
			{#each $messages as message (message.id)}
				<div class="mb-6 flex animate-[fadeIn_0.3s_ease-in] gap-4">
					<div
						class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-white {message.role ===
						'user'
							? 'bg-linear-to-br from-gray-600 to-gray-800'
							: 'bg-linear-to-br from-gray-500 to-gray-700'}"
					>
						{#if message.role === 'user'}
							<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
								<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="2" />
								<path
									d="M6 21C6 17.6863 8.68629 15 12 15C15.3137 15 18 17.6863 18 21"
									stroke="currentColor"
									stroke-width="2"
									stroke-linecap="round"
								/>
							</svg>
						{:else}
							<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
								<rect
									x="3"
									y="3"
									width="18"
									height="18"
									rx="4"
									stroke="currentColor"
									stroke-width="2"
								/>
								<circle cx="9" cy="10" r="1.5" fill="currentColor" />
								<circle cx="15" cy="10" r="1.5" fill="currentColor" />
								<path
									d="M9 15C9 15 10 16 12 16C14 16 15 15 15 15"
									stroke="currentColor"
									stroke-width="1.5"
									stroke-linecap="round"
								/>
							</svg>
						{/if}
					</div>

					<div class="min-w-0 flex-1">
						<div class="mb-2 flex items-center gap-3">
							<span class="text-sm font-semibold text-gray-900"
								>{message.role === 'user' ? 'You' : 'AI Assistant'}</span
							>
							<span class="text-xs text-gray-500">{formatTime(message.timestamp)}</span>
						</div>

						{#if message.thumbUri}
							<div class="mb-3">
								<img
									src={message.thumbUri}
									alt="Uploaded"
									class="max-h-75 max-w-xs rounded-lg border border-gray-200"
								/>
							</div>
						{/if}

						<div class="message-text leading-relaxed wrap-break-word text-gray-900">
							{@html formatMessageContent(message.content)}
						</div>
					</div>
				</div>
			{/each}

			{#if $isStreaming}
				<div class="mb-6 flex gap-4">
					<div
						class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-linear-to-br from-gray-500 to-gray-700 text-white"
					>
						<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
							<rect
								x="3"
								y="3"
								width="18"
								height="18"
								rx="4"
								stroke="currentColor"
								stroke-width="2"
							/>
						</svg>
					</div>
					<div class="min-w-0 flex-1">
						<div class="flex gap-1.5 py-3">
							<span
								class="h-2 w-2 animate-[bounce_1.4s_infinite_ease-in-out_both] rounded-full bg-gray-300 [animation-delay:-0.32s]"
							></span>
							<span
								class="h-2 w-2 animate-[bounce_1.4s_infinite_ease-in-out_both] rounded-full bg-gray-300 [animation-delay:-0.16s]"
							></span>
							<span
								class="h-2 w-2 animate-[bounce_1.4s_infinite_ease-in-out_both] rounded-full bg-gray-300"
							></span>
						</div>
					</div>
				</div>
			{/if}
		</div>
	{/if}
</div>

<style>
	.message-text :global(strong) {
		font-weight: 600;
	}

	.message-text :global(code) {
		background-color: #f5f5f5;
		padding: 0.125rem 0.375rem;
		border-radius: 0.25rem;
		font-family: 'Fira Mono', monospace;
		font-size: 0.9em;
	}

	/* Scrollbar styling for webkit browsers */
	.scrollbar-thin::-webkit-scrollbar {
		width: 8px;
	}

	.scrollbar-track-gray-100::-webkit-scrollbar-track {
		background: #f7f7f8;
	}

	.scrollbar-thumb-gray-300::-webkit-scrollbar-thumb {
		background: #d1d1d6;
		border-radius: 4px;
	}

	.scrollbar-thumb-gray-300::-webkit-scrollbar-thumb:hover {
		background: #b0b0b6;
	}
</style>
```

#### 7.2 Message Input Component

Create `src/lib/components/MessageInput.svelte`:

```svelte
<script lang="ts">
	import { sendMessage, isStreaming, currentConversationId } from '$lib/stores/chat-store';
	import { UPLOAD_CONFIG } from '$lib/config/constants';

	let textInput = $state('');
	let textareaElement: HTMLTextAreaElement;
	let selectedImage: File | null = $state(null);
	let imagePreviewUrl: string | null = $state(null);
	let fileInputElement: HTMLInputElement;
	let isSending = $state(false);
	let uploadError = $state<string | null>(null);

	function handleImageSelect(event: Event) {
		const target = event.target as HTMLInputElement;
		const file = target.files?.[0];

		if (!file) return;

		// Validate file type
		if (!UPLOAD_CONFIG.ALLOWED_MIME_TYPES.includes(file.type)) {
			uploadError = `Invalid file type. Allowed: ${UPLOAD_CONFIG.ALLOWED_EXTENSIONS.join(', ')}`;
			return;
		}

		// Validate file size
		const maxSizeMB = UPLOAD_CONFIG.MAX_FILE_SIZE / (1024 * 1024);
		if (file.size > UPLOAD_CONFIG.MAX_FILE_SIZE) {
			uploadError = `File too large. Max size: ${maxSizeMB}MB`;
			return;
		}

		uploadError = null;
		selectedImage = file;

		// Create preview URL
		const reader = new FileReader();
		reader.onload = (e) => {
			imagePreviewUrl = e.target?.result as string;
		};
		reader.readAsDataURL(file);
	}

	function removeImage() {
		selectedImage = null;
		imagePreviewUrl = null;
		uploadError = null;
		if (fileInputElement) {
			fileInputElement.value = '';
		}
	}

	async function handleSubmit() {
		if (isSending || $isStreaming || !$currentConversationId) return;

		const text = textInput.trim();
		if (!text && !selectedImage) return;

		isSending = true;
		uploadError = null;

		// Store image reference and clear input immediately
		const imageToSend = selectedImage || undefined;
		textInput = '';
		removeImage();

		// Reset textarea height
		if (textareaElement) {
			textareaElement.style.height = 'auto';
		}

		try {
			await sendMessage(text, imageToSend);
		} catch (error) {
			console.error('Failed to send message:', error);
			uploadError = String(error);
		} finally {
			isSending = false;
		}
	}

	function handleKeyDown(event: KeyboardEvent) {
		// Send on Enter (without Shift)
		if (event.key === 'Enter' && !event.shiftKey) {
			event.preventDefault();
			handleSubmit();
		}
	}

	function autoResize() {
		if (textareaElement) {
			textareaElement.style.height = 'auto';
			textareaElement.style.height = textareaElement.scrollHeight + 'px';
		}
	}

	function triggerFileInput() {
		fileInputElement?.click();
	}

	$effect(() => {
		autoResize();
	});
</script>

<div class="border-t border-gray-200 bg-white p-4">
	{#if uploadError}
		<div
			class="mb-3 flex items-center justify-between rounded-lg bg-red-50 px-4 py-3 text-sm text-red-700"
		>
			{uploadError}
			<button
				class="flex h-6 w-6 cursor-pointer items-center justify-center border-none bg-transparent p-0 text-2xl text-red-700 hover:opacity-70"
				onclick={() => (uploadError = null)}>×</button
			>
		</div>
	{/if}

	{#if imagePreviewUrl}
		<div class="relative mb-3 inline-block">
			<img
				src={imagePreviewUrl}
				alt="Preview"
				class="max-h-[200px] max-w-[200px] rounded-lg border border-gray-200"
			/>
			<button
				class="absolute top-2 right-2 flex h-7 w-7 cursor-pointer items-center justify-center rounded-full border-none bg-black/60 text-white transition-colors duration-200 hover:bg-black/80"
				onclick={removeImage}
				aria-label="Remove image"
			>
				<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
					<path
						d="M5 5L15 15M5 15L15 5"
						stroke="currentColor"
						stroke-width="2"
						stroke-linecap="round"
					/>
				</svg>
			</button>
		</div>
	{/if}

	<div class="mx-auto flex max-w-3xl items-end gap-3 rounded-xl bg-gray-100 p-3">
		<button
			class="flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-gray-600 transition-all duration-200 hover:bg-black/5 hover:text-gray-900 disabled:cursor-not-allowed disabled:opacity-50"
			onclick={triggerFileInput}
			disabled={isSending || $isStreaming || !$currentConversationId}
			aria-label="Attach image"
		>
			<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
				<path
					d="M3 12.5V5C3 3.34315 4.34315 2 6 2H14C15.6569 2 17 3.34315 17 5V12.5C17 15.5376 14.5376 18 11.5 18C8.46243 18 6 15.5376 6 12.5V7"
					stroke="currentColor"
					stroke-width="1.5"
					stroke-linecap="round"
				/>
			</svg>
		</button>

		<input
			type="file"
			bind:this={fileInputElement}
			onchange={handleImageSelect}
			accept={UPLOAD_CONFIG.ALLOWED_MIME_TYPES.join(',')}
			style="display: none;"
		/>

		<textarea
			bind:this={textareaElement}
			bind:value={textInput}
			onkeydown={handleKeyDown}
			placeholder={$currentConversationId
				? 'Type a message... (Shift + Enter for new line)'
				: 'Select or create a conversation to start chatting'}
			disabled={isSending || $isStreaming || !$currentConversationId}
			rows="1"
			class="font-inherit textarea-scrollbar max-h-[200px] min-h-9 flex-1 resize-none overflow-y-auto border-none bg-transparent py-2 text-base text-gray-900 outline-none placeholder:text-gray-500 disabled:cursor-not-allowed disabled:opacity-60"
		></textarea>

		<button
			class="flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-lg border-none bg-gray-700 text-white transition-all duration-200 hover:scale-105 hover:bg-gray-800 disabled:scale-100 disabled:cursor-not-allowed disabled:opacity-50"
			onclick={handleSubmit}
			disabled={isSending || $isStreaming || !textInput.trim() || !$currentConversationId}
			aria-label="Send message"
		>
			{#if isSending || $isStreaming}
				<svg class="animate-spin" width="20" height="20" viewBox="0 0 20 20" fill="none">
					<circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2" opacity="0.25" />
					<path
						d="M10 2C14.4183 2 18 5.58172 18 10"
						stroke="currentColor"
						stroke-width="2"
						stroke-linecap="round"
					/>
				</svg>
			{:else}
				<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
					<path
						d="M2 10L18 2L10 18L8 11L2 10Z"
						stroke="currentColor"
						stroke-width="2"
						stroke-linecap="round"
						stroke-linejoin="round"
					/>
				</svg>
			{/if}
		</button>
	</div>
</div>

<style>
	/* Scrollbar for textarea */
	.textarea-scrollbar::-webkit-scrollbar {
		width: 6px;
	}

	.textarea-scrollbar::-webkit-scrollbar-track {
		background: transparent;
	}

	.textarea-scrollbar::-webkit-scrollbar-thumb {
		background: #d1d1d6;
		border-radius: 3px;
	}
</style>
```

#### 7.3 Chat Sidebar Component

Create `src/lib/components/ChatSidebar.svelte`:

```svelte
<script lang="ts">
	import {
		conversations,
		currentConversationId,
		createNewConversation,
		deleteConversation,
		selectConversation
	} from '$lib/stores/chat-store';
	import { UI_CONFIG } from '$lib/config/constants';

	let isCreating = $state(false);
	let deletingId = $state<string | null>(null);

	async function handleNewChat() {
		if (isCreating) return;

		isCreating = true;
		try {
			await createNewConversation();
		} catch (error) {
			console.error('Failed to create new chat:', error);
		} finally {
			isCreating = false;
		}
	}

	async function handleDelete(conversationId: string, event: Event) {
		event.stopPropagation();

		if (!confirm('Delete this conversation?')) return;

		deletingId = conversationId;
		try {
			await deleteConversation(conversationId);
		} catch (error) {
			console.error('Failed to delete conversation:', error);
		} finally {
			deletingId = null;
		}
	}

	function handleSelect(conversationId: string) {
		selectConversation(conversationId);
	}

	function truncateTitle(title: string, maxLength = UI_CONFIG.MAX_TITLE_LENGTH) {
		if (title.length <= maxLength) return title;
		return title.substring(0, maxLength) + '...';
	}
</script>

<aside
	class="flex h-screen w-70 flex-col overflow-hidden border-r border-gray-200 bg-gray-50 max-md:w-full max-md:max-w-[280px]"
>
	<div class="border-b border-gray-200 p-4">
		<button
			class="flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-900 transition-all duration-200 hover:border-gray-400 hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-60"
			onclick={handleNewChat}
			disabled={isCreating}
		>
			<svg
				width="20"
				height="20"
				viewBox="0 0 20 20"
				fill="none"
				xmlns="http://www.w3.org/2000/svg"
			>
				<path d="M10 4V16M4 10H16" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
			</svg>
			{isCreating ? 'Creating...' : 'New Chat'}
		</button>
	</div>

	<div class="flex-1 overflow-y-auto p-2">
		{#if $conversations.length === 0}
			<div class="px-4 py-8 text-center text-gray-600">
				<p class="my-2">No conversations yet</p>
				<p class="my-2 text-xs opacity-70">Click "New Chat" to start</p>
			</div>
		{:else}
			{#each $conversations as conversation (conversation.conversation_id)}
				<div
					class="mb-1 flex w-full cursor-pointer items-center justify-between rounded-md border-none bg-transparent px-3 py-3 text-left transition-colors duration-200 hover:bg-gray-200 {$currentConversationId ===
					conversation.conversation_id
						? 'bg-white shadow-sm'
						: ''}"
					role="button"
					tabindex="0"
					onclick={() => handleSelect(conversation.conversation_id)}
					onkeydown={(e) => {
						if (e.key === 'Enter' || e.key === ' ') handleSelect(conversation.conversation_id);
					}}
				>
					<div class="flex min-w-0 flex-1 items-center gap-2.5">
						<svg
							class="shrink-0 text-gray-600"
							width="16"
							height="16"
							viewBox="0 0 16 16"
							fill="none"
							xmlns="http://www.w3.org/2000/svg"
						>
							<path
								d="M2 4C2 2.89543 2.89543 2 4 2H12C13.1046 2 14 2.89543 14 4V10C14 11.1046 13.1046 12 12 12H8L4 14V12H4C2.89543 12 2 11.1046 2 10V4Z"
								stroke="currentColor"
								stroke-width="1.5"
							/>
						</svg>
						<span class="overflow-hidden text-sm text-ellipsis whitespace-nowrap text-gray-900"
							>{truncateTitle(conversation.title)}</span
						>
					</div>

					<button
						class="shrink-0 cursor-pointer rounded border-none bg-transparent p-1 text-gray-500 opacity-0 transition-all duration-200 group-hover:opacity-100 hover:bg-red-50 hover:text-red-500 disabled:cursor-not-allowed disabled:opacity-50"
						style="opacity: {$currentConversationId === conversation.conversation_id ? '1' : ''}"
						onclick={(e) => {
							e.stopPropagation();
							handleDelete(conversation.conversation_id, e);
						}}
						disabled={deletingId === conversation.conversation_id}
						aria-label="Delete conversation"
						tabindex="0"
					>
						{#if deletingId === conversation.conversation_id}
							<svg
								class="animate-spin"
								width="16"
								height="16"
								viewBox="0 0 16 16"
								fill="none"
								xmlns="http://www.w3.org/2000/svg"
							>
								<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" opacity="0.25" />
								<path
									d="M8 2C11.3137 2 14 4.68629 14 8"
									stroke="currentColor"
									stroke-width="2"
									stroke-linecap="round"
								/>
							</svg>
						{:else}
							<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
								<path
									d="M5 2H11M2 4H14M12.6667 4L12.1991 11.0129C12.129 12.065 12.0939 12.5911 11.8667 12.99C11.6666 13.3412 11.3648 13.6235 11.0011 13.7998C10.588 14 10.0607 14 9.00623 14H6.99377C5.93927 14 5.41202 14 4.99889 13.7998C4.63517 13.6235 4.33339 13.3412 4.13332 12.99C3.90607 12.5911 3.871 12.065 3.80086 11.0129L3.33333 4"
									stroke="currentColor"
									stroke-width="1.5"
									stroke-linecap="round"
									stroke-linejoin="round"
								/>
							</svg>
						{/if}
					</button>
				</div>
			{/each}
		{/if}
	</div>
</aside>

<style>
	.w-70 {
		width: 280px;
	}

	/* Show delete button on hover */
	.w-full:hover button[aria-label='Delete conversation'] {
		opacity: 1;
	}
</style>
```

### 8. Building the Pages

#### 8.1 Create Chat Page

Create `src/routes/chat/+page.svelte`:

```svelte
<script lang="ts">
	import ChatMessages from '$lib/components/ChatMessages.svelte';
	import MessageInput from '$lib/components/MessageInput.svelte';
	import { currentConversation, error } from '$lib/stores/chat-store';
</script>

<svelte:head>
	<title>AI Chat Assistant</title>
</svelte:head>

<div class="flex flex-col h-full bg-white">
	<!-- Header -->
	<header class="shrink-0 border-b border-gray-200 bg-white px-6 py-4 max-md:px-4 max-md:pl-16">
		<div class="max-w-3xl mx-auto">
			{#if $currentConversation}
				<h1 class="m-0 text-lg font-semibold text-gray-900 max-md:text-base">
					{$currentConversation.title}
				</h1>
			{:else}
				<h1 class="m-0 text-lg font-semibold text-gray-900 max-md:text-base">AI Chat Assistant</h1>
			{/if}
		</div>
	</header>

	<!-- Error banner -->
	{#if $error}
		<div
			class="shrink-0 bg-red-50 text-red-600 px-6 py-3 flex items-center gap-3 border-b border-red-200"
		>
			<svg class="shrink-0" width="20" height="20" viewBox="0 0 20 20" fill="none">
				<circle cx="10" cy="10" r="8" stroke="currentColor" stroke-width="2" />
				<path d="M10 6V10M10 14V14.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
			</svg>
			<span class="text-sm">{$error}</span>
		</div>
	{/if}

	<!-- Messages area -->
	<ChatMessages />

	<!-- Input area -->
	<MessageInput />
</div>
```

#### 8.2 Create Chat Layout

Create `src/routes/chat/+layout.svelte`:

```svelte
<script lang="ts">
	import { onMount } from 'svelte';
	import ChatSidebar from '$lib/components/ChatSidebar.svelte';
	import { loadConversations } from '$lib/stores/chat-store';

	let { children } = $props();
	let sidebarOpen = $state(true);

	onMount(() => {
		// Load conversations on mount
		loadConversations();
	});

	function toggleSidebar() {
		sidebarOpen = !sidebarOpen;
	}
</script>

<div class="relative flex h-screen w-full overflow-hidden">
	<!-- Mobile sidebar toggle -->
	<button
		class="fixed top-4 left-4 z-150 hidden h-10 w-10 cursor-pointer items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-900 shadow-md transition-colors hover:bg-gray-50 max-md:flex md:hidden"
		onclick={toggleSidebar}
		aria-label="Toggle sidebar"
	>
		<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
			<path
				d="M4 6H20M4 12H20M4 18H20"
				stroke="currentColor"
				stroke-width="2"
				stroke-linecap="round"
			/>
		</svg>
	</button>

	<!-- Sidebar -->
	<div
		class="shrink-0 transition-transform duration-300 ease-in-out max-md:fixed max-md:top-0 max-md:left-0 max-md:z-110 max-md:h-screen {sidebarOpen
			? 'max-md:translate-x-0'
			: 'max-md:-translate-x-full'}"
	>
		<ChatSidebar />
	</div>

	<!-- Mobile overlay -->
	{#if sidebarOpen}
		<button
			class="fixed top-0 right-0 bottom-0 left-0 z-100 hidden bg-black/50 max-md:block"
			onclick={toggleSidebar}
			aria-label="Close sidebar"
		></button>
	{/if}

	<!-- Main content -->
	<main class="flex min-w-0 flex-1 flex-col overflow-hidden">
		{@render children()}
	</main>
</div>
```

#### 8.3 Create Home Page

Create `src/routes/+page.svelte`:

```svelte
<script lang="ts">
	import { onMount } from 'svelte';
	import { goto } from '$app/navigation';

	// Redirect to chat page on mount
	onMount(() => {
		goto('/chat');
	});
</script>

<svelte:head>
	<title>AI Chat Assistant</title>
	<meta name="description" content="AI Chat Assistant powered by JamAI Base" />
</svelte:head>

<div class="flex flex-col items-center justify-center h-screen gap-4">
	<div
		class="w-10 h-10 border-4 border-gray-200 border-t-gray-700 rounded-full animate-spin"
	></div>
	<p class="text-gray-600 text-base">Redirecting to chat...</p>
</div>
```

#### 8.4 Create Root Layout

Create `src/routes/+layout.svelte`:

```svelte
<script lang="ts">
	import './layout.css';

	let { children } = $props();
</script>

{@render children()}
```

### 9. Running the Application

#### 9.1 Start Development Server

```bash
npm run dev
```

The application will start at <http://localhost:5173>

#### 9.2 Test the Application

1. **Visit Home Page**
   * Go to <http://localhost:5173>
   * Click "Start Chatting"
2. **Create a Conversation**
   * Click "New Chat" button in the sidebar
   * Wait for conversation to be created
3. **Send Messages**
   * Type a message in the input box
   * Press Enter or click Send button
   * Watch the AI response stream in real-time
4. **Upload Images**
   * Click the attachment icon
   * Select an image file (JPEG, PNG, GIF, or WebP)
   * Add text if desired
   * Send the message
5. **Manage Conversations**
   * Click on any conversation in sidebar to switch
   * Hover over conversation to see Rename and Delete buttons
   * Rename by clicking the edit icon
   * Delete by clicking the trash icon

#### 9.3 First Run

On the first API call, the application will automatically:

1. Connect to JamAI Base using your credentials
2. Create the "simple-chat" Chat Table if it doesn't exist
3. Configure the AI column with the specified model and prompts

Check your terminal logs to see the table creation process.

### 10. How It Works

#### 10.1 Architecture Overview

```
┌─────────────┐
│   Browser   │
│ (SvelteKit) │
└──────┬──────┘
       │
       │ 1. Create Conversation
       ↓
┌─────────────┐
│Conversations│
│  API Route  │
└──────┬──────┘
       │
       │ 2. Initialize Chat Table
       ↓
┌─────────────┐
│  JamAI Base │
│ Chat Table  │
└──────┬──────┘
       │
       │ 3. Upload Image (if any)
       ↓
┌─────────────┐
│  Upload API │
│   Route     │
└──────┬──────┘
       │
       │ 4. Send Message
       ↓
┌─────────────┐
│ Messages API│
│   Route     │
└──────┬──────┘
       │
       │ 5. Stream AI Response
       ↓
┌─────────────┐
│  LLM Model  │
│ (GPT-4o-mini)│
└──────┬──────┘
       │
       │ 6. Real-time Response
       ↓
┌─────────────┐
│   Browser   │
│  (Display)  │
└─────────────┘
```

#### 10.2 Data Flow

1. **Conversation Creation**
   * User clicks "New Chat"
   * API ensures Chat Table exists
   * Creates conversation in JamAI Base
   * Returns conversation ID
2. **Message Sending**
   * User types message and optionally attaches image
   * If image: uploads to JamAI Base, gets URI
   * Sends message to conversation
   * JamAI Base processes through LLM
   * Streams response back to client
3. **Streaming Response**
   * Server creates ReadableStream
   * Chunks are sent as they're generated
   * Client updates UI in real-time via Svelte stores
   * Final message stored in Chat Table

#### 10.3 JamAI Base Chat Table Structure

The "simple-chat" table has the following schema:

| Column Name | Type             | Description              |
| ----------- | ---------------- | ------------------------ |
| Image       | Input (image)    | Optional image from user |
| User        | Input (str)      | User's message text      |
| AI          | LLM Output (str) | AI-generated response    |

Each conversation is stored separately, with messages as rows in the table.

**Chat Table Configuration:**

* **Model**: openai/gpt-4o-mini (fast and cost-effective)
* **System Prompt**: Defines AI behavior
* **Prompt Template**: References user message and image using variables
* **Temperature**: 0.7 (balanced creativity and consistency)
* **Max Tokens**: 2000 (sufficient for most responses)

### 11. Customization

#### 11.1 Modify AI Behavior

To change the AI's personality, edit src/lib/config/constants.ts:

```typescript
export const AGENT_CONFIG = {
  // ...
  SYSTEM_PROMPT:
    "You are a friendly and helpful AI assistant specializing in technical support.",
  TEMPERATURE: 0.5, // Lower for more consistent responses
};
```

#### 11.2 Change LLM Model

You can use different models for different capabilities:

```typescript
export const AGENT_CONFIG = {
  // ...
  MODEL: "openai/gpt-4o", // More capable but more expensive
  // or
  MODEL: "anthropic/claude-3-5-sonnet-20241022", // Claude Sonnet
  // or
  MODEL: "ellm/meta-llama/Llama-3.3-70B-Instruct", // Llama 3.3
};
```

**Model Recommendations:**

* **gpt-4o-mini**: Best balance of cost and performance
* **gpt-4o**: Highest quality, best for complex tasks
* **claude-3-5-sonnet**: Excellent for code and analysis
* **Llama-3.3-70B**: Open-source, cost-effective

#### 11.3 Add File Type Support

To support more file types, edit src/lib/config/constants.ts:

```typescript
export const UPLOAD_CONFIG = {
  MAX_FILE_SIZE: 20 * 1024 * 1024, // Increase to 20MB
  ALLOWED_MIME_TYPES: [
    "image/jpeg",
    "image/png",
    "application/pdf", // Add PDF support
  ],
  ALLOWED_EXTENSIONS: [".jpeg", ".jpg", ".png", ".pdf"],
};
```

#### 11.4 Customize UI Styling

The application uses Tailwind CSS. Customize colors and styles by editing components. For example, to change the primary color:

```svelte
<!-- Change button colors in ChatSidebar.svelte -->
<button class="bg-blue-600 hover:bg-blue-700 text-white ...">
```

### 12. Deployment

#### 12.1 Deploy to Vercel

1. **Push to GitHub**

   ```bash
   git init
   git add .
   git commit -m "Initial commit"
   git remote add origin YOUR_REPO_URL
   git push -u origin main
   ```
2. **Deploy on Vercel**
   * Go to [vercel.com](https://vercel.com)
   * Click "Import Project"
   * Select your GitHub repository
   * Add environment variables:
     * `JAMAI_API_KEY`
     * `JAMAI_PROJECT_ID`
   * Click "Deploy"
3. **Verify Deployment**
   * Visit your deployment URL
   * Test conversation creation and messaging

#### 12.2 Environment Variables in Production

{% hint style="warning" %}
**Never commit** your `.env` file. Always set environment variables in your deployment platform's dashboard.
{% endhint %}

### 13. Troubleshooting

#### Common Issues

**Issue:** "Missing JamAI credentials" error

**Solution:**

* Verify `.env` exists in project root
* Check variable names: `JAMAI_API_KEY` and `JAMAI_PROJECT_ID`
* Restart development server after adding env variables

***

**Issue:** Conversation creation fails

**Solution:**

* Check your JamAI Base credentials are valid
* Ensure Project ID is correct
* Check browser console for detailed error messages

***

**Issue:** Image upload fails

**Solution:**

* Ensure image is under 10MB
* Check file type is supported (JPEG, PNG, GIF, WebP)
* Verify upload endpoint is accessible

***

**Issue:** Streaming response not working

**Solution:**

* Check browser supports Server-Sent Events
* Verify API route returns proper streaming headers
* Check network tab for streaming data

***

**Issue:** Messages not displaying

**Solution:**

* Check conversation ID is valid
* Verify messages API returns data
* Check browser console for errors
* Try refreshing the page

### 14. Best Practices

#### Security

1. **Protect API Keys**
   * Never commit `.env`
   * Use environment variables in production
   * Rotate API keys periodically
2. **Validate Input**
   * Always validate file types and sizes
   * Sanitize user inputs
   * Implement rate limiting for production
3. **Error Handling**
   * Never expose sensitive error details
   * Log errors server-side
   * Provide user-friendly error messages

#### Performance

1. **Optimize API Calls**
   * Use batch operations when possible
   * Implement proper caching strategies
   * Minimize unnecessary re-fetches
2. **Image Optimization**
   * Compress images before upload
   * Use thumbnail URLs for previews
   * Lazy load images in message history
3. **Streaming**
   * Always use streaming for AI responses
   * Handle connection errors gracefully
   * Show loading indicators during streaming

#### Code Quality

1. **TypeScript**
   * Define interfaces for all data structures
   * Avoid `any` types
   * Use strict mode
2. **Components**
   * Keep components focused and reusable
   * Use Svelte 5 runes for reactivity
   * Proper TypeScript types for props
3. **Testing**
   * Add unit tests for utility functions
   * Test API routes
   * Implement E2E tests for critical flows

### 15. Next Steps

#### Enhancements to Try

1. **User Authentication**
   * Add user accounts with SvelteKit auth
   * Personal conversation history per user
   * Multi-user support
2. **Advanced Features**
   * Message search functionality
   * Export conversation to PDF
   * Voice input support
   * Code syntax highlighting
3. **File Support**
   * Support PDF documents
   * Handle multiple file uploads
   * Document analysis capabilities
4. **UI Enhancements**
   * Dark mode toggle
   * Emoji picker
   * Message reactions
   * Typing indicators
5. **Analytics**
   * Track conversation metrics
   * Monitor API usage
   * User engagement analytics

### 16. Resources

* [JamAI Base Documentation](https://docs.jamaibase.com)
* [JamAI Base Chat Tables Guide](https://docs.jamaibase.com/concepts/tables/chat)
* [SvelteKit Documentation](https://kit.svelte.dev/docs)
* [Svelte 5 Documentation](https://svelte.dev/docs/svelte/overview)
* [TypeScript Documentation](https://www.typescriptlang.org/docs)
* [Tailwind CSS Documentation](https://tailwindcss.com/docs)

### 17. Support

If you encounter issues or have questions:

1. Check the [JamAI Base Documentation](https://docs.jamaibase.com)
2. Review this tutorial's troubleshooting section
3. Visit [JamAI Base Community](https://discord.gg/jamaibase)
4. Contact JamAI Base support

### Conclusion

You've successfully built a full-stack AI chat assistant application using:

* **SvelteKit** with file-based routing and SSR
* **Svelte 5** with modern runes API
* **JamAI Base** for AI conversation management
* **TypeScript** for type safety
* **Tailwind CSS 4** for modern UI
* **Real-time Streaming** for instant AI responses

The application demonstrates how to leverage JamAI Base Chat Tables to create sophisticated AI-powered applications with minimal backend complexity. You can now extend this foundation to build more complex conversational AI systems.

Happy building!


# Receipt Extractor with Next JS

Build an AI-powered receipt extraction app with Next.js and JamAI Base

### 1. Introduction

This comprehensive tutorial will guide you through building a full-stack receipt extraction application using Next.js and the JamAI Base TypeScript SDK. The application uses AI to automatically extract key information from receipt images.

#### What We'll Build

An AI-powered web application that allows users to:

1. Upload receipt images through drag-and-drop or file selection
2. Extract information automatically using JamAI Base Action Tables:
   * Shop/Store Name
   * Category (Groceries, Restaurant, Retail, etc.)
   * Total Amount
3. View processing history with pagination
4. Display results in a modern, responsive UI

#### Key Features

* **Modern Next.js 16** with App Router and React Server Components
* **TypeScript** for type safety
* **Tailwind CSS 4** for styling with dark mode support
* **SWR** for efficient data fetching and caching
* **Real-time Processing** with loading states
* **Responsive Design** for mobile and desktop
* **Image Upload** with validation and preview

#### Prerequisites

Before starting, you'll need:

* Node.js 16.x or higher installed
* Basic knowledge of React and TypeScript
* Project ID and Personal Access Token (PAT) from JamAI Base
* A code editor (VS Code recommended)

### 2. Project Setup

#### 2.1 Create Next.js Project

First, create a new Next.js project with TypeScript:

```bash
npx create-next-app@latest receipt-extractor --typescript --tailwind --app --no-src-dir
cd receipt-extractor
```

When prompted, select:

* TypeScript: **Yes**
* ESLint: **Yes**
* Tailwind CSS: **Yes**
* `src/` directory: **Yes**
* App Router: **Yes**
* Import alias: **Yes** (@/\*)

#### 2.2 Install Dependencies

Install the required packages:

```bash
npm install jamaibase swr
```

**Dependencies:**

* `jamaibase` - JamAI Base TypeScript SDK for AI operations
* `swr` - React hooks for data fetching with caching

#### 2.3 Project Structure

Create the following folder structure:

```
receipt-extractor/
├── src/
│   ├── app/
│   │   ├── api/
│   │   │   └── jamai/
│   │   │       ├── upload/
│   │   │       │   └── route.ts
│   │   │       ├── process/
│   │   │       │   └── route.ts
│   │   │       └── history/
│   │   │           └── route.ts
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   └── globals.css
│   ├── components/
│   │   ├── ReceiptUpload.tsx
│   │   ├── ReceiptResults.tsx
│   │   ├── ReceiptHistory.tsx
│   │   └── LoadingSpinner.tsx
│   ├── hooks/
│   │   └── useReceipts.ts
│   └── lib/
│       ├── jamai.ts
│       └── types.ts
├── .env.local
├── .env.example
├── package.json
└── tsconfig.json
```

### 3. Getting Your Credentials

#### 3.1 Get Your Personal Access Token (PAT)

1. Go to <https://cloud.jamaibase.com/>
2. Click on your username in the top right
3. Select **Account Settings**
4. Navigate to **Personal Access Token** section
5. Click **Create Personal Access Token**
6. Copy and save your token securely

#### 3.2 Get Your Project ID

1. In the JamAI Base dashboard, navigate to your project
2. Look at the browser URL: `https://cloud.jamaibase.com/project/{PROJECT_ID}`
3. Copy the Project ID from the URL

#### 3.3 Configure Environment Variables

Create a `.env.local` file in your project root:

```bash
# JamAI Base Configuration
# Get your credentials from https://cloud.jamaibase.com/

# Your Personal Access Token (PAT)
JAMAI_API_KEY=your_api_key_here

# Your Project ID
JAMAI_PROJECT_ID=your_project_id_here
```

Also create a `.env.example` file for reference:

```bash
# JamAI Base Configuration
# Get your credentials from https://cloud.jamaibase.com/

# Your Personal Access Token (PAT)
# Get it from: Click user name > Account Settings > Create Personal Access Token
JAMAI_API_KEY=your_api_key_here

# Your Project ID
# Find it in your project URL
JAMAI_PROJECT_ID=your_project_id_here
```

{% hint style="info" %}
**Important:** Never commit `.env.local` to version control. Add it to your `.gitignore` file.
{% endhint %}

### 4. Core Configuration

#### 4.1 Create Type Definitions

Create `src/lib/types.ts`:

```typescript
// Receipt data structure
export interface Receipt {
  id: string;
  shopName: string;
  category: string;
  total: string;
  imageUrl: string;
  createdAt: string;
  updatedAt: string;
}

// API response types
export interface UploadResponse {
  uri: string;
}

export interface ProcessResponse {
  success: boolean;
  receipt: Receipt;
}

export interface HistoryResponse {
  receipts: Receipt[];
  total: number;
  offset: number;
  limit: number;
}

export interface ErrorResponse {
  error: string;
  details?: string;
}

// Helper function to extract value from JamAI wrapped field
// JamAI listRows returns: { 'Shop Name': { value: 'Walmart' } }
// This function unwraps to: 'Walmart'
export function getValue(field: any): any {
  if (field && typeof field === "object" && "value" in field) {
    return field.value;
  }
  return field;
}

// Helper function to unwrap a full row from JamAI listRows response
export function unwrapRow(row: Record<string, any>): Receipt {
  return {
    id: getValue(row["ID"]) || getValue(row["Row ID"]) || "",
    shopName: getValue(row["Shop Name"]) || "",
    category: getValue(row["Category"]) || "",
    total: getValue(row["Total"]) || "",
    imageUrl: getValue(row["Image"]) || "",
    createdAt:
      getValue(row["Created at"]) ||
      getValue(row["created_at"]) ||
      new Date().toISOString(),
    updatedAt:
      getValue(row["Updated at"]) ||
      getValue(row["updated_at"]) ||
      new Date().toISOString(),
  };
}
```

#### 4.2 Initialize JamAI Client

Create `src/lib/jamai.ts`:

```typescript
import JamAI from "jamaibase";

// Singleton JamAI client instance
let jamaiClient: JamAI | null = null;

/**
 * Get or create the JamAI client instance
 * This ensures we only create one client throughout the application lifecycle
 */
export function getJamaiClient(): JamAI {
  if (!jamaiClient) {
    // Validate environment variables
    const apiKey = process.env.JAMAI_API_KEY;
    const projectId = process.env.JAMAI_PROJECT_ID;

    if (!apiKey || !projectId) {
      throw new Error(
        "Missing JamAI credentials. Please set JAMAI_API_KEY and JAMAI_PROJECT_ID in .env.local"
      );
    }

    // Create client instance
    jamaiClient = new JamAI({
      token: apiKey,
      projectId: projectId,
    });
  }

  return jamaiClient;
}

/**
 * Ensure the "receipt" action table exists
 * Creates it if it doesn't exist with proper column configuration
 */
export async function ensureTableExists(): Promise<void> {
  const client = getJamaiClient();

  try {
    // Check if table already exists
    const tables = await client.table.listTables({
      table_type: "action",
    });

    const tableExists = tables.items.some((t) => t.id === "receipt");

    if (!tableExists) {
      console.log('Creating "receipt" action table...');

      // Create the action table with proper schema
      await client.table.createActionTable({
        id: "receipt",
        cols: [
          // Input column for the image file
          {
            id: "Image",
            dtype: "image",
          },
          // LLM output column for shop name
          {
            id: "Shop Name",
            dtype: "str",
            gen_config: {
              object: "gen_config.llm",
              model: "ellm/Qwen/Qwen3-VL-30B-A3B-Instruct",
              system_prompt:
                "You are a receipt data extraction assistant. Extract information accurately from receipt images.",
              prompt:
                "Table name: \"receipt\"\n\nImage: ${Image}\n\nExtract the shop or store name from this receipt image. Return only the name, nothing else. If you cannot find a shop name, return 'Unknown'.",
              temperature: 0.1,
              max_tokens: 100,
            },
          },
          // LLM output column for category
          {
            id: "Category",
            dtype: "str",
            gen_config: {
              object: "gen_config.llm",
              model: "ellm/Qwen/Qwen3-VL-30B-A3B-Instruct",
              system_prompt:
                "You are a receipt categorization assistant. Categorize receipts accurately.",
              prompt:
                'Table name: "receipt"\n\nImage: ${Image}\n\nCategorize this receipt into one of the following categories: Groceries, Restaurant, Retail, Gas, Entertainment, Healthcare, or Other. Return only the category name.',
              temperature: 0.1,
              max_tokens: 50,
            },
          },
          // LLM output column for total amount
          {
            id: "Total",
            dtype: "str",
            gen_config: {
              object: "gen_config.llm",
              model: "ellm/Qwen/Qwen3-VL-30B-A3B-Instruct",
              system_prompt:
                "You are a receipt data extraction assistant. Extract information accurately from receipt images.",
              prompt:
                "Table name: \"receipt\"\n\nImage: ${Image}\n\nExtract the total amount from this receipt. Include the currency symbol. Return only the amount (e.g., '$45.99' or '€32.50'). If you cannot find a total, return 'N/A'.",
              temperature: 0.1,
              max_tokens: 50,
            },
          },
        ],
      });

      console.log('Successfully created "receipt" table');
    } else {
      console.log('"receipt" table already exists');
    }
  } catch (error) {
    console.error("Error ensuring table exists:", error);
    throw new Error("Failed to initialize receipt table");
  }
}
```

**Key Points:**

* Uses singleton pattern for the JamAI client
* Validates environment variables on initialization
* Automatically creates the Action Table if it doesn't exist
* Configures three LLM output columns with specific prompts
* Uses Qwen3-VL-30B (embedded LLM) for cost-effective, fast processing
* Prompts include table context and use `${Image}` variable reference

### 5. Building the API Routes

#### 5.1 File Upload Route

Create `src/app/api/jamai/upload/route.ts`:

```typescript
import { NextResponse } from "next/server";
import { getJamaiClient } from "@/lib/jamai";

// Allowed image MIME types
const ALLOWED_TYPES = [
  "image/jpeg",
  "image/jpg",
  "image/png",
  "image/webp",
  "image/gif",
];

// Maximum file size: 10MB
const MAX_FILE_SIZE = 10 * 1024 * 1024;

export async function POST(request: Request) {
  try {
    // Parse form data
    const formData = await request.formData();
    const file = formData.get("file") as File | null;

    if (!file) {
      return NextResponse.json({ error: "File is required" }, { status: 400 });
    }

    // Validate file type
    if (!ALLOWED_TYPES.includes(file.type)) {
      return NextResponse.json(
        {
          error: "Invalid file type",
          details: `Allowed types: ${ALLOWED_TYPES.join(", ")}`,
        },
        { status: 400 }
      );
    }

    // Validate file size
    if (file.size > MAX_FILE_SIZE) {
      return NextResponse.json(
        {
          error: "File too large",
          details: `Maximum file size is ${MAX_FILE_SIZE / 1024 / 1024}MB`,
        },
        { status: 413 }
      );
    }

    // Upload to JamAI Base, passing the File directly
    const client = getJamaiClient();
    const fileResponse = await client.file.uploadFile({
      file, // pass the File object directly
    });

    // Return the JamAI file URI
    return NextResponse.json({
      uri: fileResponse.uri,
    });
  } catch (error) {
    console.error("[Upload API Error]:", error);

    if (error instanceof Error) {
      return NextResponse.json(
        {
          error: "Upload failed",
          details: error.message,
        },
        { status: 500 }
      );
    }

    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}
```

**What this does:**

* Accepts multipart form data with an image file
* Validates file type (images only) and size (10MB max)
* Uploads the file to JamAI Base storage
* Returns a URI for the uploaded file

#### 5.2 Receipt Processing Route

Create `src/app/api/jamai/process/route.ts`:

```typescript
import { NextResponse } from "next/server";
import { getJamaiClient, ensureTableExists } from "@/lib/jamai";
import type { ProcessResponse } from "@/lib/types";

export async function POST(request: Request) {
  try {
    // Parse JSON body
    const body = await request.json();
    const { imageUri } = body;

    if (!imageUri || typeof imageUri !== "string") {
      return NextResponse.json(
        { error: "imageUri is required and must be a string" },
        { status: 400 }
      );
    }

    // Ensure the receipt table exists
    await ensureTableExists();

    // Get JamAI client
    const client = getJamaiClient();

    // Process the receipt by adding a row to the action table
    const response = await client.table.addRow({
      table_type: "action",
      table_id: "receipt",
      data: [
        {
          Image: imageUri,
        },
      ],
      concurrent: false, // Process synchronously for immediate results
    });

    // Extract the first row from the response
    const row = response.rows[0];

    if (!row) {
      throw new Error("No row returned from JamAI Base");
    }

    // Extract the LLM-generated outputs from the columns
    const shopName =
      row.columns["Shop Name"]?.choices?.[0]?.message?.content || "Unknown";
    const category =
      row.columns["Category"]?.choices?.[0]?.message?.content || "Other";
    const total = row.columns["Total"]?.choices?.[0]?.message?.content || "N/A";

    // Get the row ID for future reference
    const rowId = row.row_id || "";

    // Construct the receipt object
    const receipt = {
      id: rowId,
      shopName: String(shopName).trim(),
      category: String(category).trim(),
      total: String(total).trim(),
      imageUrl: imageUri,
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };

    const result: ProcessResponse = {
      success: true,
      receipt,
    };

    return NextResponse.json(result);
  } catch (error) {
    console.error("[Process API Error]:", error);

    if (error instanceof Error) {
      return NextResponse.json(
        {
          error: "Processing failed",
          details: error.message,
        },
        { status: 500 }
      );
    }

    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}
```

**What this does:**

* Receives the uploaded image URI
* Ensures the Action Table exists
* Adds a row to the table with the image
* JamAI Base automatically processes the image through LLMs
* Extracts the AI-generated outputs (shop name, category, total)
* Returns the structured receipt data

#### 5.3 History Retrieval Route

Create `src/app/api/jamai/history/route.ts`:

```typescript
import { NextResponse } from "next/server";
import { getJamaiClient } from "@/lib/jamai";
import { unwrapRow, type HistoryResponse } from "@/lib/types";

export async function GET(request: Request) {
  try {
    // Parse query parameters
    const { searchParams } = new URL(request.url);
    const offset = parseInt(searchParams.get("offset") || "0", 10);
    const limit = parseInt(searchParams.get("limit") || "50", 10);

    // Validate parameters
    if (offset < 0 || limit < 1 || limit > 100) {
      return NextResponse.json(
        {
          error: "Invalid parameters",
          details: "offset must be >= 0, limit must be between 1 and 100",
        },
        { status: 400 }
      );
    }

    // Get JamAI client
    const client = getJamaiClient();

    // Fetch rows from the receipt table
    const response = await client.table.listRows({
      table_type: "action",
      table_id: "receipt",
      offset,
      limit,
    });

    if (response?.items?.length > 0) {
      // Unwrap the values from each row
      // CRITICAL: JamAI wraps values like { 'Shop Name': { value: 'Walmart' } }
      const receipts = response.items?.map((row) => unwrapRow(row));

      const result: HistoryResponse = {
        receipts,
        total: response.total || response.items?.length,
        offset: response.offset || offset,
        limit: response.limit || limit,
      };

      return NextResponse.json(result);
    }
    const emptyResponse: HistoryResponse = {
      receipts: [],
      total: 0,
      offset: 0,
      limit,
    };
    return NextResponse.json(emptyResponse);
  } catch (error) {
    console.error("[History API Error]:", error);

    if (error instanceof Error) {
      // If it's a "table not found" error, return empty results
      if (
        error.message.includes("not found") ||
        error.message.includes("does not exist")
      ) {
        const emptyResponse: HistoryResponse = {
          receipts: [],
          total: 0,
          offset: 0,
          limit: 50,
        };
        return NextResponse.json(emptyResponse);
      }

      return NextResponse.json(
        {
          error: "Failed to fetch history",
          details: error.message,
        },
        { status: 500 }
      );
    }

    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}
```

**What this does:**

* Accepts pagination parameters (offset, limit)
* Fetches historical receipts from the Action Table
* Unwraps JamAI's nested data structure
* Returns paginated results
* Handles the case when the table doesn't exist yet

### 6. Building the UI Components

#### 6.1 Loading Spinner Component

Create `src/components/LoadingSpinner.tsx`:

```typescript
interface LoadingSpinnerProps {
  size?: "sm" | "md" | "lg";
}

export function LoadingSpinner({ size = "md" }: LoadingSpinnerProps) {
  const sizeClasses = {
    sm: "w-4 h-4",
    md: "w-8 h-8",
    lg: "w-12 h-12",
  };

  return (
    <div className={`${sizeClasses[size]} animate-spin`}>
      <svg
        className="w-full h-full text-zinc-600 dark:text-zinc-400"
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
      >
        <circle
          className="opacity-25"
          cx="12"
          cy="12"
          r="10"
          stroke="currentColor"
          strokeWidth="4"
        />
        <path
          className="opacity-75"
          fill="currentColor"
          d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
        />
      </svg>
    </div>
  );
}
```

#### 6.2 Receipt Upload Component

Create `src/components/ReceiptUpload.tsx`:

```typescript
"use client";

import { useState, useRef } from "react";
import type { Receipt } from "@/lib/types";
import { LoadingSpinner } from "./LoadingSpinner";

interface ReceiptUploadProps {
  onUploadComplete: (receipt: Receipt) => void;
  onError: (error: string) => void;
}

type ProgressState = "idle" | "uploading" | "processing";

export function ReceiptUpload({
  onUploadComplete,
  onError,
}: ReceiptUploadProps) {
  const [isDragging, setIsDragging] = useState(false);
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [isProcessing, setIsProcessing] = useState(false);
  const [progress, setProgress] = useState<ProgressState>("idle");
  const fileInputRef = useRef<HTMLInputElement>(null);

  const validateFile = (file: File): boolean => {
    // Validate file type
    if (!file.type.startsWith("image/")) {
      onError("Please upload an image file (JPEG, PNG, WebP, or GIF)");
      return false;
    }

    // Validate file size (10MB max)
    const maxSize = 10 * 1024 * 1024;
    if (file.size > maxSize) {
      onError("File size must be less than 10MB");
      return false;
    }

    return true;
  };

  const handleFile = (file: File) => {
    if (!validateFile(file)) {
      return;
    }

    setSelectedFile(file);
    const url = URL.createObjectURL(file);
    setPreviewUrl(url);
  };

  const handleDragEnter = (e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(true);
  };

  const handleDragOver = (e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
  };

  const handleDragLeave = (e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(false);
  };

  const handleDrop = (e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    setIsDragging(false);

    const files = e.dataTransfer.files;
    if (files && files[0]) {
      handleFile(files[0]);
    }
  };

  const handleClick = () => {
    fileInputRef.current?.click();
  };

  const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files;
    if (files && files[0]) {
      handleFile(files[0]);
    }
  };

  const handleProcess = async () => {
    if (!selectedFile) return;

    setIsProcessing(true);
    setProgress("uploading");

    try {
      // Step 1: Upload file
      const formData = new FormData();
      formData.append("file", selectedFile);

      const uploadRes = await fetch("/api/jamai/upload", {
        method: "POST",
        body: formData,
      });

      if (!uploadRes.ok) {
        const errorData = await uploadRes.json();
        throw new Error(errorData.error || "Upload failed");
      }

      const { uri } = await uploadRes.json();

      // Step 2: Process receipt
      setProgress("processing");
      const processRes = await fetch("/api/jamai/process", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ imageUri: uri }),
      });

      if (!processRes.ok) {
        const errorData = await processRes.json();
        throw new Error(errorData.error || "Processing failed");
      }

      const { receipt } = await processRes.json();

      // Success! Notify parent and reset
      onUploadComplete(receipt);

      // Cleanup
      if (previewUrl) {
        URL.revokeObjectURL(previewUrl);
      }
      setPreviewUrl(null);
      setSelectedFile(null);
      setProgress("idle");

      // Reset file input
      if (fileInputRef.current) {
        fileInputRef.current.value = "";
      }
    } catch (error) {
      const message =
        error instanceof Error ? error.message : "An error occurred";
      onError(message);
      setProgress("idle");
    } finally {
      setIsProcessing(false);
    }
  };

  const handleClear = () => {
    if (previewUrl) {
      URL.revokeObjectURL(previewUrl);
    }
    setPreviewUrl(null);
    setSelectedFile(null);
    if (fileInputRef.current) {
      fileInputRef.current.value = "";
    }
  };

  const getProgressText = () => {
    switch (progress) {
      case "uploading":
        return "Uploading image...";
      case "processing":
        return "Extracting receipt data...";
      default:
        return "";
    }
  };

  return (
    <div className="w-full">
      {!previewUrl ? (
        // Upload zone
        <div
          className={`
            relative border-2 border-dashed rounded-lg p-12 text-center
            transition-colors cursor-pointer
            ${
              isDragging
                ? "border-blue-500 bg-blue-50 dark:bg-blue-900/20"
                : "border-zinc-300 dark:border-zinc-700 hover:border-zinc-400 dark:hover:border-zinc-600"
            }
          `}
          onDragEnter={handleDragEnter}
          onDragOver={handleDragOver}
          onDragLeave={handleDragLeave}
          onDrop={handleDrop}
          onClick={handleClick}
        >
          <input
            ref={fileInputRef}
            type="file"
            accept="image/*"
            onChange={handleFileInput}
            className="hidden"
          />

          <div className="flex flex-col items-center gap-4">
            <svg
              className="w-16 h-16 text-zinc-400 dark:text-zinc-600"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
              />
            </svg>

            <div>
              <p className="text-lg font-medium text-zinc-900 dark:text-zinc-100">
                Drag and drop receipt image
              </p>
              <p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
                or click to select a file
              </p>
            </div>

            <p className="text-xs text-zinc-400 dark:text-zinc-600">
              Supports JPEG, PNG, WebP, GIF (max 10MB)
            </p>
          </div>
        </div>
      ) : (
        // Preview and process
        <div className="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 p-6">
          <div className="flex flex-col md:flex-row gap-6">
            {/* Image preview */}
            <div className="shrink-0">
              <img
                src={previewUrl}
                alt="Receipt preview"
                className="w-full md:w-48 h-auto max-h-64 object-contain rounded border border-zinc-200 dark:border-zinc-700"
              />
            </div>

            {/* Actions */}
            <div className="flex-1 flex flex-col justify-between">
              <div>
                <h3 className="text-lg font-semibold text-zinc-900 dark:text-zinc-100 mb-2">
                  Ready to process
                </h3>
                <p className="text-sm text-zinc-600 dark:text-zinc-400 mb-4">
                  Click the button below to extract receipt information
                </p>

                {isProcessing && (
                  <div className="flex items-center gap-3 text-sm text-zinc-600 dark:text-zinc-400 mb-4">
                    <LoadingSpinner size="sm" />
                    <span>{getProgressText()}</span>
                  </div>
                )}
              </div>

              <div className="flex gap-3">
                <button
                  onClick={handleProcess}
                  disabled={isProcessing}
                  className="flex-1 bg-zinc-900 dark:bg-zinc-100 text-zinc-50 dark:text-zinc-900 px-6 py-3 rounded-lg font-medium hover:bg-zinc-800 dark:hover:bg-zinc-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
                >
                  {isProcessing ? "Processing..." : "Process Receipt"}
                </button>
                <button
                  onClick={handleClear}
                  disabled={isProcessing}
                  className="px-6 py-3 rounded-lg font-medium border border-zinc-300 dark:border-zinc-700 text-zinc-900 dark:text-zinc-100 hover:bg-zinc-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
                >
                  Clear
                </button>
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}
```

**Key Features:**

* Drag-and-drop file upload
* Click to select file
* Image preview before processing
* Client-side validation
* Visual loading states with progress messages
* Error handling

#### 6.3 Receipt Results Component

Create `src/components/ReceiptResults.tsx`:

```typescript
"use client";

import type { Receipt } from "@/lib/types";

interface ReceiptResultsProps {
  receipt: Receipt | null;
  onClear: () => void;
}

interface FieldProps {
  label: string;
  value: string;
  highlighted?: boolean;
}

function Field({ label, value, highlighted = false }: FieldProps) {
  return (
    <div className="space-y-1">
      <p className="text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wide">
        {label}
      </p>
      <p
        className={`font-medium ${
          highlighted
            ? "text-2xl text-zinc-900 dark:text-zinc-50"
            : "text-lg text-zinc-800 dark:text-zinc-100"
        }`}
      >
        {value}
      </p>
    </div>
  );
}

export function ReceiptResults({ receipt, onClear }: ReceiptResultsProps) {
  if (!receipt) return null;

  return (
    <div className="bg-white dark:bg-zinc-900 rounded-lg shadow-lg p-6 border border-zinc-200 dark:border-zinc-800 animate-in fade-in duration-300">
      <div className="flex items-start justify-between mb-6">
        <div>
          <h3 className="text-xl font-bold text-zinc-900 dark:text-zinc-50">
            Extraction Results
          </h3>
          <p className="text-sm text-zinc-500 dark:text-zinc-400 mt-1">
            Successfully extracted receipt information
          </p>
        </div>
        <div className="flex items-center justify-center w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/30">
          <svg
            className="w-6 h-6 text-green-600 dark:text-green-400"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M5 13l4 4L19 7"
            />
          </svg>
        </div>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
        <Field label="Shop Name" value={receipt.shopName} />
        <Field label="Category" value={receipt.category} />
        <Field label="Total Amount" value={receipt.total} highlighted />
      </div>

      <div className="flex gap-3 pt-4 border-t border-zinc-200 dark:border-zinc-800">
        <button
          onClick={onClear}
          className="flex-1 bg-zinc-900 dark:bg-zinc-100 text-zinc-50 dark:text-zinc-900 px-6 py-3 rounded-lg font-medium hover:bg-zinc-800 dark:hover:bg-zinc-200 transition-colors"
        >
          Process Another Receipt
        </button>
      </div>
    </div>
  );
}
```

#### 6.4 Receipt History Component

Create `src/components/ReceiptHistory.tsx`:

```typescript
"use client";

import { useState, useEffect } from "react";
import useSWR from "swr";
import type { HistoryResponse } from "@/lib/types";
import { LoadingSpinner } from "./LoadingSpinner";

const fetcher = (url: string) => fetch(url).then((res) => res.json());

interface ReceiptHistoryProps {
  refreshTrigger?: number;
}

export function ReceiptHistory({ refreshTrigger }: ReceiptHistoryProps) {
  const [offset, setOffset] = useState(0);
  const limit = 50;

  const { data, error, isLoading, mutate } = useSWR<HistoryResponse>(
    `/api/jamai/history?offset=${offset}&limit=${limit}`,
    fetcher,
    {
      revalidateOnFocus: false,
      revalidateOnReconnect: true,
    }
  );

  // Refetch data when refreshTrigger changes (after new receipt is processed)
  useEffect(() => {
    if (refreshTrigger !== undefined && refreshTrigger > 0) {
      mutate();
    }
  }, [refreshTrigger, mutate]);

  const handlePrevious = () => {
    setOffset(Math.max(0, offset - limit));
  };

  const handleNext = () => {
    if (data && offset + limit < data.total) {
      setOffset(offset + limit);
    }
  };

  if (isLoading) {
    return (
      <div className="flex items-center justify-center py-12">
        <LoadingSpinner size="lg" />
      </div>
    );
  }

  if (error) {
    return (
      <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
        <p className="text-red-900 dark:text-red-200 text-sm">
          Failed to load history. Please try again.
        </p>
      </div>
    );
  }

  if (!data || data.receipts?.length === 0) {
    return (
      <div className="text-center py-12 bg-zinc-50 dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800">
        <svg
          className="w-16 h-16 text-zinc-300 dark:text-zinc-700 mx-auto mb-4"
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth={2}
            d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
          />
        </svg>
        <h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-1">
          No receipts yet
        </h3>
        <p className="text-sm text-zinc-500 dark:text-zinc-400">
          Upload your first receipt to get started
        </p>
      </div>
    );
  }

  return (
    <div className="space-y-4">
      {/* Desktop Table View */}
      <div className="hidden md:block bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 overflow-hidden">
        <table className="w-full">
          <thead className="bg-zinc-50 dark:bg-zinc-800 border-b border-zinc-200 dark:border-zinc-700">
            <tr>
              <th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
                Shop Name
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
                Category
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
                Total
              </th>
              <th className="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">
                Date
              </th>
            </tr>
          </thead>
          <tbody className="divide-y divide-zinc-200 dark:divide-zinc-800">
            {data.receipts?.map((receipt) => (
              <tr
                key={receipt.id}
                className="hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors"
              >
                <td className="px-6 py-4 whitespace-nowrap">
                  <div className="text-sm font-medium text-zinc-900 dark:text-zinc-100">
                    {receipt.shopName}
                  </div>
                </td>
                <td className="px-6 py-4 whitespace-nowrap">
                  <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200">
                    {receipt.category}
                  </span>
                </td>
                <td className="px-6 py-4 whitespace-nowrap">
                  <div className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
                    {receipt.total}
                  </div>
                </td>
                <td className="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">
                  {new Date(receipt.updatedAt).toLocaleDateString()}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {/* Mobile Card View */}
      <div className="md:hidden space-y-3">
        {data.receipts?.map((receipt) => (
          <div
            key={receipt.id}
            className="bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 p-4"
          >
            <div className="flex items-start justify-between mb-3">
              <div>
                <h4 className="font-medium text-zinc-900 dark:text-zinc-100">
                  {receipt.shopName}
                </h4>
                <p className="text-xs text-zinc-500 dark:text-zinc-400 mt-1">
                  {new Date(receipt.updatedAt).toLocaleDateString()}
                </p>
              </div>
              <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200">
                {receipt.category}
              </span>
            </div>
            <div className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
              {receipt.total}
            </div>
          </div>
        ))}
      </div>

      {/* Pagination */}
      {data.total > limit && (
        <div className="flex items-center justify-between bg-white dark:bg-zinc-900 rounded-lg border border-zinc-200 dark:border-zinc-800 px-4 py-3">
          <div className="text-sm text-zinc-600 dark:text-zinc-400">
            Showing {offset + 1}-{Math.min(offset + limit, data.total)} of{" "}
            {data.total}
          </div>
          <div className="flex gap-2">
            <button
              onClick={handlePrevious}
              disabled={offset === 0}
              className="px-4 py-2 text-sm font-medium rounded-lg border border-zinc-300 dark:border-zinc-700 text-zinc-900 dark:text-zinc-100 hover:bg-zinc-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
            >
              Previous
            </button>
            <button
              onClick={handleNext}
              disabled={offset + limit >= data.total}
              className="px-4 py-2 text-sm font-medium rounded-lg border border-zinc-300 dark:border-zinc-700 text-zinc-900 dark:text-zinc-100 hover:bg-zinc-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
            >
              Next
            </button>
          </div>
        </div>
      )}
    </div>
  );
}
```

**Key Features:**

* Uses SWR for data fetching with caching
* Auto-refreshes when new receipts are added
* Responsive design (table on desktop, cards on mobile)
* Pagination support
* Empty state handling

#### 6.5 Custom Hook for Data Fetching

Create `src/hooks/useReceipts.ts`:

```typescript
import useSWR from "swr";
import type { HistoryResponse } from "@/lib/types";

const fetcher = (url: string) => fetch(url).then((res) => res.json());

interface UseReceiptsOptions {
  offset?: number;
  limit?: number;
}

export function useReceipts({
  offset = 0,
  limit = 50,
}: UseReceiptsOptions = {}) {
  const { data, error, mutate, isLoading } = useSWR<HistoryResponse>(
    `/api/jamai/history?offset=${offset}&limit=${limit}`,
    fetcher,
    {
      revalidateOnFocus: false,
      revalidateOnReconnect: true,
    }
  );

  return {
    receipts: data?.receipts || [],
    total: data?.total || 0,
    offset: data?.offset || offset,
    limit: data?.limit || limit,
    isLoading,
    error,
    mutate, // For manual refresh after upload
  };
}
```

### 7. Building the Main Page

#### 7.1 Update Layout

Update `src/app/layout.tsx`:

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Receipt Extraction App",
  description:
    "AI-powered receipt extraction using JamAI Base. Extract shop names, categories, and totals from receipt images.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}
```

#### 7.2 Create Main Page

Update `src/app/page.tsx`:

```typescript
"use client";

import { useState } from "react";
import { ReceiptUpload } from "@/components/ReceiptUpload";
import { ReceiptResults } from "@/components/ReceiptResults";
import { ReceiptHistory } from "@/components/ReceiptHistory";
import type { Receipt } from "@/lib/types";

export default function Home() {
  const [currentReceipt, setCurrentReceipt] = useState<Receipt | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [refreshTrigger, setRefreshTrigger] = useState(0);

  const handleUploadComplete = (receipt: Receipt) => {
    setCurrentReceipt(receipt);
    setError(null);
    // Trigger history refresh
    setRefreshTrigger((prev) => prev + 1);
  };

  const handleError = (errorMessage: string) => {
    setError(errorMessage);
    // Auto-dismiss error after 5 seconds
    setTimeout(() => setError(null), 5000);
  };

  const handleClear = () => {
    setCurrentReceipt(null);
  };

  return (
    <div className="min-h-screen bg-zinc-50 dark:bg-black">
      <main className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
        {/* Header */}
        <header className="mb-12">
          <h1 className="text-4xl font-bold text-zinc-900 dark:text-zinc-50 mb-2">
            Receipt Extraction
          </h1>
          <p className="text-lg text-zinc-600 dark:text-zinc-400">
            Upload receipts to extract shop name, category, and total amount
            using AI
          </p>
        </header>

        {/* Error Toast */}
        {error && (
          <div className="mb-6 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 animate-in fade-in duration-200">
            <div className="flex items-start gap-3">
              <svg
                className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5"
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
                />
              </svg>
              <div className="flex-1">
                <h3 className="text-sm font-medium text-red-900 dark:text-red-200">
                  Error
                </h3>
                <p className="text-sm text-red-800 dark:text-red-300 mt-1">
                  {error}
                </p>
              </div>
              <button
                onClick={() => setError(null)}
                className="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200"
              >
                <svg
                  className="w-5 h-5"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth={2}
                    d="M6 18L18 6M6 6l12 12"
                  />
                </svg>
              </button>
            </div>
          </div>
        )}

        {/* Upload Section */}
        <section className="mb-12">
          <h2 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50 mb-4">
            Upload Receipt
          </h2>
          <ReceiptUpload
            onUploadComplete={handleUploadComplete}
            onError={handleError}
          />
        </section>

        {/* Results Section */}
        {currentReceipt && (
          <section className="mb-12">
            <h2 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50 mb-4">
              Latest Result
            </h2>
            <ReceiptResults receipt={currentReceipt} onClear={handleClear} />
          </section>
        )}

        {/* History Section */}
        <section>
          <h2 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50 mb-4">
            Processing History
          </h2>
          <ReceiptHistory refreshTrigger={refreshTrigger} />
        </section>
      </main>
    </div>
  );
}
```

### 8. Running the Application

#### 8.1 Start Development Server

```bash
npm run dev
```

The application will start at <http://localhost:3000>

#### 8.2 Test the Application

1. **Upload a Receipt**
   * Drag and drop a receipt image, or click to select a file
   * Supported formats: JPEG, PNG, WebP, GIF (max 10MB)
2. **Process Receipt**
   * Click "Process Receipt" button
   * Watch the progress indicators:
     * "Uploading image..."
     * "Extracting receipt data..."
3. **View Results**
   * See extracted information:
     * Shop Name
     * Category
     * Total Amount
4. **Check History**
   * Scroll down to see all processed receipts
   * Navigate through pages if you have more than 50 receipts

#### 8.3 First Run

On the first API call, the application will automatically:

1. Connect to JamAI Base using your credentials
2. Create the "receipt" Action Table if it doesn't exist
3. Configure the LLM columns with extraction prompts

Check your terminal logs to see:

```
Creating "receipt" action table...
Successfully created "receipt" table
```

### 9. How It Works

#### 9.1 Architecture Overview

```
┌─────────────┐
│   Browser   │
│  (Next.js)  │
└──────┬──────┘
       │
       │ 1. Upload Image
       ↓
┌─────────────┐
│ Upload API  │
│  Route      │
└──────┬──────┘
       │
       │ 2. Store in JamAI
       ↓
┌─────────────┐
│  JamAI Base │
│ File Storage│
└──────┬──────┘
       │
       │ 3. Get File URI
       ↓
┌─────────────┐
│ Process API │
│   Route     │
└──────┬──────┘
       │
       │ 4. Add Row to Action Table
       ↓
┌─────────────┐
│  JamAI Base │
│Action Table │
└──────┬──────┘
       │
       │ 5. LLM Processing
       ↓
┌─────────────┐
│  Qwen3-VL   │
│    30B      │
└──────┬──────┘
       │
       │ 6. Return Results
       ↓
┌─────────────┐
│   Browser   │
│  (Display)  │
└─────────────┘
```

#### 9.2 Data Flow

1. **Upload Phase**
   * User selects/drops an image file
   * Client validates file type and size
   * FormData sent to `/api/jamai/upload`
   * Image stored in JamAI Base, returns URI
2. **Processing Phase**
   * Client sends image URI to `/api/jamai/process`
   * API ensures Action Table exists
   * Row added to table with image URI
   * JamAI Base triggers LLM processing:
     * Qwen3-VL-30B (embedded LLM) analyzes the image
     * Three separate prompts extract different fields
     * Results stored in table columns
3. **Display Phase**
   * API returns structured receipt data
   * Client displays results
   * History automatically refreshes

#### 9.3 JamAI Base Action Table Structure

The "receipt" table has the following schema:

| Column Name | Type             | Description                |
| ----------- | ---------------- | -------------------------- |
| Image       | Input (image)    | Receipt image file         |
| Shop Name   | LLM Output (str) | Extracted store name       |
| Category    | LLM Output (str) | Receipt category           |
| Total       | LLM Output (str) | Total amount with currency |

Each LLM Output column has:

* **Model:** ellm/Qwen/Qwen3-VL-30B-A3B-Instruct (embedded vision LLM)
* **Temperature:** 0.1 (for consistency)
* **Custom Prompts:** Specialized for each field with table context and variable references

### 10. Customization

#### 10.1 Modify Extraction Prompts

To change what information is extracted, edit the prompts in src/lib/jamai.ts:

```typescript
// Example: Extract date instead of shop name
{
  id: "Date",
  dtype: "str",
  gen_config: {
    object: "gen_config.llm",
    model: "ellm/Qwen/Qwen3-VL-30B-A3B-Instruct",
    system_prompt: "You are a receipt data extraction assistant.",
    prompt: "Table name: \"receipt\"\n\nImage: ${Image}\n\nExtract the date from this receipt. Format as YYYY-MM-DD.",
    temperature: 0.1,
    max_tokens: 50,
  },
}
```

#### 10.2 Add New Fields

To extract additional information:

1. Add the column definition in `ensureTableExists()`:

```typescript
{
  id: "Tax Amount",
  dtype: "str",
  gen_config: {
    object: "gen_config.llm",
    model: "ellm/Qwen/Qwen3-VL-30B-A3B-Instruct",
    system_prompt: "You are a receipt data extraction assistant.",
    prompt: "Table name: \"receipt\"\n\nImage: ${Image}\n\nExtract the tax amount from this receipt. Include currency symbol.",
    temperature: 0.1,
    max_tokens: 50,
  },
}
```

2. Update the `Receipt` interface in src/lib/types.ts:

```typescript
export interface Receipt {
  id: string;
  shopName: string;
  category: string;
  total: string;
  taxAmount: string; // Add new field
  imageUrl: string;
  createdAt: string;
  updatedAt: string;
}
```

3. Update extraction in the process route
4. Update UI components to display the new field

#### 10.3 Change LLM Model

You can use different models for different accuracy and cost trade-offs:

```typescript
gen_config: {
  object: "gen_config.llm",
  model: "ellm/Qwen/Qwen3-VL-30B-A3B-Instruct",  // Default: Fast embedded vision LLM
  // or
  model: "openai/gpt-4o",  // More accurate but more expensive
  // or
  model: "anthropic/claude-3-5-sonnet-20241022",  // Claude Sonnet
  // ...
}
```

**Note:** When using different models, remember to include proper context in prompts. For vision models, use the `${Image}` variable reference as shown in the examples.

#### 10.4 Styling Customization

The app uses Tailwind CSS. Customize colors, spacing, and styles:

```typescript
// Example: Change primary button color
className = "bg-blue-600 hover:bg-blue-700 text-white ...";
```

### 11. Deployment

#### 11.1 Deploy to Vercel

1. **Push to GitHub**

   ```bash
   git init
   git add .
   git commit -m "Initial commit"
   git remote add origin YOUR_REPO_URL
   git push -u origin main
   ```
2. **Deploy on Vercel**
   * Go to [vercel.com](https://vercel.com)
   * Click "Import Project"
   * Select your GitHub repository
   * Add environment variables:
     * `JAMAI_API_KEY`
     * `JAMAI_PROJECT_ID`
   * Click "Deploy"
3. **Verify Deployment**
   * Visit your deployment URL
   * Test receipt upload and processing

#### 11.2 Environment Variables in Production

{% hint style="warning" %}
**Never commit** your `.env.local` file. Always set environment variables in your deployment platform's dashboard.
{% endhint %}

### 12. Troubleshooting

#### Common Issues

**Issue:** "Missing JamAI credentials" error

**Solution:**

* Verify `.env.local` exists in project root
* Check that variables are named correctly:
  * `JAMAI_API_KEY` (not `JAMAI_TOKEN`)
  * `JAMAI_PROJECT_ID` (not `PROJECT_ID`)
* Restart development server after adding env variables

***

**Issue:** Upload fails with "File too large"

**Solution:**

* Ensure image is under 10MB
* Compress image before uploading
* Or increase `MAX_FILE_SIZE` in src/app/api/jamai/upload/route.ts

***

**Issue:** Processing takes too long

**Solution:**

* This is normal for first request (table creation)
* Subsequent requests are faster
* Consider using concurrent: true for background processing

***

**Issue:** Extraction accuracy is poor

**Solution:**

* Use higher quality images (clear, well-lit receipts)
* Try different vision models (e.g., openai/gpt-4o for better accuracy)
* Refine prompts to be more specific
* Ensure prompts include proper table context and variable references like `${Image}`
* Adjust temperature slightly (lower for consistency, higher for variety)

***

**Issue:** History not refreshing

**Solution:**

* Check browser console for errors
* Verify `refreshTrigger` is incrementing
* Clear browser cache
* Check API route returns valid data

### 13. Best Practices

#### Security

1. **Protect API Keys**
   * Never commit `.env.local`
   * Use environment variables in production
   * Rotate API keys periodically
2. **Validate Input**
   * Always validate file types and sizes
   * Sanitize user inputs
   * Implement rate limiting for production
3. **Error Handling**
   * Never expose sensitive error details to users
   * Log errors server-side
   * Provide user-friendly error messages

#### Performance

1. **Optimize Images**
   * Compress images before upload
   * Consider image optimization libraries
2. **Caching**
   * SWR handles client-side caching
   * Consider server-side caching for frequently accessed data
3. **Concurrent Processing**
   * For bulk uploads, use `concurrent: true` in `addRow()`
   * Implement queue system for large batches

#### Code Quality

1. **TypeScript**
   * Define interfaces for all data structures
   * Avoid `any` types
   * Use strict mode
2. **Components**
   * Keep components focused and reusable
   * Extract common logic into hooks
   * Use proper TypeScript types for props
3. **Testing**
   * Add unit tests for utility functions
   * Test API routes
   * Implement E2E tests for critical flows

### 14. Next Steps

#### Enhancements to Try

1. **Multi-file Upload**
   * Process multiple receipts at once
   * Show progress for batch processing
2. **Export Functionality**
   * Export history to CSV
   * Generate expense reports
   * Integration with accounting software
3. **Advanced Features**
   * Receipt search and filtering
   * Date range selection
   * Analytics dashboard
   * Monthly/yearly summaries
4. **Mobile App**
   * Build React Native app
   * Use device camera for capture
   * Offline support with sync
5. **Authentication**
   * Add user accounts
   * Personal receipt history
   * Multi-user support

### 15. Resources

* [JamAI Base Documentation](https://docs.jamaibase.com)
* [Next.js Documentation](https://nextjs.org/docs)
* [TypeScript Documentation](https://www.typescriptlang.org/docs)
* [Tailwind CSS Documentation](https://tailwindcss.com/docs)
* [SWR Documentation](https://swr.vercel.app)

### 16. Support

If you encounter issues or have questions:

1. Check the [JamAI Base Documentation](https://docs.jamaibase.com)
2. Review this tutorial's troubleshooting section
3. Contact JamAI Base support

### Conclusion

You've successfully built a full-stack receipt extraction application using:

* **Next.js 16** with App Router
* **JamAI Base** for AI-powered data extraction
* **TypeScript** for type safety
* **Tailwind CSS** for modern UI
* **SWR** for efficient data fetching

The application demonstrates how easy it is to integrate AI capabilities into web applications using JamAI Base Action Tables. You can now extend this foundation to build more complex document processing applications.

Happy building!


# OpenAPI Documentation

Welcome to the JamAI Base API documentation! This is your fast track to integrating powerful, secure AI solutions into any application using a clean RESTful design.

JamAI Base lets you treat our expert-created AI components—Generative Tables—as simple, ready-to-use AI microservices. By interacting with a single, well-defined API endpoint, your developers can completely bypass the underlying complexity and 'plumbing' of Large Language Models (LLMs). This extensible architecture ensures a seamless developer handoff, enabling your team to focus on rapid UI development and integration, not complex AI logic.

The platform is engineered for enterprise control. Every API interaction is managed by built-in Concurrency and Rate Control features, giving you the power to set departmental throttling, manage API keys, and secure your deployment. This ensures predictable performance and controlled resource allocation across your entire organization.

Our RESTful API is organized around resource-oriented URLs, which directly correspond to the sections in our comprehensive [API Reference](https://jamaibase.readme.io/reference). When you're ready to start building, head to the Making your first API request guide and discover how easy, fast, and controlled AI integration can be.


# Making your first API request

#### Prerequisites

* JamAI Base account: [sign up ](https://jamai.us.auth0.com/u/signup?state=hKFo2SBuZk1mVHZlNXhjRHEteHBjdGkwVUVsQ2RxU3M3MnAxY6Fur3VuaXZlcnNhbC1sb2dpbqN0aWTZIFFiamtaS3k2OUw4QUExY0dEZndYZGZxeFNaalJvaVFio2NpZNkgNGR5YUhDc3UwcEZ5Zzh0S2Rtc1ZjV0N2eGw1N1JibnI)
* A project and its project ID: Create a new project
* Personal Access Token (PAT): Generate in Account Settings (can be global or scoped to a project)&#x20;

#### Getting Started

1\) Create a Personal Access Token (PAT)&#x20;

Create a PAT with the scopes your role needs. You may:&#x20;

* Scope to a specific project (recommended for least privilege), or
* Leave unscoped to use the PAT across all projects.

Include the PAT on every request using Bearer auth:&#x20;

```
Authorization: Bearer <YOUR_PAT> 
```

{% hint style="info" %}
Using our API reference doc/ Swagger UI? Paste your PAT into the Bearer token field.
{% endhint %}

2\) Get project information (quick smoke test)&#x20;

This call returns project metadata, including LLM token usage and DB storage usage. It also verifies your PAT and project\_id.

```
curl --request GET \
     --url 'https://api.jamaibase.com/api/v2/projects?project_id=<project_id>' \
     --header 'Authorization: Bearer <your PAT>' \
     --header 'accept: application/json'
```

Expected results&#x20;

* 200 OK — Project details returned (includes usage fields).
* 422 Unprocessable Entity — Validation error (e.g., missing/invalid project\_id).


# Writing to Action Table with OpenAPI

Use the OpenAPI-described endpoint to insert a new row into an existing Action Table. You’ll validate the schema, POST a row, and verify the result—via cURL or generated clients.

#### Prerequisites

* table\_id for an existing action table
* PAT with scope action\_tables&#x20;

#### Discover the endpoint via OpenAPI&#x20;

In API reference document, navigate to Generative Table (v2) →  [Add rows to a table](https://jamaibase.readme.io/reference/add_rows_api_v2_gen_tables__table_type__rows_add_post)<br>

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FwhIn6kL60ieWA171t7Df%2FFrame%2028505.png?alt=media&amp;token=9484a287-cb5e-458f-ac01-1765640d8254" alt=""><figcaption></figcaption></figure>

#### Create a row (POST)

As shown in Step 2, JamAI Base supports multiple data types, including primitives (integer, float, boolean, text) and file-backed types (image, audio, document). For file-backed types, there’s one extra step: upload the file first, then reference the returned file URI in your create-row request.<br>

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2F5gq1vGFojdmNmqw2j3kC%2FScreenshot%202025-10-27%20at%2000.07.11.png?alt=media&amp;token=ddf2632f-005d-4b16-ba13-37311c33ad4e" alt="" width="375"><figcaption></figcaption></figure>

Below we’ll demonstrate 2 cases:

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td><a href="/developer-reference/openapi-documentation/writing-to-action-table-with-openapi/primitives-integer-float-boolean-text">Primitives (integer, float, boolean, text)</a></td><td><a href="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FQvum6wTqyiJY62vUvInD%2FUntitled-7.png?alt=media&amp;token=e7574009-c095-4d99-bfb4-f90b0ad59592">Untitled-7.png</a></td></tr><tr><td><a href="/developer-reference/openapi-documentation/writing-to-action-table-with-openapi/multimodel-image-audio-document">Multimodel: Image / Audio / Document</a></td><td><a href="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2F93zwc2foqhjXKIuFBtxh%2FUntitled-4.png?alt=media&amp;token=94a8b588-2a70-44ce-a0a0-84b58fd5d64b">Untitled-4.png</a></td></tr></tbody></table>


# Primitives (integer, float, boolean, text)

Use this flow when all fields are primitive types (integer, float, boolean, text). No file upload is required.

#### Steps

1. Inspect the table schema

   In JamAI Base Cloud (or via GET /api/v2/gen\_tables/action?table\_id={table\_id}), confirm:

   * Required fields (input columns)
   * Data types&#x20;
2. Identify input columns

   Note the column names you’ll set (e.g., newKey1, newKey2) and their expected types.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FUHQx2YZKNV1gRE3RB2Qu%2FFrame%2028506.png?alt=media&amp;token=a1809d10-44c0-42a5-8ea7-5e8e49284d65" alt=""><figcaption></figcaption></figure>

\
3\. POST a new row\
Navigate to [Add rows to a table](https://jamaibase.readme.io/reference/add_rows_api_v2_gen_tables__table_type__rows_add_post) then build a JSON body by filling in newKey and its corresponding value, then click "Try It!" send the request.&#x20;

> Remember to copy your PAT and paste into the Bearer token column before clicking "Try It!"&#x20;

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FmMjRPaD5B0nLLLntzqA4%2FFrame%2028507.png?alt=media&amp;token=415a476b-6bd2-4eba-b59b-6e90d1fea7ac" alt=""><figcaption></figcaption></figure>

4. Evaluate the response
   * 200 OK — Returned the existing resource for the same Idempotency-Key.
   * 422 Unprocessable Entity — Validation/type error (check schema and field types).
5. Validate the result
   * In JamAI Base Cloud: open the Action Table and confirm the new row is present.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FHxNBjt0LiG1WIcVmzdRC%2FFrame%2028508.png?alt=media&amp;token=f8376668-1641-4a33-97d0-68086fc54883" alt=""><figcaption></figcaption></figure>


# Multimodel: Image / Audio / Document

Steps

1. Inspect the table schema

   In JamAI Base Cloud (or via GET /api/v2/gen\_tables/action?table\_id={table\_id}), confirm:

   * Required fields (input columns)
   * Data types&#x20;
2. Identify input columns

   Note the column names you’ll set (e.g., newKey1, newKey2) and their expected types.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fsyn7JEnITD8bSWHrI0N8%2FFrame%2028509.png?alt=media&amp;token=8ec64d11-5a63-46df-9c05-e4d78ce81959" alt=""><figcaption></figcaption></figure>

3. Upload the file to get a uri \
   [Navigate to Upload a file ](https://jamaibase.readme.io/reference/upload_file_api_v2_files_upload_post)to the server in API reference document then upload your file (image / audio / document).&#x20;

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FMvmD8X25iek7l7eSF0vh%2FScreenshot%202025-10-27%20at%2002.08.48.png?alt=media&amp;token=cb6b9bca-b3a5-4433-af61-581b9c6b06a4" alt=""><figcaption></figcaption></figure>

If the response is 200, it means the file is uploaded successfully. Copy the output from the RESPONSE column for next step.&#x20;

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FEVPLYYjji8PEfn27WtqG%2FFrame%2028510.png?alt=media&amp;token=e83aa4a7-9973-4925-9949-c55e94ee4fba" alt=""><figcaption></figcaption></figure>

4. Create a row using the uri in the input column\
   Navigate to [Add rows to a table](https://jamaibase.readme.io/reference/add_rows_api_v2_gen_tables__table_type__rows_add_post) then build a JSON body by filling in newKey and the file uri from previous step if the input type is image / audio / document, then click "Try It!" send the request.<br>

   <figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FhOl0kB2wT2uLwlx9sp1W%2Furi.png?alt=media&amp;token=cd7bc8fb-d57e-49d3-aaba-daf9d8e87b5d" alt=""><figcaption></figcaption></figure>

5. Evaluate the response
   * 200 OK — Returned the existing resource for the same Idempotency-Key.
   * 422 Unprocessable Entity — Validation/type error (check schema and field types).

6. Validate the result

   * In JamAI Base Cloud: open the Action Table and confirm the new row is present.

   <figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FbZ50roePpMEzzVBIreL2%2Ffire.png?alt=media&amp;token=f8f2d4c1-4378-4338-a3bc-a125b6f5b0c5" alt=""><figcaption></figcaption></figure>


# TS/JS

## Installation

```bash
$ npm install jamaibase@latest
```

## Usage

### Create API Client

#### Create an API client with baseURL:

```typescript
import JamAI from "jamaibase";

const jamai = new JamAI({ baseURL: "http://localhost:5173/" });
```

#### Create an API client with API key and project id:

```typescript
import JamAI from "jamaibase";

const jamai = new JamAI({ apiKey: "jamai_apikey", projectId: "proj_id" });
```

The default base URL is set to <http://api.jamaibase.com/>

#### Create an API client with custom HTTP client:

```typescript
import axios from "axios";
import JamAI from "jamaibase";

const httpClient = axios.create({
    headers: {
        Authorization: `Bearer ${credentials}`,
        "Content-Type": "application/json"
    }
});

const jamai = new JamAI({
    baseURL: "https://api.jamaibase.com",
    httpClient: httpClient
});
```

#### Create an API client with max-retry and timeout:

```typescript
import JamAI from "jamaibase";

const jamai = new JamAI({
    baseURL: "https://api.jamaibase.com",
    maxRetries: 3,
    timeout: 500
});
```

#### Configure httpAgent / httpsAgent

```typescript
import JamAI from "jamaibase";

const jamai = new JamAI({
    baseURL: "https://api.jamaibase.com"
});

jamai.setHttpagentConfig({
    maxSockets: 100,
    maxFreeSockets: 10,
    freeSocketTimeout: 30000 // free socket keepalive for 30 seconds
});
```

#### Imports

Can be imported from different modules depending on the need:

```typescript
import JamAI from "jamaibase/index.umd.js";
```

### Types

Types can be imported from resources:

```typescript
import { ChatRequest } from "jamaibase/resources/llm/chat";

let response: ChatRequest;
```

### Using the Client

Example of adding a row to an action table:

```typescript
try {
        let data = await jamai.addRow({
            table_type: "action",
            data: [
                {
                    age: 22,
                    height: 178,
                    weight: 66.5,
                    sex: male,
                    preferred_body_type: lean,
                },
            ],
            table_id: "fitness_planner",
            reindex: null,
        });

        console.log(data)
    } catch (error: any) {
        console.error("Error fetching data:", error?.message);
    }
```

Example of adding row with streaming output:

```typescript
try {
    const stream = await jamai.addRowStream({
        table_type: "action",
        table_id: "action-table-example-1",
        data: [{
            Name: "Albert Eistein"
        }]
    });

    const reader = stream.getReader();

    while (true) {
        const { done, value } = await reader.read();
        if (done) {
            console.log("Done");
            break;
        }
        console.log(value);
        if (value) {
            console.log(value?.choices[0]?.message.content);
        }
    }
} catch (err) {}
```

## Constructor Parameters for APIClient Configuration

| Parameter               | Type                | Description                                                  | Default Value               | Required                    |
| ----------------------- | ------------------- | ------------------------------------------------------------ | --------------------------- | --------------------------- |
| baseURL                 | string              | Base URL for the API request.                                | <http://api.jamaibase.com/> | false                       |
| maxRetries              | number              | Maximum number of retries for failed requests.               | 0                           | false                       |
| httpClient              | AxiosInstance       | Axios instance for making HTTP requests.                     | AxiosInstance               | false                       |
| timeout                 | number \| undefined | Timeout for the requests.                                    | undefined                   | false                       |
| apiKey                  | string \| undefined | apiKey.                                                      | undefined                   | required if accessing cloud |
| projectId               | string \| undefined | projectId.                                                   | undefined                   | required if accessing cloud |
| dangerouslyAllowBrowser | boolean             | Allowing the insecure usage of JamAI API Key on client side. | false                       | false                       |

## SDK

The complete SDK Documentation link can be found at <https://embeddedllm.github.io/jamaibase-ts-docs/> [TS/JS](/developer-reference/ts-js)


# Framework Integration

<table data-view="cards"><thead><tr><th data-type="content-ref"></th></tr></thead><tbody><tr><td><a href="/developer-reference/framework-integration/reactjs">ReactJS</a></td></tr><tr><td><a href="/developer-reference/framework-integration/next-js">Next JS</a></td></tr><tr><td><a href="/developer-reference/framework-integration/nuxt">Nuxt</a></td></tr><tr><td><a href="/developer-reference/framework-integration/nlux-+-express.js">NLUX + Express.js</a></td></tr><tr><td><a href="/developer-reference/framework-integration/nlux-frontend-only">NLUX (Frontend Only)</a></td></tr><tr><td><a href="/developer-reference/framework-integration/sveltekit">SvelteKit</a></td></tr></tbody></table>


# ReactJS

## 1. Setup JamAIBase project.

* Create a new project.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fcb6CxaTjAbzhEQWqvs1Y%2Fimage.png?alt=media&amp;token=53d96b1e-f942-4c6b-8614-9b5708b0f238" alt=""><figcaption></figcaption></figure>

* Get the **Project ID**.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FYUP5b640Ay6rWcQakEOt%2Fimage.png?alt=media&amp;token=0e40cd24-0756-4734-b0fd-27f498aa9d2c" alt=""><figcaption></figcaption></figure>

* Create **JamAI API Key** at **Organization > Secrets > Create API Key.**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FiKEvdvKwhs9LsLPImRxc%2FWhatsApp%20Image%202024-05-30%20at%2020.54.27.jpeg?alt=media&amp;token=aa43f907-abda-425a-81b8-93aa18c2004d" alt=""><figcaption></figcaption></figure>

### 2. Install React and Create a New Project

```
npx create-react-app my-app
cd my-app
```

## 3. Install jamaibase.

```
npm install jamaibase
```

## 4. Create and Use the JamAI Client in your React component

Note: Exposing your API key directly in the client-side code is a security risk. For improved security, consider using a framework like Next.js or SvelteKit that allows server-side rendering.

```tsx
// App.tsx

import { useEffect, useState } from "react";
import JamAI from "jamaibase";
import { PageListTableMetaResponse } from "jamaibase/resources/gen_tables/tables";

export default function Home() {
    const [tableData, setTableData] = useState<PageListTableMetaResponse>();

    useEffect(() => {
        const fetchData = async () => {
            const jamai = new JamAI({
                baseURL: process.env.NEXT_PUBLIC_JAMAI_BASEURL!,
                apiKey: process.env.JAMAI_API_KEY,
                projectId: process.env.JAMAI_PROJECT_ID,
            });
            try {
                const response = await jamai.listTables({
                    table_type: "action",
                });
                setTableData(response);
            } catch (err: any) {
                console.error(err.message);
            }
            fetchData();
        };
    }, []);

    return (
        <div>
            <h1>List of Tables</h1>
            <ul>
                {tableData?.items.map((table) => (
                    <li key={table.id}>
                        <h2>Table ID: {table.id}</h2>
                        <h3>Columns:</h3>
                        <ul>
                            {table.cols.map((column) => (
                                <li key={column.id}>
                                    <p>ID: {column.id}</p>
                                    <p>Data Type: {column.dtype}</p>
                                    {/* Render other properties as needed */}
                                </li>
                            ))}
                        </ul>
                    </li>
                ))}
            </ul>
        </div>
    );
}
```

## 5. Start the app

```
npm run dev
```


# Next JS

### 1. Setup JamAIBase project.

* Create a new project.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fcb6CxaTjAbzhEQWqvs1Y%2Fimage.png?alt=media&amp;token=53d96b1e-f942-4c6b-8614-9b5708b0f238" alt=""><figcaption></figcaption></figure>

* Get the **Project ID**.

<div align="left"><figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FYUP5b640Ay6rWcQakEOt%2Fimage.png?alt=media&amp;token=0e40cd24-0756-4734-b0fd-27f498aa9d2c" alt=""><figcaption></figcaption></figure></div>

* Create **JamAI API Key** at **Organization > Secrets > Create API Key.**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FiKEvdvKwhs9LsLPImRxc%2FWhatsApp%20Image%202024-05-30%20at%2020.54.27.jpeg?alt=media&amp;token=aa43f907-abda-425a-81b8-93aa18c2004d" alt=""><figcaption></figcaption></figure>

### 2. Create Table

We will fetch table data and display on our Next.js application. If your project is empty, you can create new table through our UI on <https://cloud.jamaibase.com> or through the API:

```bash
curl 'https://api.jamaibase.com/api/v1/gen_tables/action' \
    -H "Authorization: Bearer <your-jamai-api-key>" \
    -H "X-PROJECT-ID: <project-id>" \
    -H 'Content-Type: application/json' \
    --data-raw '{"id":"CountryCapital","cols":[{"id":"Country","dtype":"str","index":false,"gen_config":null},{"id":"Capital City","dtype":"str","index":false,"gen_config":{"model":"ellm/meta-llama/Llama-3-8B-Instruct","messages":[{"role":"system","content":"You are a helpful assistant."},{"role":"user","content":"What is the capital city of ${Country}? Don'\''t explain.\n<example 1>\nWhat is the capital city of USA?\nNew York\n</example 1>"}],"temperature":1,"max_tokens":10,"top_p":0.1}}]}'
```

### 3. Create a new Next.js Project

```bash
npx create-next-app@latest
cd (path/to/your/app)
```

### 4. Install jamaibase.

```bash
npm install jamaibase
```

### 5. Create `.env.local` file at the root of your project and add the keys:

```
NEXT_PUBLIC_JAMAI_BASEURL=http://api.jamaibase.com/
JAMAI_API_KEY=your_jamai_sk_api_key
JAMAI_PROJECT_ID=your_proj_id
```

### 6. Create a route handler to fetch data.

```tsx
// src/app/api/list-tables/route.ts

import JamAI from "jamaibase";
import { PageListTableMetaResponse } from "jamaibase/resources/gen_tables/tables";
import { NextResponse } from "next/server";

const jamai = new JamAI({
    baseURL: process.env.NEXT_PUBLIC_JAMAI_BASEURL!,
    apiKey: process.env.JAMAI_API_KEY,
    projectId: process.env.JAMAI_PROJECT_ID,
});

export async function GET() {
    try {
        let data: PageListTableMetaResponse = await jamai.listTables({
            table_type: "action",
        });
        return NextResponse.json(data);
    } catch (error: any) {
        console.error("Error fetching tables:", error.response);
        return NextResponse.json(
            { message: "Internal server error" },
            { status: 500 }
        );
    }
}
```

### 7. Then, in your Next.js component, you can fetch this data from the API route and render it

```tsx
// src/app/page.tsx

"use client";

import { PageListTableMetaResponse } from "jamaibase/resources/gen_tables/tables";
import { useEffect, useState } from "react";

export default function Home() {
    const [tableData, setTableData] = useState<PageListTableMetaResponse>();

    useEffect(() => {
        const fetchData = async () => {
            const response = await fetch("/api/list-tables");
            if (response.ok) {
                const data: PageListTableMetaResponse = await response.json();
                setTableData(data);
            } else {
                console.error("Failed to fetch data:", response.statusText);
            }
        };
        fetchData();
    }, []);
    return (
        <main className="flex min-h-screen flex-col  p-24">
            <div className="space-y-6">
                <h1 className="text-4xl">List of Tables</h1>
                <table className="min-w-full divide-y divide-gray-200">
                    <thead className="bg-gray-50">
                        <tr>
                            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                Table ID
                            </th>
                            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                                Columns
                            </th>
                        </tr>
                    </thead>
                    <tbody className="bg-white divide-y divide-gray-200 text-black">
                        {tableData?.items.map((table) => (
                            <tr key={table.id}>
                                <td className="px-6 py-4 whitespace-nowrap">
                                    {table.id}
                                </td>
                                <td className="px-6 py-4 whitespace-nowrap">
                                    <ul className="space-y-2">
                                        {table.cols.map((column) => (
                                            <li key={column.id}>
                                                <span>{column.id}: </span>
                                                <span>{column.dtype}</span>
                                            </li>
                                        ))}
                                    </ul>
                                </td>
                            </tr>
                        ))}
                    </tbody>
                </table>
            </div>
        </main>
    );
}
```

### 8. Start the app

```
npm run dev
```


# Nuxt

### 1. Setup JamAIBase project.

* Create a new project.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fcb6CxaTjAbzhEQWqvs1Y%2Fimage.png?alt=media&amp;token=53d96b1e-f942-4c6b-8614-9b5708b0f238" alt=""><figcaption></figcaption></figure>

* Get the **Project ID**.

<div align="left"><figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FYUP5b640Ay6rWcQakEOt%2Fimage.png?alt=media&amp;token=0e40cd24-0756-4734-b0fd-27f498aa9d2c" alt=""><figcaption></figcaption></figure></div>

* Create **JamAI API Key** at **Organization > Secrets > Create API Key.**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FiKEvdvKwhs9LsLPImRxc%2FWhatsApp%20Image%202024-05-30%20at%2020.54.27.jpeg?alt=media&amp;token=aa43f907-abda-425a-81b8-93aa18c2004d" alt=""><figcaption></figcaption></figure>

### 2. Create a Nuxt app

```bash
npx nuxi init jamai-example-app
cd jamai-example-app
npm install
```

### 3. Install the JamAI Base client library, `jamaibase`

```bash
npm install jamaibase
```

### 4. Create a `.env` file at the root or your project and add the keys

```
PUBLIC_JAMAI_BASEURL=http://api.jamaibase.com/
JAMAI_API_KEY=your_jamai_sk_api_key
JAMAI_PROJECT_ID=your_proj_id
```

### 5. In the `nuxt.config.ts` file add runtimeConfig to use the environment variables

```javascript
runtimeConfig: {
        JAMAI_API_KEY: process.env.JAMAI_API_KEY,
        public: {
            JAMAI_BASEURL: process.env.JAMAI_BASEURL,
            JAMAI_PROJECT_ID: process.env.JAMAI_PROJECT_ID,
        },
    },
```

### 6. Create a page to list tables

We will create a page to list the tables from the project.

Create a new file `pages/index.vue` and add the following code:

```html
<template>
    <main>
        <div class="container">
            <label>Choose Table Type:</label>
            <select v-model="tableType" @change="fetchTables">
                <option value="action">Action</option>
                <option value="chat">Chat</option>
                <option value="knowledge">Knowledge</option>
            </select>
        </div>

        <div class="container">
            <h1>List of Tables</h1>
            <table>
                <thead>
                    <tr>
                        <th>Table ID</th>
                        <th>Columns</th>
                        <th>Column Type</th>
                        <th>Number of Rows</th>
                    </tr>
                </thead>
                <tbody>
                    <tr v-for="table in tableData.items" :key="table.id">
                        <td>{{ table.id }}</td>
                        <td>
                            <ul>
                                <li
                                    v-for="column in table.cols"
                                    :key="column.id"
                                >
                                    {{ column.id }}: {{ column.dtype }}
                                </li>
                            </ul>
                        </td>
                        <td>
                            <ul>
                                <li
                                    v-for="column in table.cols"
                                    :key="column.id"
                                >
                                    {{ column.gen_config ? "Output" : "Input" }}
                                </li>
                            </ul>
                        </td>
                        <td>{{ table.num_rows }}</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </main>
</template>

<script setup>
import { ref, onMounted } from "vue";

const tableType = ref("action");
const tableData = ref({ items: [] });

async function fetchTables() {
    const response = await $fetch(`/api/list-tables?type=${tableType.value}`);

    if (response.success) {
        tableData.value = response.data;
    } else {
        console.error("Failed to fetch data");
    }
}

onMounted(() => {
    fetchTables();
});
</script>
```

Add the following CSS to make the UI look better:

```css
<style scoped>
/* Main layout styling */
main {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    padding: 24px;
}

/* Container styling */
.container {
    max-width: 600px;
    margin: 40px auto;
    padding: 20px;
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

/* Label and select input styling */
label {
    display: block;
    margin-bottom: 8px;
    font-size: 14px;
    font-weight: 500;
    color: #333;
}

select {
    display: block;
    width: 100%;
    padding: 8px;
    border: 1px solid #ccc;
    border-radius: 4px;
    background-color: #fff;
    margin-bottom: 20px;
    font-size: 14px;
}

/* Table styling */
table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 20px;
}

th,
td {
    padding: 12px;
    border: 1px solid #ddd;
    text-align: left;
}

th {
    background-color: #f4f4f4;
    font-weight: 600;
}

tr:nth-child(even) {
    background-color: #f9f9f9;
}

tr:hover {
    background-color: #f1f1f1;
}

/* Responsive styling */
@media (max-width: 600px) {
    .container {
        padding: 10px;
    }

    table,
    th,
    td {
        font-size: 12px;
    }

    th,
    td {
        padding: 8px;
    }
}
</style>
```

### 7. Create server handler to fetch tables:

create a new handler at `server/api/list-tables.js` and add the following code:

```javascript
import JamAI from "jamaibase";

const {
    JAMAI_API_KEY,
    public: { JAMAI_BASEURL, JAMAI_PROJECT_ID },
} = useRuntimeConfig();

const jamai = new JamAI({
    baseURL: JAMAI_BASEURL,
    apiKey: JAMAI_API_KEY,
    projectId: JAMAI_PROJECT_ID,
});

export default defineEventHandler(async (event) => {
    const { type = "action" } = getQuery(event);

    try {
        const data = await jamai.listTables({ table_type: type });
        return { success: true, data: data };
    } catch (error) {
        console.error("Error fetching tables:", error);
        return { success: false, data: "Something went wrong" };
    }
});
```

### 8. Create a page to create new action table

We will create a UI that will create a new action table.&#x20;

Create a new file `server/api/create-table.js` and add the following code:

```html
<template>
    <main>
        <h1>Create Action Table</h1>

        <div v-if="form.success">
            <p>Successfully created the table.</p>
        </div>
        <div v-else-if="form.error">
            <p>Sorry, something went wrong!</p>
        </div>

        <form @submit.prevent="submitForm">
            <label>
                Table ID
                <input v-model="tableId" />
            </label>
            <label>
                Column Name
                <input v-model="columnName" />
            </label>
            <label>
                Column Data Type
                <select v-model="columnDType">
                    <option value="str">str</option>
                    <option value="int">int</option>
                    <option value="float">float</option>
                    <option value="bool">bool</option>
                </select>
            </label>
            <button type="submit">Create</button>
        </form>
    </main>
</template>

<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";

const tableId = ref("");
const columnName = ref("");
const columnDType = ref("str");
const form = ref({ success: false, error: false });
const router = useRouter();

async function submitForm() {
    const { data } = useFetch("/api/create-table", {
        method: "post",
        headers: {
            "Content-Type": "application/json",
        },
        body: {
            table_id: tableId.value,
            column_name: columnName.value,
            column_d_type: columnDType.value,
        },
    });

    if (data.value?.success) {
        form.value.success = true;
        form.value.error = false;
    } else {
        form.value.success = false;
        form.value.error = true;
    }
}
</script>
```

### 9. Create an API to create a new table

Create a server handler at `server/api/create-table.js` to accept the request and create a new table:

```javascript
import JamAI from "jamaibase";

const {
    JAMAI_API_KEY,
    public: { JAMAI_BASEURL, JAMAI_PROJECT_ID },
} = useRuntimeConfig();

const jamai = new JamAI({
    baseURL: JAMAI_BASEURL,
    apiKey: JAMAI_API_KEY,
    projectId: JAMAI_PROJECT_ID,
});

export default defineEventHandler(async (event) => {
    const { table_id, column_name, column_d_type } = await readBody(event);

    try {
        const response = await jamai.createActionTable({
            id: table_id,
            cols: [{ id: column_name, dtype: column_d_type }],
        });

        return { success: true, data: response };
    } catch (error) {
        console.error("error: ", error.response);
        return { success: false, message: "Something went wrong!" };
    }
});
```

### 10. Add default layout

Finally, create a defalut layout (`layouts/defalut.vue`) to navigate between pages:

```html
<template>
    <div>
        <nav>
            <ul>
                <li><NuxtLink to="/">Home</NuxtLink></li>
                <li><NuxtLink to="/create-table">Create Table</NuxtLink></li>
            </ul>
        </nav>
        <slot />
    </div>
</template>

<script setup></script>

<style scoped>
nav {
    background-color: #f8f9fa;
    padding: 10px;
}

nav ul {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    gap: 20px;
}

nav ul li {
    display: inline;
}

nav a {
    text-decoration: none;
    color: #007bff;
}

nav a:hover {
    text-decoration: underline;
}
</style>
```

### 12. Start the app

Start the app and go to <http://localhost:3000> in a browser and you should see the list of tables in your project.

```
npm run dev
```


# NLUX + Express.js

## 1. Create Chat Agent

* Create **Project**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fcb6CxaTjAbzhEQWqvs1Y%2Fimage.png?alt=media&amp;token=53d96b1e-f942-4c6b-8614-9b5708b0f238" alt=""><figcaption></figcaption></figure>

* Create a **Chat Agent** at **Project >  Chat Table > New Agent**
* Configure your agent. You can define the following parameters.

| Parameter     | Description                                          |   |
| ------------- | ---------------------------------------------------- | - |
| Agent ID      | Name of Agent                                        |   |
| Model         | LLM model                                            |   |
| Temperature   |                                                      |   |
| Max tokens    |                                                      |   |
| Top-p         |                                                      |   |
| System Prompt | Define the persona of the model                      |   |
| User message  | Define the conversational opener between User and AI |   |
| AI response   | Define the conversational opener between User and AI |   |

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FK2mZz1WPgnd15hO1IIRh%2Fimage.png?alt=media&amp;token=0cfb965f-9667-4679-bd57-ff0dc5fe9e90" alt=""><figcaption></figcaption></figure>

## 2. Get the following information to start using JamAIBase

* Get the **Agent ID.**
* Get the **Project ID**.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FYUP5b640Ay6rWcQakEOt%2Fimage.png?alt=media&amp;token=0e40cd24-0756-4734-b0fd-27f498aa9d2c" alt=""><figcaption></figcaption></figure>

* Create **JamAI API Key** at **Organization > Secrets > Create API Key.**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FiKEvdvKwhs9LsLPImRxc%2FWhatsApp%20Image%202024-05-30%20at%2020.54.27.jpeg?alt=media&amp;token=aa43f907-abda-425a-81b8-93aa18c2004d" alt=""><figcaption></figcaption></figure>

## 3. Clone the Chatbot Example

```bash
git clone https://github.com/EmbeddedLLM/jamaibase-cookbook

# example is at https://github.com/EmbeddedLLM/jamaibase-cookbook/tree/main/quickstart/nlux/0002-chat-with-jamai-agent-express
```

## 4. Create .env file

Create an`.env` file at \`0002-chat-with-jamai-agent-express/server\` with the following content.

````typescript
// 0002-chat-with-jamai-agent-express/server.env
```properties
BASEURL="https://api.jamaibase.com"
JAMAI_APIKEY="<jamaibase-api-key>"
PROJECT_ID="<jamaibase-project-id>"
AGENT_ID="<your-agent-id"

````

## 5. Start app

```bash
cd jamaibase-cookbook/quickstart/nlux/0002-chat-with-jamai-agent-expressjs
npm install

# on first terminal
npm run server

# on second terminal
npm run app
```

* Open up your app at <http://localhost:5173> .&#x20;

## 6. Manage and View Conversation Histories in Chat Table

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fj9eHaMq1N4FEIWgV76M8%2Fimage.png?alt=media&amp;token=b538ed8f-0d28-45c2-997a-41164999d489" alt=""><figcaption></figcaption></figure>


# NLUX (Frontend Only)

Want to get started quickly? Check out this frontend only example! They're the fastest way to see the SDK in action. However, these examples are meant for testing and exploration only. In a real application, it's important to protect your API key. We'll show you how to do that in our secure setup guide.

## 1. Create Chat Agent

* Create **Project**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fcb6CxaTjAbzhEQWqvs1Y%2Fimage.png?alt=media&amp;token=53d96b1e-f942-4c6b-8614-9b5708b0f238" alt=""><figcaption></figcaption></figure>

* Create a **Chat Agent** at **Project >  Chat Table > New Agent**
* Configure your agent. You can define the following parameters.

| Parameter     | Description                                          |   |
| ------------- | ---------------------------------------------------- | - |
| Agent ID      | Name of Agent                                        |   |
| Model         | LLM model                                            |   |
| Temperature   |                                                      |   |
| Max tokens    |                                                      |   |
| Top-p         |                                                      |   |
| System Prompt | Define the persona of the model                      |   |
| User message  | Define the conversational opener between User and AI |   |
| AI response   | Define the conversational opener between User and AI |   |

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FK2mZz1WPgnd15hO1IIRh%2Fimage.png?alt=media&amp;token=0cfb965f-9667-4679-bd57-ff0dc5fe9e90" alt=""><figcaption></figcaption></figure>

## 2. Get the following information to start using JamAIBase

* Get the **Agent ID.**
* Get the **Project ID**.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FYUP5b640Ay6rWcQakEOt%2Fimage.png?alt=media&amp;token=0e40cd24-0756-4734-b0fd-27f498aa9d2c" alt=""><figcaption></figcaption></figure>

* Create **JamAI API Key** at **Organization > Secrets > Create API Key.**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FiKEvdvKwhs9LsLPImRxc%2FWhatsApp%20Image%202024-05-30%20at%2020.54.27.jpeg?alt=media&amp;token=aa43f907-abda-425a-81b8-93aa18c2004d" alt=""><figcaption></figcaption></figure>

## 3. Clone the Chatbot Example

```bash
git clone https://github.com/EmbeddedLLM/jamaibase-cookbook

# example is at https://github.com/EmbeddedLLM/jamaibase-cookbook/tree/main/quickstart/nlux/0001-chat-with-jamai-agent
```

## 4. Update the App.tsx

```typescript
...
const baseURL = "https://api.jamaibase.com";
const apiKey = "<jamaibase-api-key";
const projectId = "<jamaibase-project-id>";
const agentId = "<your-agent-id";
...

```

## 5. Start app

```bash
cd jamaibase-cookbook/quickstart/nlux/0001-chat-with-jamai-agent
npm install
npm run dev
```

* Open up your app at <http://localhost:5173> .&#x20;

## 6. Manage and View Conversation Histories in Chat Table

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fj9eHaMq1N4FEIWgV76M8%2Fimage.png?alt=media&amp;token=b538ed8f-0d28-45c2-997a-41164999d489" alt=""><figcaption></figcaption></figure>


# SvelteKit

### 1. Setup JamAIBase project.

* Create a new project.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fcb6CxaTjAbzhEQWqvs1Y%2Fimage.png?alt=media&amp;token=53d96b1e-f942-4c6b-8614-9b5708b0f238" alt=""><figcaption></figcaption></figure>

* Get the **Project ID**.

<div align="left"><figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FYUP5b640Ay6rWcQakEOt%2Fimage.png?alt=media&amp;token=0e40cd24-0756-4734-b0fd-27f498aa9d2c" alt=""><figcaption></figcaption></figure></div>

* Create **JamAI API Key** at **Organization > Secrets > Create API Key.**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FiKEvdvKwhs9LsLPImRxc%2FWhatsApp%20Image%202024-05-30%20at%2020.54.27.jpeg?alt=media&amp;token=aa43f907-abda-425a-81b8-93aa18c2004d" alt=""><figcaption></figcaption></figure>

### 2. Create a SvelteKit app using the npm create command.

```bash
npm create svelte@latest jamai-example-app
cd jamai-example-app
npm install
```

### 3. Install the JamAI Base client library, `jamaibase`

```bash
npm install jamaibase
```

### 4. Create a `.env` file at the root or your project and add the keys

```
PUBLIC_JAMAI_BASEURL=http://api.jamaibase.com/
JAMAI_API_KEY=your_jamai_sk_api_key
JAMAI_PROJECT_ID=your_proj_id
```

### 5. Create a page

We will create a UI that will create a new action table.&#x20;

Create a new file `src/routes/create-table/+page.svelte` and add the following form.

```html
<script>
   <script lang="ts">
    export let form;
</script>

<main>
    <h1>Create Action Table</h1>

    {#if form?.success}
        <!-- this message is ephemeral; it exists because the page was rendered in
           response to a form submission. it will vanish if the user reloads -->
        <p>Successfully created the table.</p>
    {/if}
    {#if !form?.success}
        <!-- this message is ephemeral; it exists because the page was rendered in
           response to a form submission. it will vanish if the user reloads -->
        <p>Sorry, something went wrong!</p>
    {/if}

    <form method="POST" action="/create-table">
        <label>
            Table ID
            <input name="table_id" />
        </label>
        <label>
            Column Name
            <input name="column_name" />
        </label>
        <label>
            Columng Data Type
            <select name="column_d_type">
                <option value="str">str</option>
                <option value="int">int</option>
                <option value="float">float</option>
                <option value="bool">bool</option>
            </select>
        </label>
        <button type="submit">Create</button>
    </form>
</main>
```

### 6. Create a [form action](https://kit.svelte.dev/docs/form-actions)

We will utilize form action provided by sveltekit to accept the inputs from the client side. Then use jamai client to create a action table.

Create a `+page.server.ts` file in the same path of the form element to accept post data from the form.

```javascript
import JamAI from "jamaibase";
import { fail } from "@sveltejs/kit";
import { PUBLIC_JAMAI_URL } from "$env/static/public";
import { JAMAI_API_KEY, JAMAI_PROJECT_ID } from "$env/static/private";

const jamai = new JamAI({
    baseURL: PUBLIC_JAMAI_URL,
    apiKey: JAMAI_API_KEY,
    projectId: JAMAI_PROJECT_ID,
});

export const actions = {
    default: async ({ request }) => {

        type DTypes = "float" | "int" | "bool" | "str" | undefined;

        const data = await request.formData();
        const tableId = data.get("table_id")?.toString();
        const columnName = data.get("column_name")?.toString();
        const columnDType = data.get("column_d_type")?.toString() as DTypes;

        console.log("data: ", data);
        try {
            const response = await jamai.createActionTable({
                id: tableId!,
                cols: [{ id: columnName!, dtype: columnDType! }],
            });

            return { success: true, data: response };
        } catch (error) {
            console.error(error);
            fail(500, { message: "Something went wrong!" });
        }
    },
};
```

### 7. Start the app

Start the app and go to <http://localhost:5173> in a browser.

```
npm run dev
```


# Action Table

{% embed url="<https://www.jamaibase.com/_app/immutable/assets/Action-Table.CsAFJSZa.mp4>" %}
Demo on Action Table Usage
{% endembed %}

1. Go to **>> Project >> Action Table**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2F0m3lUA4ZvySHNPfAHO1M%2FScreenshot%20(270).png?alt=media&amp;token=e6ee7b2a-a6fc-4ccc-8ca7-2809b67aa0ce" alt=""><figcaption></figcaption></figure>

2. Create a **New Action Table**. Define the **Column ID** and the **Data Type**. Each column can be specified as **Output** ( :heavy\_check\_mark: ) or **Input. Every Output Column is an LLM Agent.**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FlKHXVsD5e5DHEJKD2fvW%2FScreenshot%20(272).png?alt=media&amp;token=6329ba7c-eef8-4310-9705-40749c23f81c" alt=""><figcaption></figcaption></figure>

3. Open up the created table.
4. Configure all the **Output Columns,** by c**licking on the** <img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fzt91xt8QdMURiy4elkEa%2Fmore.png?alt=media&amp;token=699bac55-a55c-4448-9f62-5682c8516163" alt="" data-size="line">  of the **Output Column** and **open settings.**

* Under **Prompt** tab, you can write the prompt of the LLM Agent. You can referenced any of the column to the left of the current column. E.g. In this case. **Malay** Output Column can reference the Fruit Name Column as an input in its prompt, but not any other columns (Chinese, Arabic, etc,) which are to the right of the **Malay** Output Column.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FdFGmR2IwNzKscdh0Oqlw%2FScreenshot%20(274).png?alt=media&amp;token=8eaa711a-34d7-48a9-8be3-98c9257d7fab" alt=""><figcaption><p>Specify your prompt</p></figcaption></figure>

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2F3wLgoyYpZpR2QDlYhHyK%2FScreenshot%20(275).png?alt=media&amp;token=9da9b8ca-ad11-4954-85ec-ef0bf887d767" alt=""><figcaption><p>Define your LLM Agent Parameters</p></figcaption></figure>

5. **Click** **Update.**
6. Start using the table!
   1. On the **app.jamaibase.com,** in the table view, click <img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FjibSYo6Gn7zIgWkFn21S%2FScreenshot%20(278)-AddRow.png?alt=media&amp;token=7fa157f1-3827-4ed9-99cf-f157f51a95b4" alt="" data-size="line">

      <figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FPDenTqgOr76DMzWlHUFd%2FScreenshot%20(277).png?alt=media&amp;token=7b29da14-a81e-4ce7-8073-4817663fd9c0" alt=""><figcaption></figcaption></figure>
   2. You can also add row through our SDK [Broken mention](broken://pages/Dzw6vpYEfw7ZfRPixN4l). at endpoint app.jamaibase.com with auth.

## Advanced Usage: Give your LLM Agent Knowledge

1. When you are updating the configuration  <img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fzt91xt8QdMURiy4elkEa%2Fmore.png?alt=media&amp;token=699bac55-a55c-4448-9f62-5682c8516163" alt="" data-size="line">   of the Output Column, enable the **RAG.**
2. There are a few settings to configure:

* **k**: The number of maximum Knowledge Row that can be fetched as references during RAG.
* **Reranking Model** (<mark style="color:orange;">optional</mark>): Reranking the Knowledge Row retrieved before passing it into LLM Agent.
* **Knowledge Table**: The table where LLM Agent will search for Knowledge Rows and use it as references.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2F0yvHjh0aeO9jvX0d7s1a%2FScreenshot%20(279).png?alt=media&amp;token=d2f943aa-84d0-4e7a-84dd-0bf83e35fa56" alt=""><figcaption></figcaption></figure>


# Chat Table

{% embed url="<https://www.jamaibase.com/_app/immutable/assets/Chat-Table.149z18hV.mp4>" %}
Demo of Chat Table Usage
{% endembed %}

## Basic Usage

1. Every chat table needs a LLM Agent. Let's create our agent. **>> Chat Table >> Agents >> New Agent.**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fkl4Y0TAO3Ks8gQqfYaM7%2FScreenshot%20(257).png?alt=media&amp;token=8e99e909-cf0f-4e11-a1bb-9aac32267522" alt=""><figcaption></figcaption></figure>

2. Let's add and configure our LLM Agent.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FeKuXWtehflaH2wvODFuh%2FScreenshot%20(256).png?alt=media&amp;token=31149c42-9da5-4c1f-90f6-14315519ef44" alt=""><figcaption></figcaption></figure>

Configuration Parameters:

* Agent ID: Name of your agent.
* Models: LLM Model.
* Temperature:
* Max tokens:
* Top-p:
* Customize system prompt (<mark style="color:orange;">optional</mark>): Define the behaviour of your LLM Agent.
* User message (<mark style="color:orange;">optional</mark>): Set a first reply to your LLM Agent.
* AI response (<mark style="color:orange;">optional</mark>): Conversational opener by your LLM Agent.

3. Select the **LLM Agent** that you have created and create a **new conversation.**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2F3bp3rF8fnKg4YRKOcdMo%2FScreenshot%20(257).png?alt=media&amp;token=ff2990ed-b904-4fe6-87d6-0205ce219159" alt=""><figcaption></figcaption></figure>

4. You will be brought to the **Chat Table** interface.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FnBqOkSZbBQgjGmFQF646%2FScreenshot%20(261).png?alt=media&amp;token=0e2b27c3-4983-49c8-82df-ce85c8167a00" alt=""><figcaption></figcaption></figure>

5. Toggle between **Conversation Mode** and **Table Mode** using the toggle bar on the top right of the interface.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FLwbxJ5Opfo3rwrUUNZaP%2FToggleChat-Table.png?alt=media&amp;token=22f878c5-79c8-47cf-8446-bc5fc56ea951" alt=""><figcaption><p>Toggle button is circled in Orange.</p></figcaption></figure>

6. Start chatting with your LLM Agent.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FCoO3HtRrL79y7yHYftSs%2FScreenshot%20(266).png?alt=media&amp;token=4f141db2-11ce-4e2d-90a4-fd649fb90901" alt=""><figcaption></figcaption></figure>

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FOeKnsVAMhf4eNDJNry8T%2FScreenshot%20(267).png?alt=media&amp;token=f46ecc14-df91-4a5a-90cc-c2eff1b6bed3" alt=""><figcaption></figcaption></figure>

## Advanced Usage: Chat with your Knowledge Table

1. After creating your LLM Agent, select it. Click on the LLM Agent. You will be brought to a **Table View**.
2. Update the LLM Agent configuration to use RAG.
   1. If you are in **Table View. Click on the** <img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Fzt91xt8QdMURiy4elkEa%2Fmore.png?alt=media&amp;token=699bac55-a55c-4448-9f62-5682c8516163" alt="" data-size="line">  of **AI** (<mark style="color:orange;">output</mark> column)and **open settings.**
   2. If you are in **Conversation View. Click** <img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FjbPAdx9ZUzUSjRVu5FXT%2Fsettings.png?alt=media&amp;token=0f8efd18-a9a3-437d-ba59-89f230375670" alt="" data-size="line"> to bring up the Model setting.
3. There are a few settings to configure:

* **k**: The number of maximum Knowledge Row that can be fetched as references during RAG.
* **Reranking Model** (<mark style="color:orange;">optional</mark>): Reranking the Knowledge Row retrieved before passing it into LLM Agent.
* **Knowledge Table**: The table to search for Knowledge Rows as references.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FfXFLWABieezcO1u7D9uh%2FScreenshot%20(263).png?alt=media&amp;token=53d66901-c75c-4ab3-abdf-a9562feed182" alt=""><figcaption></figcaption></figure>

4. Start chatting.


# Knowledge Table

{% embed url="<https://www.jamaibase.com/_app/immutable/assets/Knowledge-Table.BjVTVFjb.mp4>" %}
Demo of Knowledge Table Usage
{% endembed %}

1. Go to **>> Project >> Knowledge Table**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2Foku0LJklfJoUK5dHpYO3%2FScreenshot%20(88).png?alt=media&amp;token=786e585d-22aa-418a-aefb-3ea222257b61" alt=""><figcaption></figcaption></figure>

1. Create a **New Knowledge Table** with your desired Table ID (table name) and pick a **Text Embedding Model.**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FX4PAUURboC8cdOobgeOT%2FScreenshot%20(245).png?alt=media&amp;token=f75aa5d7-4f40-4d21-8f28-6f42603b7719" alt=""><figcaption></figcaption></figure>

1. Open the table that you have just created. **Upload** **your files** to fill up the **Knowledge Table.**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FBpmrKGz2FrTJYq402sRw%2FScreenshot%20(138).png?alt=media&amp;token=219ca70d-28e0-46dd-881a-e45e0e89d84d" alt=""><figcaption></figcaption></figure>

***And.... Let the magic begins. Jamjam** will cast an extraction spell to process your files into Knowledge Rows which you can quickly search up later.*

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FUqArVeoVmjmfoTKFgRPD%2FScreenshot%20(216).png?alt=media&amp;token=f70feb1c-a991-455e-b2ce-3b7dcf70503c" alt=""><figcaption></figcaption></figure>

When it is done you will see the **Knowledge Table** is filled with **Knowledge Rows.**

3. Now it is ready to be used for **Searching! Checkout** [Action Table](/using-the-platform/action-table) **and** [Chat Table](/using-the-platform/chat-table) on How-To -Use the **Knowledge Table**.

## Advanced LLM Magic

1. After you created a new Knowledge Table, add new Output Column (**>> Action >> Add&#x20;**<mark style="color:orange;">**output**</mark>**&#x20;column)**

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FK4JOS4mzGGys7aWSuvv2%2FScreenshot%20(247).png?alt=media&amp;token=e8ae0cdf-71fa-4390-bfc4-d56cd6770113" alt=""><figcaption></figcaption></figure>

2. Setup your Knowledge LLM Agent. It will help you to further process the text content to enrich your **Knowledge Row**.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FUk15s9IXEcORlTC2ZPra%2FScreenshot%20(248).png?alt=media&amp;token=bd3e1541-659b-4837-955f-f61fe6ff720e" alt=""><figcaption></figcaption></figure>

To setup LLM Agent, follow this spell template:

* Column ID: \<The title of the column>
* Data Type: **str**
* Models: The LLM models.
* Temperature: 0.1
* Max Tokens: 512
* Top-p: 1.0
* Customize prompt: \<Prompt to process the Input columns>

*To refer to the column content, you can click on the Columns Title listed and it will be automatically referenced in the prompt in the form of `${Column ID}`.*

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FtodiTCXKrqoqPQmj13zs%2FScreenshot%20(248)%20-%20cropped.png?alt=media&amp;token=4ef7983b-cad5-4ce9-9e31-7c7fb01ac55f" alt=""><figcaption></figcaption></figure>

* Customize system prompt: \<Agent system prompt>

3. Let's start using the **Knowledge Table** and see the LLM agent magically process your **Knowledge Row** when you upload files to the **Knowledge Table**.


# Supported Models

The following is the list of model and provider that are currently supported by JamAIBase.

## Chat Models

<table data-full-width="false"><thead><tr><th width="120">Provider</th><th width="158">Model</th><th width="100">Context Window</th><th width="98">Output Tokens</th><th width="134">Input Cost (1M token)</th><th width="140">Output Cost (1M token)</th></tr></thead><tbody><tr><td>OpenAI</td><td>GPT-4 Turbo</td><td>128K</td><td>4096</td><td>10</td><td>30</td></tr><tr><td>OpenAI</td><td>GPT-3.5 Turbo</td><td>16385</td><td>4096</td><td>0.5</td><td>1.5</td></tr><tr><td>Anthropic</td><td>Claude-3 Haiku</td><td>200K</td><td>4096</td><td>0.25</td><td>1.25</td></tr><tr><td>Anthropic</td><td>Claude-3 Opus</td><td>200K</td><td>4096</td><td>15</td><td>75</td></tr><tr><td>Anthropic</td><td>Claude-3 Sonnet</td><td>200K</td><td>4096</td><td>3</td><td>15</td></tr><tr><td>Google Vertex AI</td><td>Gemini-1.0 Pro</td><td>32760</td><td>8192</td><td>0.5</td><td>1.5</td></tr><tr><td>Google Vertex AI</td><td>Gemini-1.5 Pro</td><td>1000K</td><td>8192</td><td>None</td><td>None</td></tr><tr><td>Cohere</td><td>Command-R</td><td>128K</td><td>4096</td><td>0.5</td><td>1.5</td></tr><tr><td>Cohere</td><td>Command-R+</td><td>128K</td><td>4096</td><td>3</td><td>15</td></tr></tbody></table>

{% content-ref url="/pages/clAAFIzteUSbCrcHNNYn" %}
[Which LLM Should You Choose?](/using-the-platform/model-providers/which-llm-should-you-choose)
{% endcontent-ref %}

## Vision LLM (Support Image)

<table data-full-width="false"><thead><tr><th width="120">Provider</th><th width="157">Model</th><th width="105">Context Window</th><th width="103">Output Tokens</th><th width="116">Input Cost (1M token)</th><th width="143">Output Cost (1M token)</th></tr></thead><tbody><tr><td>OpenAI</td><td>GPT-4 Vision</td><td>128K</td><td>4096</td><td>10</td><td>30</td></tr><tr><td>Anthropic</td><td>Claude-3 Haiku</td><td>200K</td><td>4096</td><td>0.25</td><td>1.25</td></tr><tr><td>Anthropic</td><td>Claude-3 Opus</td><td>200K</td><td>4096</td><td>15</td><td>75</td></tr><tr><td>Anthropic</td><td>Claude-3 Sonnet</td><td>200K</td><td>4096</td><td>3</td><td>15</td></tr><tr><td>Google Vertex AI</td><td>Gemini-1.0 Pro Vision</td><td>16384</td><td>2048</td><td>0.5</td><td>1.5</td></tr><tr><td>Google Vertex AI</td><td>Gemini-1.5 Pro Vision</td><td>1000K</td><td>8192</td><td>None</td><td>None</td></tr></tbody></table>

{% content-ref url="/pages/NQbiwjugHURDoSwQuvOj" %}
[Comparative Analysis of Large Language Models in Vision Tasks](/using-the-platform/model-providers/comparative-analysis-of-large-language-models-in-vision-tasks)
{% endcontent-ref %}


# Which LLM Should You Choose?

A guide for choosing the best LLM API for your use case

**eneral Recommendation**

<table data-full-width="false"><thead><tr><th width="157">Use Case</th><th width="454">Use Case Description</th><th>Preferred Model(s)</th></tr></thead><tbody><tr><td>General Tasks (Open-Ended)</td><td>Handle diverse requests without specific instructions. Relies on the model's broad knowledge and understanding.</td><td>GPT-4-Turbo</td></tr><tr><td>General Tasks (Guided)</td><td>Handling a wide range of tasks with the help of few-shot or chain-of-thought prompting, enabling smaller, faster models to produce reasonably good quality outputs with reduced latency and cost.</td><td>Claude 3 Haiku</td></tr><tr><td>Low Latency &#x26; Cost-Sensitive</td><td>Prioritize quick responses and affordability over the absolute best output quality. Ideal for real-time or high-volume applications.</td><td>Claude 3 Haiku/Mixtral</td></tr><tr><td>Auto Prompt Rewriter and Refinement</td><td>Automatically refine prompts for better results. Understands intent and suggests changes for improved effectiveness with smaller LLMs.</td><td>GPT-4-Turbo/ Claude 3 Opus</td></tr></tbody></table>

**Core Applications:**

<table><thead><tr><th width="457">Use Case</th><th>Model</th></tr></thead><tbody><tr><td><p><strong>Language Understanding and Generation:</strong> </p><ul><li>Content Creation and Summarization</li><li>Language Translation and Localization</li><li>Conversational AI and Virtual Assistants (Chatbot)</li><li>Sentiment Analysis</li><li>Intelligent Writing Assistance</li></ul></td><td><p>Claude 3 Haiku</p><p>Claude 3 Opus for Multilingual</p></td></tr><tr><td><p><strong>Computer Vision and Multimodal Integration:</strong></p><ul><li>Image Captioning</li><li>Visual Question Answering</li><li>Multimodal Data Processing for Enhanced Insights</li></ul></td><td>Claude 3 Haiku</td></tr><tr><td><p><strong>Information Management:</strong> </p><ul><li>Data Retrieval and Analysis</li><li>Knowledge Management and Research Support</li><li>Personalization and Recommendation Systems</li><li>Visual/Text Search</li></ul></td><td>Option: <br>Retrieval-augmented generation (RAG)<br>Model:<br>Claude 3 Haiku/ Command R+</td></tr></tbody></table>

**Specialized Applications:**

<table><thead><tr><th width="618">Use Case</th><th>Model</th></tr></thead><tbody><tr><td><p><strong>Code and Automation:</strong></p><ul><li><strong>Development and Coding Support:</strong> Automating code generation and offering software development assistance.</li><li><strong>Operational Efficiency:</strong> Enhancing process automation and intelligent document processing for improved workflow.</li><li><strong>Content and Security Management:</strong> Implementing content moderation, filtering, and mechanisms for fraud detection and prevention.</li></ul></td><td>Claude 3 Opus</td></tr><tr><td><p><strong>Business and Commerce:</strong></p><ul><li><strong>E-commerce and Retail Enhancements:</strong> Optimizing online shopping experiences and supporting retail operations.</li><li><strong>Customer Engagement:</strong> Utilizing chatbots for service and support, and deploying personalized email marketing campaigns.</li><li><strong>Marketing and Sales:</strong> Developing targeted marketing strategies, generating product descriptions, and leveraging intelligent upselling and cross-selling.</li><li><strong>Market Insights:</strong> Conducting market research, analyzing customer reviews, and forecasting demand.</li><li><strong>Product Management:</strong> Automating product categorization, tagging, and inventory management.</li></ul></td><td>Claude 3 Haiku</td></tr><tr><td><p><strong>Education and Learning:</strong></p><ul><li><strong>Personalized Education:</strong> Creating adaptive learning materials and tools tailored to individual learning styles and needs.</li></ul></td><td>Claude 3 Opus</td></tr><tr><td><p><strong>Geospatial and Environmental Analysis:</strong></p><ul><li><strong>Spatial Data Insights:</strong> Offering mapping and spatial data analysis for various applications.</li></ul></td><td>Claude-3 Sonnet</td></tr></tbody></table>

## Performance vs Price Analysis

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FOldmjMmYrwMZKFnYzFUI%2Fvllmchart.png?alt=media&amp;token=5753558c-067a-4016-8f88-84d16b1945ac" alt=""><figcaption><p>Input token price: This graph illustrates a comparison of popular large language models (LLMs), plotting 'LMSYS Better Arena Elo' against 'Price per 1M Token (Input)', both on a logarithmic scale.</p></figcaption></figure>


# Comparative Analysis of Large Language Models in Vision Tasks

Examining GPT-4 Turbo, Claude-3 Family, and Gemini for Performance and Value.

### Introduction <a href="#introduction" id="introduction"></a>

This documentation provides an analysis of various Large Language Models (LLMs) with a focus on their performance in vision-related tasks. The models under consideration include GPT-4 Turbo, Claude-3 Haiku, Claude-3 Opus, Claude-3 Sonnet, Gemini-1.0 Pro, and Gemini-1.5 Pro. The analysis covers aspects such as pricing, overall quality, and performance in specific vision and reasoning tasks.

### TL;DR

* **Budget-conscious start:** Try Claude-3 Haiku first; it offers the best quality score for the lowest price.
* **Quality boost:** If you find text generation quality is crucial for your project, consider upgrading to GPT-4 Turbo. It has a solid quality score for text generation, though it comes at a higher cost.
* **Other options:** The Claude-3 models (Opus and Sonnet) and the Gemini-1.5 Pro Vision might be suitable depending on your specific use case and quality vs. cost trade-offs.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FpWmvfNSPRyqOzkw5V5ig%2Fvllmchart-v1.5.png?alt=media&amp;token=deaad015-793d-4576-9cb4-2083eb289a5c" alt=""><figcaption><p>This scatter plot compares the quality and price of several vision LLMs. Average quality is determined by the combined scores of MMMU, DocVQA, MathVista, AI2D, and ChartQA. Price reflects the cost of processing one full-resolution image, along with 100 tokens for input (prompt) and 100 tokens for output.</p></figcaption></figure>

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FyOscUGsmGNxAzq6Kx685%2Fradar_chart.png?alt=media&amp;token=2ec4c7ad-96e3-4c92-b4a8-b6ff405fc1f8" alt="" width="375"><figcaption><p>This radar chart comparing the performance of vision-based language models across five diverse tasks: Math &#x26; reasoning (MMMU), DocVQA, MathVista, Science diagrams (AI2D), and ChartQA.</p></figcaption></figure>

**Model Overview**

The models evaluated in this analysis are:

* **GPT-4 Turbo**: A high-performance model known for text generation quality. Can do vision pretty well.
* **Claude-3 Haiku**: Optimized for fast and cost-effective performance, making it ideal for most generic enterprise tasks.
* **Claude-3 Opus**: Offers the highest quality among the models, suitable for tasks requiring deep understanding.
* **Claude-3 Sonnet**: Provides the best performance/price ratio, making it a versatile choice.
* **Gemini-1.0 Pro**: A model designed for vision tasks, with a focus on affordability.
* **Gemini-1.5 Pro**: An updated version of Gemini-1.0 Pro, offering improved quality.

### Pricing and Quality <a href="#pricing-and-quality" id="pricing-and-quality"></a>

Price reflects the cost of processing one full-resolution image, along with 100 tokens for input (prompt) and 100 tokens for output.&#x20;

Average quality is determined by the combined scores of MMMU, DocVQA, MathVista, AI2D, and ChartQA.

| Model                 | Price   | Average quality |
| --------------------- | ------- | --------------- |
| GPT-4 Vision          | 0.01505 | 70.36%          |
| Claude-3 Haiku        | 0.00055 | 70.76%          |
| Claude-3 Opus         | 0.033   | 73.62%          |
| Claude-3 Sonnet       | 0.0066  | 72.06%          |
| Gemini-1.0 Pro Vision | 0.0027  | 65.84%          |
| Gemini-1.5 Pro Vision | Unknown | 71.74%          |

* **Note about resolution:**
  * **GPT-4 Turbo:** Images are first scaled to fit within a 2048 x 2048 square while maintaining their aspect ratio. They are then resized so that the shortest side of the image is 768 pixels long.
  * **Claude-3:** Can handle slightly larger images, for example, 1092 x 1092 pixels for a 1:1 aspect ratio and 819 x 1456 pixels for a 9:16 aspect ratio.
  * **Gemini:** No information is available on its image resolution capabilities.


# Frequently Asked Questions (FAQ)

**1. What is JamAI Base?**

JamAI Base is an open-source platform designed to simplify the integration of Large Language Models (LLMs) into applications. It provides a **Backend as a Service (BaaS)** for LLM integration, leveraging tools like LanceDB to manage and query embeddings on large-scale multi-modal data.

**2. What is LanceDB and why is it used in JamAI Base?**

LanceDB is an open-source vector database designed for AI workloads. It is used in JamAI Base to store, manage, query, and retrieve embeddings on large-scale multi-modal data, enabling powerful AI applications. LanceDB offers the functionality and scalability required to support advanced AI features.

**3. How does JamAI Base make LLM integration easier?**

JamAI Base abstracts the complexities of working directly with LLMs by providing a simple, **intuitive spreadsheet interface** for developers of all skill levels. It automates data generation and management, allowing users to focus on defining their data requirements through natural language prompts.

**4. What are Generative Tables, Action Tables, and Knowledge Tables?**

* **Generative Tables:** Transform static database tables into dynamic, AI-enhanced entities capable of generating and interacting with data autonomously.
* **Action Tables:** Create columns for user prompts, automatically generating API endpoints that interpret and respond to queries, with LLM-generated results streamed directly to the frontend.
* **Knowledge Tables:** Serve as repositories for collections of documents, forming a multimodal context that can be integrated into applications, enhancing the structure of retrieval systems with generated metadata columns.

**5. How does JamAI Base handle data privacy and security?**

JamAI Base prioritizes the privacy and security of user data by implementing robust security measures to protect sensitive information.&#x20;

**6. Can JamAI Base be customized and extended?**

Yes, JamAI Base provides a REST API that allows developers to customize and extend the platform to suit their specific needs. Developers can define custom prompts, configure AI agents, and integrate additional data sources to enhance the generated content.

**7. What is the declarative paradigm in JamAI Base?**

The declarative paradigm in JamAI Base focuses on the "**what**" rather than the "**how**." Users define relationships and desired outcomes, and the platform handles the underlying computations. This approach simplifies interactions and makes complex data operations accessible to users with varying levels of technical expertise.

**8. How does JamAI Base compare to no-code platforms?**

No-code platforms often require users to build pipelines using drag-and-drop components to specify the **how**, which can be challenging to build and maintain. JamAI Base abstracts this complexity, allowing users to focus on defining **what** they want to achieve rather than how to achieve it, making complex data operations more accessible.

**9. Why use JamAI Base instead of LangChain or LlamaIndex?**

JamAI Base offers a streamlined, backend-as-a-service solution specifically designed for integrating Large Language Models (LLMs) into your applications. Unlike LangChain or LlamaIndex, which might require you to piece together various components and manage separate databases for chat history and data storage, JamAI Base provides a comprehensive suite of tools including Generative, Action, and Knowledge Tables, all seamlessly integrated with LanceDB for optimized data handling.

With JamAI Base, you don't need to worry about setting up and managing external databases or grappling with the complexities of integrating different libraries. Our declarative programming model simplifies the development process, making it easier and faster to build powerful, intelligent applications that leverage the full potential of LLMs.

If you're looking for a hassle-free, end-to-end solution for LLM integration that handles everything from data storage to model deployment, JamAI Base is the clear choice. Imagine having the ease of use of Firebase, with the RAG and vector database logic of LangChain and LlamaIndex handled for you.

<figure><img src="https://949944545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FRNnvWSkFcg9eUeFklENf%2Fuploads%2FqrKQD7yWwgI3uyloFs9g%2Fstack-vs-jamai.png?alt=media&amp;token=910d6e39-5570-4182-8b50-127f1dc5afde" alt=""><figcaption><p><strong>JamAI Base vs. LLM Orchestration Frameworks + VectorDB + Database</strong></p></figcaption></figure>

**10. Why use JamAI Base instead of other no-code LLM platforms?**

JamAI Base is a game-changer in the no-code LLM space. We ditch the clunky, drag-and-drop pipelines of traditional platforms in favor of a simple, spreadsheet-like interface. You focus on defining ***what*** you want, not ***how*** to get it. Just specify the prompt for each output, reference inputs from other columns, and let JamAI Base handle the rest.

This declarative approach is not just easier to use, it's also more powerful. You can build complex, intelligent applications without getting bogged down in the technical details. And because JamAI Base is designed for LLM development, you get access to all the latest advancements in natural language processing right out of the box.

**11. What kind of support and resources are available for developers?**

JamAI Base fosters a vibrant developer community, providing comprehensive documentation, tutorials and [Discord](https://discord.gg/FD5BH9xqKz) community to support developers in building AI-powered applications. The platform encourages collaboration, knowledge sharing, and the creation of reusable components and templates.

**12. How does JamAI Base ensure scalability and performance?**

Built on top of LanceDB, JamAI Base inherits its scalability and performance characteristics. The platform is designed to handle large-scale data generation and retrieval efficiently, ensuring optimal performance even as the dataset grows. It seamlessly scales to accommodate increasing demands, allowing developers to build applications that can handle high traffic and data-intensive workloads.

**13. What LLMs does JamAI Base support?**

JamAI Base integrates with state-of-the-art LLMs like OpenAI GPT-4 Turbo/GPT-4o, Anthropic Claude 3, Mistral AI Mixtral, Llama 3, and more. This seamless integration abstracts away the complexities of working directly with these models, enabling developers to leverage their capabilities easily.

**14. How can I get started with JamAI Base?**

To get started with JamAI Base, visit our GitHub repository for the open-source code, documentation, and tutorials. Join our [Discord](https://discord.gg/FD5BH9xqKz) community for support and collaboration with other developers.


