TechLead
Lesson 20 of 25
6 min read
AI-Native Engineering

Building Internal AI Tools and Agents

Learn when and how to build custom AI tools for your team — from Slack bots that answer codebase questions to domain-specific code generators

When to Build Custom AI Tooling

Off-the-shelf AI tools like Claude Code and Cursor handle most engineering tasks well. But every organization has domain-specific needs that generic tools cannot address: querying proprietary databases, generating code that follows internal frameworks, or answering questions about internal documentation. When you find your team doing the same AI-assisted task repeatedly, it is time to build a custom tool.

Build Custom When... Use Off-the-Shelf When...
You repeat the same AI workflow 5+ times per weekThe task is general-purpose (write tests, review code)
The task requires domain knowledge not in public dataThe task only needs public language/framework knowledge
You need to enforce specific output format/structureFree-form output is acceptable
Multiple team members need the same AI-powered workflowOnly one person needs it occasionally

Building a Codebase Q&A Slack Bot

One of the most impactful internal tools: a Slack bot that answers questions about your codebase. New engineers ask "Where is the payment logic?" and get an instant answer instead of waiting for a colleague.

// internal-tools/slack-codebase-bot/src/index.ts
import Anthropic from "@anthropic-ai/sdk";
import { App } from "@slack/bolt";
import { readFileSync, readdirSync, statSync } from "fs";
import { join } from "path";

const anthropic = new Anthropic();
const CODEBASE_PATH = process.env.CODEBASE_PATH || "/app/src";

// Index the codebase (simplified — use vector DB for large codebases)
function indexCodebase(dir: string, files: Map<string, string> = new Map()): Map<string, string> {
  const entries = readdirSync(dir);
  for (const entry of entries) {
    const fullPath = join(dir, entry);
    const stat = statSync(fullPath);
    if (stat.isDirectory() && !entry.startsWith(".") && entry !== "node_modules") {
      indexCodebase(fullPath, files);
    } else if (stat.isFile() && /.(ts|tsx|js|jsx)$/.test(entry)) {
      const content = readFileSync(fullPath, "utf-8");
      files.set(fullPath.replace(CODEBASE_PATH, ""), content);
    }
  }
  return files;
}

const codeIndex = indexCodebase(CODEBASE_PATH);

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
});

app.message(async ({ message, say }) => {
  if (message.type !== "message" || !("text" in message)) return;
  const question = message.text;

  // Find relevant files (simplified — use embeddings for better search)
  const relevantFiles = findRelevantFiles(question, codeIndex);

  const context = relevantFiles
    .map(([path, content]) => `--- ${path} ---\n${content}`)
    .join("\n\n");

  const response = await anthropic.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 1024,
    system: `You are a helpful codebase assistant. Answer questions about the codebase
based on the provided code files. Be specific — reference file paths and function names.
If you are not sure, say so.`,
    messages: [{
      role: "user",
      content: `Question: ${question}\n\nRelevant code:\n${context}`,
    }],
  });

  const answer = response.content[0].type === "text" ? response.content[0].text : "Could not generate answer.";
  await say(answer);
});

function findRelevantFiles(question: string, index: Map<string, string>): [string, string][] {
  const keywords = question.toLowerCase().split(/\s+/);
  const scored = [...index.entries()].map(([path, content]) => {
    const score = keywords.reduce((acc, kw) =>
      acc + (path.toLowerCase().includes(kw) ? 10 : 0) +
      (content.toLowerCase().includes(kw) ? 1 : 0), 0);
    return { path, content, score };
  });
  return scored
    .sort((a, b) => b.score - a.score)
    .slice(0, 5)
    .map(({ path, content }) => [path, content]);
}

(async () => {
  await app.start(process.env.PORT || 3000);
  console.log("Codebase bot is running");
})();

Building a Domain-Specific Code Generator

// internal-tools/api-generator/src/generate.ts
import Anthropic from "@anthropic-ai/sdk";
import { writeFileSync, mkdirSync } from "fs";

const anthropic = new Anthropic();

interface APIEndpointSpec {
  name: string;           // e.g., "createOrder"
  method: "GET" | "POST" | "PUT" | "DELETE";
  path: string;           // e.g., "/api/orders"
  description: string;
  requestBody?: string;   // TypeScript type as string
  responseBody: string;   // TypeScript type as string
}

async function generateEndpoint(spec: APIEndpointSpec): Promise<void> {
  const prompt = `Generate a Next.js API route handler with these specs:
- Name: ${spec.name}
- Method: ${spec.method}
- Path: ${spec.path}
- Description: ${spec.description}
${spec.requestBody ? `- Request Body Type: ${spec.requestBody}` : ""}
- Response Body Type: ${spec.responseBody}

Follow these conventions (our internal standards):
- Use Zod for request validation
- Use our auth middleware: import { withAuth } from "@/lib/auth"
- Use our database client: import { db } from "@/lib/db"
- Use our error handler: import { AppError } from "@/lib/errors"
- Return JSON with { success: boolean, data?: T, error?: string }
- Include proper error handling for 400, 401, 404, 500
- Generate the route handler file AND a test file

Return two code blocks: one for the route and one for the test.`;

  const response = await anthropic.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 4096,
    messages: [{ role: "user", content: prompt }],
  });

  const text = response.content[0].type === "text" ? response.content[0].text : "";
  const codeBlocks = text.match(/```typescript([\s\S]*?)```/g) || [];

  if (codeBlocks.length >= 2) {
    const routeCode = codeBlocks[0].replace(/```typescript|\n```/g, "");
    const testCode = codeBlocks[1].replace(/```typescript|\n```/g, "");

    const routePath = `app/${spec.path}/route.ts`;
    const testPath = `app/${spec.path}/route.test.ts`;

    mkdirSync(`app/${spec.path}`, { recursive: true });
    writeFileSync(routePath, routeCode.trim());
    writeFileSync(testPath, testCode.trim());

    console.log(`Generated: ${routePath}`);
    console.log(`Generated: ${testPath}`);
  }
}

// Usage: generate multiple endpoints from a spec file
const endpoints: APIEndpointSpec[] = [
  {
    name: "createOrder",
    method: "POST",
    path: "api/orders",
    description: "Create a new order with line items",
    requestBody: "{ items: { productId: string; quantity: number }[]; shippingAddress: Address }",
    responseBody: "{ orderId: string; total: number; status: string }",
  },
];

endpoints.forEach(generateEndpoint);

RAG for Internal Documentation

# Build a simple RAG system for internal docs:

# 1. Index your documentation
# Use a vector database (Pinecone, Weaviate, Chroma, or pgvector)
# Chunk your docs into ~500 token segments
# Generate embeddings with an embedding model
# Store in the vector database

# 2. At query time:
# Embed the user's question
# Find the top 5 most similar document chunks
# Send them as context to Claude with the question
# Return Claude's answer with source references

# 3. Keep the index updated
# Set up a CI job that re-indexes docs on every merge to main
# This ensures the AI always has current documentation

Start Small, Iterate

Do not try to build a sophisticated AI platform on day one. Start with the simplest useful tool: a script that calls the Claude API with a hardcoded prompt and your codebase context. If it saves time, iterate. Add better search, a Slack integration, or a web UI. The best internal tools grow organically from solving real pain points — not from grand architectural visions.

Summary

Building internal AI tools is the next level of AI-native engineering. Start when you notice your team repeating the same AI-assisted workflow multiple times per week. Build the simplest version first — often just a script calling the Claude API with domain-specific context. The highest-impact tools are codebase Q&A bots, domain-specific code generators, and RAG systems for internal documentation. Build for your team's specific needs, not for general-purpose AI capabilities.

Continue Learning