npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@devoven/assistant

v0.0.1

Published

Assistant module for NestJS — hexagonal architecture

Downloads

134

Readme

@devoven/assistant

AI assistant module for NestJS. Streaming chat with tool calling support.

Installation

npm install @devoven/assistant
# or
pnpm add @devoven/assistant

Peer Dependencies

Standard NestJS peer dependencies (@nestjs/common, @nestjs/core, rxjs, reflect-metadata, class-validator, class-transformer) must be present in your project. Any NestJS application already has these.

The default provider is GeminiAssistantService, which requires @google/genai. Install it if you plan to use the built-in Gemini backend:

npm install @google/genai
# or
pnpm add @google/genai

@google/genai is an optional peer dependency — you can skip it if you bring your own AssistantProviderPort implementation.

Quick Start

import { AssistantModule } from '@devoven/assistant';

@Module({
  imports: [
    AssistantModule.register({
      assistantApiKey: process.env.GEMINI_API_KEY,
      assistantModel: 'gemini-2.0-flash',
    }),
  ],
})
export class AppModule {}

This registers the module with the built-in Gemini provider, an in-memory conversation store, and the ChatController mounted at /chat.

Module Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | assistantApiKey | string | — | Required. API key for the underlying AI provider | | assistantModel | string | — | Required. Model identifier (e.g. "gemini-2.0-flash") | | assistantMaxTokens | number | 4096 | Maximum output tokens per generation | | systemPrompt | string | null | System instruction prepended to every conversation | | maxToolTurns | number | 10 | Maximum tool-call cycles per request before throwing | | provider | Class or instance of AssistantProviderPort | GeminiAssistantService | Swap in a custom AI backend | | conversationRepository | Class or instance of ConversationRepositoryPort | InMemoryConversationRepository | Persistence adapter for conversations | | toolProviders | (Type<IToolProvider> \| IToolProvider)[] | [] | Tool providers to register with the tool context | | controllers | boolean | true | Set to false to omit ChatController and use the ports directly | | global | boolean | false | Register the module globally |

Async Registration

Use registerAsync when options come from a config service or need to be resolved asynchronously:

import { AssistantModule } from '@devoven/assistant';
import { ConfigService } from '@nestjs/config';

@Module({
  imports: [
    AssistantModule.registerAsync({
      useFactory: (config: ConfigService) => ({
        assistantApiKey: config.get('GEMINI_API_KEY'),
        assistantModel: config.get('GEMINI_MODEL'),
        systemPrompt: config.get('ASSISTANT_SYSTEM_PROMPT'),
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

When using registerAsync, pass provider and conversationRepository as instances (not classes). If you need a class provider, declare it in your own module and resolve it with useFactory.

REST API

The ChatController is mounted at /chat. All endpoints use a conversationId path parameter to identify a conversation session.

Open an SSE stream

GET /chat/:conversationId/stream

Opens a Server-Sent Events stream for the given conversation. Subscribe to this before sending any messages. If no stream exists yet for the ID, one is created automatically.

Response: SSE stream. Each event carries a data field with a JSON payload:

| Event type | Payload shape | Description | |------------|---------------|-------------| | text_chunk | { type: "text_chunk", content: string } | Incremental text from the model | | tool_call | { type: "tool_call", name: string, args: object } | A tool the model is invoking | | tool_result | { type: "tool_result", name: string, result: unknown } | The result returned to the model | | done | { type: "done", stopReason: string } | Stream completed | | error | { type: "error", message: string } | An error occurred during generation |

Send a message

POST /chat/:conversationId

Sends a user message to the conversation. The response streams back through the SSE connection opened above. You must open the stream first — this endpoint returns 404 if there is no active stream for the conversation.

Request body:

{
  "message": "What is the capital of France?"
}

Response: 204 No Content

Errors: 404 Not Found if no active SSE stream exists for conversationId.

Typical usage sequence

  1. GET /chat/my-conversation-id/stream — open the SSE stream and listen for events
  2. POST /chat/my-conversation-id — send the first message
  3. Receive text_chunk events as the model responds, followed by done
  4. Repeat from step 2 for follow-up messages

Architecture

The module follows hexagonal architecture with three port categories.

Port / Token Mapping

| Token | Interface | Direction | Description | |-------|-----------|-----------|-------------| | ASSISTANT_TOKENS.StreamChatPort | StreamChatPort | Driving | Entry point — executes a streaming chat turn | | ASSISTANT_TOKENS.AssistantProviderPort | AssistantProviderPort | Driven | Calls the underlying AI API | | ASSISTANT_TOKENS.ConversationRepositoryPort | ConversationRepositoryPort | Driven | Loads and persists conversation history | | ASSISTANT_TOKENS.IToolContext | IToolContext | Internal | Registry of all available tools at runtime |

All four tokens are exported from the module and can be injected into other services.

Tool system

Tools are provided by implementing IToolProvider:

import { IToolProvider, IExecutableTool } from '@devoven/assistant';
import { Injectable } from '@nestjs/common';

@Injectable()
export class WeatherToolProvider implements IToolProvider {
  getListTools(): IExecutableTool[] {
    return [
      {
        name: 'get_weather',
        description: 'Get current weather for a city',
        parameters: {
          type: 'object',
          properties: {
            city: { type: 'string', description: 'City name' },
          },
          required: ['city'],
        },
        async handler(args: unknown) {
          const { city } = args as { city: string };
          // fetch weather...
          return { temperature: 22, condition: 'sunny' };
        },
      },
    ];
  }
}

Register providers when importing the module:

AssistantModule.register({
  assistantApiKey: process.env.GEMINI_API_KEY,
  assistantModel: 'gemini-2.0-flash',
  toolProviders: [WeatherToolProvider],
})

Pass class references for providers that need their own injected dependencies. Pass instances directly if they have no dependencies.

Domain model

Conversation — aggregate root for a chat session.

| Method | Description | |--------|-------------| | Conversation.create() | Create a new empty conversation with a generated UUID | | Conversation.reconstitute(data) | Rebuild from persistence | | addMessage(msg) | Append an AssistantMessage | | getMessages() | Return all messages as an array | | lastMessage | Last message in the conversation, or null |

AssistantMessage — immutable value object representing a single message.

| Method | Description | |--------|-------------| | AssistantMessage.create({ role, content }) | Create with role "user" or "assistant" |

Custom Adapters

Custom AI provider

Implement AssistantProviderPort to connect a different model backend:

import { AssistantProviderPort, AssistantMessage, AssistantResponse } from '@devoven/assistant';
import { Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { MessageEvent } from '@nestjs/common';
import { IExecutableTool } from '@devoven/assistant';

@Injectable()
export class OpenAIAssistantService implements AssistantProviderPort {
  async chat(
    messages: AssistantMessage[],
    tools: Record<string, IExecutableTool>,
  ): Promise<AssistantResponse> {
    // call OpenAI...
  }

  streamChat(
    messages: AssistantMessage[],
    tools: Record<string, IExecutableTool>,
  ): Observable<MessageEvent> {
    // stream from OpenAI...
  }
}

Pass the instance via the provider option:

AssistantModule.register({
  assistantApiKey: 'unused',
  assistantModel: 'gpt-4o',
  provider: new OpenAIAssistantService(openAiClient),
})

Custom conversation repository

Implement ConversationRepositoryPort to persist conversations in a database:

import { ConversationRepositoryPort, Conversation } from '@devoven/assistant';
import { Injectable } from '@nestjs/common';

@Injectable()
export class PrismaConversationRepository implements ConversationRepositoryPort {
  constructor(private readonly prisma: PrismaService) {}

  async findById(id: string): Promise<Conversation | null> { /* ... */ }
  async save(conversation: Conversation): Promise<void> { /* ... */ }
}

Pass the instance via conversationRepository:

AssistantModule.register({
  assistantApiKey: process.env.GEMINI_API_KEY,
  assistantModel: 'gemini-2.0-flash',
  conversationRepository: new PrismaConversationRepository(prisma),
})

Using ports directly (without the HTTP controller)

Set controllers: false and inject the exported ports into your own services:

import { Inject, Injectable } from '@nestjs/common';
import { ASSISTANT_TOKENS, StreamChatPort } from '@devoven/assistant';

@Injectable()
export class MyService {
  constructor(
    @Inject(ASSISTANT_TOKENS.StreamChatPort)
    private readonly streamChat: StreamChatPort,
  ) {}
}