# AIJSX

AIJSX is a TypeScript framework for building LLM applications using familiar JSX patterns. It provides a declarative approach to AI prompting, streaming, and structured output handling while maintaining type safety and composability.

## Quick Start

```typescript
import { renderText, AnthropicChatCompletion } from '@gammatech/aijsx'

const result = await renderText(
  <AnthropicChatCompletion model="claude-3-sonnet-20240229">
    <SystemMessage>You are a helpful AI assistant.</SystemMessage>
    <UserMessage>Explain quantum computing in one sentence.</UserMessage>
  </AnthropicChatCompletion>
)
```

## Core Concepts

**Components**

The simplest form of a component is a function that returns a renderable value:

```typescript
const Hello = (props: { name: string }) => `Hello ${props.name}!`
```

Components can be composed by creating JSX elements:

```typescript
import { AINode, renderText } from '@gammatech/aijsx'

const MarkdownTitle = (props: {
  level: 1 | 2 | 3 | 4 | 5 | 6
  children: AINode
}) => {
  return (
    <>
      {'#'.repeat(props.level)}
      {props.children}
    </>
  )
}

const Message = () => (
  <MarkdownTitle level={2}>
    <Greeting name="John" />
  </MarkdownTitle>
)

// ## Hello John!
const result = await renderText(<Message />)
```

If you are familar with React, this component syntax will feel extremely natural.

### Async and Streamable components

Because AIJSX's main purpose is to interact with LLMs, components can perform asynchronous operations, unlike `react` which uses entirely syncronous rendering.

**Components can return promises**

```typescript
const OpenAIPromise = async (props: {
  model: string
  userMessage: string
}): Promise<string> => {
  const client = new OpenAIClient()

  const response = await client.chat.completions.create({
    model: props.model,
    messages: [{ role: 'user', content: props.userMessage }],
  })

  return response.choices[0].message.content
}

const result = await renderText(
  <OpenAIPromise model="gpt-4o-mini" userMessage="tell me a joke" />
)
```

**Or `AsyncGenerators<string>`**

```typescript
import { OpenAI } from 'openai'
import { streamText } from '@gammatech/aijsx'

async function* OpenAIAsyncGenerator(props: {
  model: string
  userMessage: string
}): Promise<string> => {
  const client = new OpenAI()

  const response = await client.chat.completions.create({
    model: props.model,
    messages: [{ role: 'user', content: props.userMessage }]
    stream: true
  })

  for await (const message of chatResponse) {
    yield message.choices[0].delta
  }
}

// streamed
const stream = streamText(
  <OpenAIAsyncGenerator model="gpt-4o-mini" userMessage="tell me a joke" />
)
// or accumulated as a promise
const result = await renderText(
  <OpenAIAsyncGenerator model="gpt-4o-mini" userMessage="tell me a joke" />
)
// when possible, components should be async generators since they can support both streaming and accumulating
```

Above is just an example, AIJSX provides ChatCompletion components for `openai`, `anthropic`, and `google` out of the box.

- `<AnthropicChatCompletion>`
- `<GoogleChatCompletion>`
- `<OpenAIChatCompletion>`

The asynchronous render abstraction is powerful, allowing chaining of results from asynchronous operations (such as an LLM API call) to the input of another component.

## Rendering APIs

Similar to ReactDOM components, AIJSX components are lazy and their render functions are not run until needed. AIJSX provides three functions to render Components into types that can be used in imperative js code.
AIJSX provides three fundamental ways to interact with LLMs:

### renderText

Renders components to a complete string:

```typescript
const StoryComponent = ({ topic }: { topic: string }) => (
  <AnthropicChatCompletion model="claude-3-sonnet-20240229">
    <SystemMessage>You are a creative storyteller.</SystemMessage>
    <UserMessage>Write a short story about {topic}.</UserMessage>
  </AnthropicChatCompletion>
)

const story = await renderText(<StoryComponent topic="time travel" />)
```

### streamText

Streams component output incrementally:

```typescript
const stream = streamText(<StoryComponent topic="space exploration" />)
for await (const chunk of stream) {
  process.stdout.write(chunk)
}
```

### renderObject

Ensure structured output from LLMs:

```typescript
const SentimentAnalysisSchema = z.object({
  sentiment: z.enum(['positive', 'negative', 'neutral']),
  confidence: z.number(),
})

// type-safe based on zod schema
const analysis = await renderObject(OpenAIStructuredOutput, {
  model: 'gpt-4o' as const,
  schema: SentimentAnalysisSchema,
  children: (
    <>
      <SystemMessage>
        Analyze the sentiment of the text and respond in JSON format with
        fields: sentiment (positive/negative/neutral) and confidence (0-1).
      </SystemMessage>
      <UserMessage>
        <StoryComponent topic="Elle Woods first day at Harvard" />
      </UserMessage>
    </>
  ),
})
```

## Type System

AIJSX's type system is built around the concept of `Renderable`, which represents anything that can be rendered as output:

```typescript
type Renderable =
  | AINode // Component nodes
  | PromiseLike<Renderable> // Async values
  | AsyncGenerator<string, void> // Streams
  | Literal // Strings, numbers, etc.
```

Components can take two forms:

```typescript
// Text-producing components
type AIComponent<P> = (props: P) => Renderable

// Object-producing components
type AIObjectComponent<P, O extends PlainObject> = (props: P) => MaybePromise<O>
```

## Prompts

Prompts in AIJSX wrap a component with all you need to develop, debug and productionize your LLM calls by:

- Defining a schema for input and output (optional), allowing runtime validation of the prompt input variables and automatic type generation for the client.
- Attaching llm and non llm based evaluators to a component.
- Opinionated and built-in observability and tracing

```typescript
import { createPrompt } from '@gammatech/aijsx'
import { z } from 'zod'

const SummaryComponent: AIComponent<{ article: string, maxLength: number }> = ({ article, maxLength }) => (
  <AnthropicChatCompletion model="claude-3-sonnet-20240229">
    <SystemMessage>
      Summarize articles and extract key topics.
      Respond in JSON format with summary and keywords fields.
    </SystemMessage>
    <UserMessage>
      Summarize this article in {maxLength} words: {article}
    </UserMessage>
  </AnthropicChatCompletion>
)

const summarizeArticle = createPrompt({
  key: 'summarize-article',

  // Input validation schema
  schema: z.object({
    article: z.string(),
    maxLength: z.number()
  }),

  // The prompt implementation
  component: SummaryComponent

  // Optional evaluation functions
  evaluators: [
    async (variables, output) => ({
      key: 'length',
      passes: output.split(' ').length <= variables.maxLength,
      result: output.split(' ').length
    })
  ]
})

// Usage
const result = await renderPrompt(summarizeArticle, {
  article: "Long article text...",
  maxLength: 100
})

// by defualt prompt evaluators are run and put on an `ai.eval` span child of `ai.prompt`

// you can also run them yourself
const evaluatorResults = await runPromptEvaluators({
  prompt: summarizeArticle,
  variables: {
    article: "Long article text...",
    maxLength: 100
  },
  output: result
})
```

### Serving prompts with NestJS

```typescript
import { Injectable } from '@nestjs/common'
import { renderPrompt } from '@gammatech/aijsx'

@Injectable()
export class PromptController {
  private prompts: Record<string, Prompt> = {
    [summarizeArticle.key]: summarizeArticle,
  }

  @Post('/ai/jsx/render-prompt')
  async renderPrompt(
    @Body() body: { promptKey: string; variables: Record<string, any> }
  ): Promise<{ result: string }> {
    const prompt = this.prompts[body.promptKey]
    if (!prompt) {
      throw new Error(`Prompt not found: ${body.promptKey}`)
    }
    // this validates the variables based on the prompt schema
    const result = await renderPrompt(prompt, body.variables, {
      validateInput: true,
    })
    return { result }
  }
}
```

## Control Flow Components

AIJSX leverages declarative composition to encapsulate complex control flow patterns. By modeling retry logic, fallbacks, and tracing as components, we gain powerful abstractions that maintain code clarity while handling sophisticated execution flows.

### Retry

Implements exponential backoff retry logic with configurable parameters. Particularly useful for handling rate limits and transient API failures:

```typescript
const ReliableCompletion = () => (
  <Retry maxRetries={3} shouldRetry={(error) => error.status === 429}>
    <AnthropicChatCompletion model="claude-3-sonnet-20240229">
      <UserMessage>Generate a complex analysis</UserMessage>
    </AnthropicChatCompletion>
  </Retry>
)

// Compose with other control flow components
const RobustPipeline = () => (
  <Trace name="analysis.pipeline">
    <Retry maxRetries={3}>
      <Fallback fallbackFn={handleFailure}>
        <ReliableCompletion />
      </Fallback>
    </Retry>
  </Trace>
)
```

### Fallback

Provides elegant degradation paths for component failures. Enables graceful error handling while maintaining the declarative flow:

```typescript
const SmartAnalyzer = ({ text }: { text: string }) => (
  <Fallback
    fallbackFn={async (error) => {
      if (error.status === 429) {
        return <SimpleBackupAnalysis text={text} />
      }
      return `Analysis failed: ${error.message}`
    }}
  >
    <AnthropicChatCompletion model="claude-3-sonnet-20240229">
      <UserMessage>Analyze: {text}</UserMessage>
    </AnthropicChatCompletion>
  </Fallback>
)
```

### Trace

Enables granular observability with structured span management. Automatically captures timing, errors, and custom attributes:

```typescript
const AnalysisPipeline = ({ document }: { document: string }) => (
  <Trace
    name="document.analysis"
    attributes={{ documentLength: document.length }}
  >
    <Retry maxRetries={2}>
      <>
        {/* Extraction phase */}
        <Trace name="analysis.extraction">
          <KeyPointExtractor text={document} />
        </Trace>

        {/* Synthesis phase */}
        <Trace name="analysis.synthesis">
          <SentimentAnalyzer text={document} />
        </Trace>
      </>
    </Retry>
  </Trace>
)
```

### TimeToFirstTokenTimeout

Implements timeout on time to first token received from stream. Useful
to fail quickly and retry on a different deployment when a provider is degraded and experiencing very high latency.

```typescript
// Compose with other control flow components
const RobustPipeline = () => (
  <Retry maxRetries={3}>
    <Trace name="reliableCompletion">
      <TimeToFirstTokenTimeout timeoutMs={5000}>
        <ReliableCompletion />
      </TimeToFirstTokenTimeout>
    </Trace>
  </Retry>
)
```

## Advanced Patterns

Combine control components to create sophisticated execution patterns while maintaining code clarity:

```typescript
const EnterpriseProcessor = ({ data }: { data: string }) => (
  <Trace name="enterprise.process" attributes={{ dataSize: data.length }}>
    <Fallback
      fallbackFn={async (error, retryCount) => {
        // Progressive fallback strategy
        if (retryCount < 2) {
          return <BackupProcessor data={data} />
        }
        if (retryCount < 4) {
          return <EmergencyProcessor data={data} />
        }
        throw new Error(
          `Processing failed after ${retryCount} retries: ${error.message}`
        )
      }}
    >
      <Accumulate enabled={true}>
        <Retry
          maxRetries={5}
          shouldRetry={(error) => {
            // Sophisticated retry logic
            if (error.status === 429) return true
            if (error.message.includes('timeout')) return true
            return false
          }}
        >
          <ParallelProcessor chunks={chunkData(data)} />
        </Retry>
      </Accumulate>
    </Fallback>
  </Trace>
)
```

The composition model enables:

- **Hierarchical error handling**: Errors bubble up through the component tree until handled
- **Contextual retries**: Retry logic that's aware of its position in the processing pipeline
- **Granular tracing**: Nested spans that provide detailed execution insights
- **Progressive degradation**: Multiple fallback strategies based on failure context
- **Automatic cleanup**: Proper span closure and resource cleanup through the component lifecycle

These abstractions significantly reduce the cognitive overhead of implementing robust error handling and observability, while maintaining the declarative benefits of JSX composition.

## Updating AIJSX

Steps to publish an update:

1. Bump version in package.json
2. Update CHANGELOG.md
3. Run tests: `yarn test`
4. `npm publish --tag dev` (Must be part of Gamma NPM org to run)
