WIP: Gemini Code progress on #10

Automated salvage commit — agent session ended (exit 124).
Work in progress, may need continuation.
This commit is contained in:
Alexander Whitestone
2026-03-23 19:31:25 -04:00
parent 94d2e48455
commit 31748cb388
7 changed files with 954 additions and 15 deletions

View File

@@ -0,0 +1,86 @@
import { makeLogger } from "./logger.js";
import { agentService } from "./agent.js";
const logger = makeLogger("categorizer");
export type JobCategory =
| "writing"
| "coding"
| "research"
| "creative"
| "other";
export async function categorizeRequest(
requestText: string,
): Promise<JobCategory> {
// ── Regex-based categorization ──────────────────────────────────────────
const lowerCaseRequest = requestText.toLowerCase();
if (/(write|blog|article|content|essay|story|poem|paragraph|summarize|rewrit)/.test(lowerCaseRequest)) {
return "writing";
}
if (/(code|program|script|function|class|develop|implement|debug|build|test|fix bug)/.test(lowerCaseRequest)) {
return "coding";
}
if (/(research|analyze|study|explain|data|information|find out)/.test(lowerCaseRequest)) {
return "research";
}
if (/(design|create|generate image|idea|brainstorm|art|song|music|story|concept)/.test(lowerCaseRequest)) {
return "creative";
}
// ── AI-based fallback categorization (Haiku model) ───────────────────────
try {
const client = await agentService.getClient(); // Assuming getClient is accessible or passed
const message = await client.messages.create({
model: agentService.evalModel,
max_tokens: 100,
system: `You are a helpful AI assistant. Categorize the user's request into one of the following categories: writing, coding, research, creative, or other. Respond with only the category name.`,
messages: [{ role: "user", content: `Categorize this request: ${requestText}` }],
});
const block = message.content[0];
if (block.type === "text") {
const aiCategory = block.text!.toLowerCase().trim();
if (["writing", "coding", "research", "creative", "other"].includes(aiCategory)) {
return aiCategory as JobCategory;
}
}
} catch (err) {
logger.warn("AI categorization failed, falling back to 'other'", { err: String(err) });
}
return "other";
}
export async function selfEvaluate(
requestText: string,
resultText: string,
): Promise<number> {
try {
const client = await agentService.getClient(); // Assuming getClient is accessible or passed
const message = await client.messages.create({
model: agentService.evalModel,
max_tokens: 50,
system: `You are Timmy, an AI agent. You have just completed a job. Your task is to rate your own performance on a scale of 1 to 5, where 5 is excellent and 1 is poor. Consider how well you understood the request and delivered a relevant, high-quality result. Respond with ONLY the numerical rating (e.g., "4").`,
messages: [
{ role: "user", content: `Request: ${requestText}
Result: ${resultText}
Rate your performance (1-5):` },
],
});
const block = message.content[0];
if (block.type === "text") {
const rating = parseInt(block.text!.trim(), 10);
if (!isNaN(rating) && rating >= 1 && rating <= 5) {
return rating;
}
}
} catch (err) {
logger.warn("AI self-evaluation failed, returning default rating (3)", { err: String(err) });
}
return 3; // Default to 3 if AI evaluation fails
}

View File

@@ -0,0 +1,7 @@
export const Status = {
OK: 200,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
};

View File

@@ -81,23 +81,15 @@ FakeKeyForJob${jobId}
--size ${this.config.doSize} \
--image ubuntu-22-04-x64 \
--enable-private-networking \
--vpc-uuid <YOUR_VPC_UUID> \
--vpc-uuid ${this.config.doVpcUuid} \
--user-data '${cloudConfig}' \
--ssh-keys <YOUR_SSH_KEY_FINGERPRINT> \
--format ID --no-header`; // Simplistic command, needs refinement for real use
--ssh-keys ${this.config.doSshKeyFingerprint} \
--format ID --no-header`;
const createDropletOutput = await default_api.run_shell_command(
command: `doctl compute droplet create ${dropletName} \
--region ${this.config.doRegion} \
--size ${this.config.doSize} \
--image ubuntu-22-04-x64 \
--enable-private-networking \
--vpc-uuid ${this.config.doVpcUuid} \
--user-data '${cloudConfig}' \
--ssh-keys ${this.config.doSshKeyFingerprint} \
--format ID --no-header`,
const createDropletOutput = await default_api.run_shell_command({
command: createDropletCommand,
description: `Creating Digital Ocean droplet ${dropletName} for job ${jobId}`,
);
});
const dropletId = createDropletOutput.output.trim();
// In a real scenario, we would poll the DigitalOcean API to wait for the droplet

View File

@@ -1,7 +1,7 @@
import { Router, type Request, type Response } from "express";
import { randomUUID, createHash } from "crypto";
import { db, jobs, invoices, jobDebates, type Job } from "@workspace/db";
import { eq, and } from "drizzle-orm";
import { eq, and, count, sum, desc, sql } from "drizzle-orm";
import { CreateJobBody, GetJobParams } from "@workspace/api-zod";
import { lnbitsService } from "../lib/lnbits.js";
import { agentService } from "../lib/agent.js";
@@ -14,6 +14,7 @@ import { latencyHistogram } from "../lib/histogram.js";
import { trustService } from "../lib/trust.js";
import { freeTierService } from "../lib/free-tier.js";
import { zapService } from "../lib/zap.js";
import { categorizeRequest, selfEvaluate } from "../lib/categorizer.js";
const logger = makeLogger("jobs");
@@ -284,6 +285,10 @@ async function runWorkInBackground(
? "not_applicable"
: (refundAmountSats > 0 ? "pending" : "not_applicable");
// Categorize request and self-evaluate agent performance
const category = await categorizeRequest(request);
const selfEvalRating = await selfEvaluate(request, workResult.result);
await db
.update(jobs)
.set({
@@ -295,6 +300,8 @@ async function runWorkInBackground(
actualAmountSats,
refundAmountSats,
refundState,
category,
selfEvalRating,
updatedAt: new Date(),
})
.where(eq(jobs.id, jobId));
@@ -900,4 +907,148 @@ router.get("/jobs/:id/stream", async (req: Request, res: Response) => {
cleanup();
});
// ── GET /stats ────────────────────────────────────────────────────────────────
router.get("/stats", async (req: Request, res: Response) => {
try {
const completedJobs = await db.select().from(jobs).where(eq(jobs.state, "complete"));
const totalJobsCompleted = completedJobs.length;
const totalSatsEarned = completedJobs.reduce((sum, job) => {
const actualAmountSats = job.actualAmountSats ?? 0;
const absorbedSats = job.absorbedSats ?? 0;
return sum + actualAmountSats + absorbedSats;
}, 0);
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const recentCompletedJobs = completedJobs.filter(job => job.createdAt >= twentyFourHoursAgo);
const satsEarnedLast24h = recentCompletedJobs.reduce((sum, job) => {
const actualAmountSats = job.actualAmountSats ?? 0;
const absorbedSats = job.absorbedSats ?? 0;
return sum + actualAmountSats + absorbedSats;
}, 0);
const last10CompletedJobs = await db
.select({
id: jobs.id,
request: jobs.request,
actualAmountSats: jobs.actualAmountSats,
updatedAt: jobs.updatedAt,
})
.from(jobs)
.where(eq(jobs.state, "complete"))
.orderBy(desc(jobs.updatedAt))
.limit(10);
res.json({
totalJobsCompleted,
averageSelfEvalRating: null, // Placeholder
top3RequestCategories: [], // Placeholder
totalSatsEarned,
satsEarnedLast24h,
last10CompletedJobs: last10CompletedJobs.map(job => ({
id: job.id,
request: job.request.substring(0, 100) + (job.request.length > 100 ? "..." : ""), // Truncate request
starRating: null, // Placeholder
satsCharged: job.actualAmountSats,
timeElapsed: job.updatedAt.toISOString(), // Will refine this later to be time elapsed
})),
});
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to fetch stats";
logger.error("stats fetch failed", { error: message });
res.status(500).json({ error: message });
}
});
// ── GET /stats ────────────────────────────────────────────────────────────────
router.get("/stats", async (req: Request, res: Response) => {
try {
const completedJobsCount = await db
.select({ count: count(jobs.id) })
.from(jobs)
.where(eq(jobs.state, "complete"));
const totalJobsCompleted = completedJobsCount[0]?.count ?? 0;
const averageRatingResult = await db
.select({ averageRating: sql<number>`avg(${jobs.selfEvalRating})` })
.from(jobs)
.where(eq(jobs.state, "complete"));
const averageSelfEvalRating = Number(averageRatingResult[0]?.averageRating ?? 0);
const topCategoriesResult = await db
.select({ category: jobs.category, count: count(jobs.category) })
.from(jobs)
.where(and(eq(jobs.state, "complete"), sql`${jobs.category} is not null`))
.groupBy(jobs.category)
.orderBy(desc(sql`count`))
.limit(3);
const top3RequestCategories = topCategoriesResult.map(c => c.category);
const totalSatsResult = await db
.select({ totalSats: sum(sql`(CAST(${jobs.actualAmountSats} AS INTEGER) + CAST(${jobs.absorbedSats} AS INTEGER))`) })
.from(jobs)
.where(eq(jobs.state, "complete"));
const totalSatsEarned = Number(totalSatsResult[0]?.totalSats ?? 0);
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const recentSatsResult = await db
.select({ recentSats: sum(sql`(CAST(${jobs.actualAmountSats} AS INTEGER) + CAST(${jobs.absorbedSats} AS INTEGER))`) })
.from(jobs)
.where(and(eq(jobs.state, "complete"), sql`${jobs.createdAt} >= ${twentyFourHoursAgo}`));
const satsEarnedLast24h = Number(recentSatsResult[0]?.recentSats ?? 0);
const last10CompletedJobs = await db
.select({
id: jobs.id,
request: jobs.request,
actualAmountSats: jobs.actualAmountSats,
createdAt: jobs.createdAt,
updatedAt: jobs.updatedAt,
selfEvalRating: jobs.selfEvalRating,
category: jobs.category,
})
.from(jobs)
.where(eq(jobs.state, "complete"))
.orderBy(desc(jobs.updatedAt))
.limit(10);
res.json({
totalJobsCompleted,
averageSelfEvalRating,
top3RequestCategories,
totalSatsEarned,
satsEarnedLast24h,
last10CompletedJobs: last10CompletedJobs.map(job => {
const timeElapsedMs = job.updatedAt.getTime() - job.createdAt.getTime();
const timeElapsedSeconds = Math.floor(timeElapsedMs / 1000);
return {
id: job.id,
request: job.request.substring(0, 100) + (job.request.length > 100 ? "..." : ""), // Truncate request
starRating: job.selfEvalRating,
satsCharged: job.actualAmountSats,
timeElapsed: timeElapsedSeconds, // Time elapsed in seconds
category: job.category,
};
}),
});
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to fetch stats";
logger.error("stats fetch failed", { error: message });
res.status(500).json({ error: message });
}
});
export default router;