Files
growqr-backend/src/db/schema.ts
NinjasPyajamas 9ddbb4a8e5 feat: wire real service agents into chat with LLM tool dispatch + Rivet proxy fix (#3)
# Wire All 4 Microservice Agents Into Chat

Wires all 4 microservice-backed agents into the chat so the LLM can call real services and return session URLs.

---

## Changes

### New

* `src/routes/chat.ts`

  * Added a direct HTTP chat endpoint.
  * When the LLM calls:

    * `start_interview_session`
    * `analyze_resume`
    * `start_roleplay_session`
    * `compute_qscore`
  * The route executes real service probes and returns live session URLs.

---

### Fixed

* `src/index.ts`

  * Rivet proxy now forwards requests to the engine at `localhost:6420`
    instead of using `registry.handler()`.
  * Prevents the:

    ```txt
    Runtime already started as runner
    ```

    conflict.

* `src/actors/user-actor.ts`

  * `receiveMessage()` now returns:

    ```ts
    {
      reply,
      sessions: []
    }
    ```
  * Includes per-module session URLs in responses.

* `docker-compose.yml`

  * Fixed:

    * Gitea health check port
    * Port mapping
    * `A2A_ALLOWED_KEY` default value

* `src/config.ts`

  * Added:

    ```ts
    resumeServiceUrl
    ```
  * Configured to use port `8002`.

---

### Rewritten

* `prompts/system.txt`

  * Reworked into a conversational step-by-step flow.
  * Added explicit rule:

    > CALL THE TOOL IMMEDIATELY

---

### Updated

* `agents/*.md` (6 files)

  * Updated:

    * Domain descriptions
    * Trigger phrases
    * Agent boundaries

---

## Verified

| Agent         | Service                  | Result                      |
| ------------- | ------------------------ | --------------------------- |
| Resume (Mira) | `resume-builder:8002`    | Real analysis               |
| Sara          | `interview-service:8007` | Real Gemini session + URL   |
| Emily         | `roleplay-service:8008`  | Real roleplay session + URL |
| Quinn         | `qscore-service:8000`    | Real Q-Score (~84)          |

---

## Outcome

The chat system can now:

* Trigger real backend agent services directly from LLM tool calls
* Return live session URLs
* Maintain structured multi-agent responses
* Avoid Rivet runtime conflicts
* Support end-to-end conversational workflows across all 4 agents

Reviewed-on: puter/growqr-backend#3
Co-authored-by: NinjasPyajamas <divyansh242805@gmail.com>
Co-committed-by: NinjasPyajamas <divyansh242805@gmail.com>
2026-06-01 09:26:19 +00:00

175 lines
5.3 KiB
TypeScript

import { sql } from "drizzle-orm";
import {
pgTable,
text,
timestamp,
integer,
jsonb,
uniqueIndex,
index,
primaryKey,
} from "drizzle-orm/pg-core";
// Users are mirrored from Clerk on first sign-in.
// id = Clerk user id (e.g., "user_2abc..."), email is the canonical Clerk email.
export const users = pgTable(
"users",
{
id: text("id").primaryKey(),
email: text("email").notNull(),
displayName: text("display_name"),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(t) => ({
emailIdx: uniqueIndex("users_email_idx").on(t.email),
}),
);
// One per user. Tracks the user's unified agent's container stack + Git repo.
// Per changes.md §2A: per-user Gitea containers removed; central Gitea shared.
// Per changes.md §5: ONE actor per user manages the full orchestration layer.
export const userStacks = pgTable(
"user_stacks",
{
userId: text("user_id")
.primaryKey()
.references(() => users.id, { onDelete: "cascade" }),
status: text("status", {
enum: ["provisioning", "running", "stopped", "error"],
})
.notNull()
.default("provisioning"),
// Central Gitea (shared org-wide, changes.md §2A).
giteaRepoName: text("gitea_repo_name"),
giteaRepoOwner: text("gitea_repo_owner"),
// Per-user OpenCode container (from shared image, changes.md §3).
opencodeContainerId: text("opencode_container_id"),
opencodeContainerName: text("opencode_container_name"),
opencodeHost: text("opencode_host"),
opencodePort: integer("opencode_port"),
opencodePassword: text("opencode_password"),
workspacePath: text("workspace_path"),
// Version tracking for image rollouts (changes.md §9).
imageVersion: text("image_version"),
migrationVersion: text("migration_version"),
promptVersion: text("prompt_version"),
lastError: text("last_error"),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(t) => ({
statusIdx: index("user_stacks_status_idx").on(t.status),
}),
);
// Per changes.md §5: ONE unified actor per user (no separate grow/sub actors).
// The actor manages: infra state, git state, runtime comms, migrations, API orchestration.
export const actors = pgTable(
"actors",
{
actorId: text("actor_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
kind: text("kind", { enum: ["user"] }).notNull().default("user"),
status: text("status", {
enum: ["idle", "running", "done", "error"],
})
.notNull()
.default("idle"),
lastActivityAt: timestamp("last_activity_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(t) => ({
pk: primaryKey({ columns: [t.userId, t.actorId] }),
kindIdx: index("actors_user_kind_idx").on(t.userId, t.kind),
}),
);
// Per-user repo registry (in addition to the primary memory repo).
export const repos = pgTable(
"repos",
{
id: text("id").primaryKey(), // `${userId}:${name}`
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
name: text("name").notNull(),
role: text("role", { enum: ["memory", "project"] }).notNull(),
giteaOwner: text("gitea_owner").notNull(),
giteaName: text("gitea_name").notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(t) => ({
userRoleIdx: index("repos_user_role_idx").on(t.userId, t.role),
}),
);
// OpenCode sessions opened by sub-agents.
export const opencodeSessions = pgTable(
"opencode_sessions",
{
id: text("id").primaryKey(), // OpenCode session id from POST /session
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
actorId: text("actor_id"),
title: text("title"),
parentId: text("parent_id"),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(t) => ({
userIdx: index("opencode_sessions_user_idx").on(t.userId),
}),
);
// Audit/event log — small append-only stream used for debugging + the
// frontend's "task progress timeline" until we move it to Rivet streams only.
export const events = pgTable(
"events",
{
id: text("id")
.primaryKey()
.default(sql`gen_random_uuid()::text`),
userId: text("user_id").notNull(),
actorId: text("actor_id"),
type: text("type").notNull(),
payload: jsonb("payload").$type<Record<string, unknown>>(),
createdAt: timestamp("created_at", { withTimezone: true })
.defaultNow()
.notNull(),
},
(t) => ({
userIdx: index("events_user_idx").on(t.userId, t.createdAt),
}),
);
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type UserStack = typeof userStacks.$inferSelect;
export type NewUserStack = typeof userStacks.$inferInsert;
export type ActorRow = typeof actors.$inferSelect;
export type RepoRow = typeof repos.$inferSelect;
export type OpencodeSessionRow = typeof opencodeSessions.$inferSelect;