شيفرة جاهزة للاستخدام

اربط وكيل تحليل البيانات بشيفرتك

شيفرة جاهزة للنسخ واللصق تربط وكيل تحليل البيانات بمستودعك على GitHub أو بمجلد على حاسوبك. قبل وصول أي شيء إلى نموذج الذكاء الاصطناعي، يستبدل مرشح الخصوصية كلمات المرور والمفاتيح وعناوين البريد الإلكتروني وأرقام الهواتف وغيرها من التفاصيل الخاصة بعناصر نائبة، ثم يعيد القيم الحقيقية إلى الإجابة لديك.

يقرأ: sql/, migrations/, models/, schema.prisma, dbt_project.yml
يعمل عند كل طلب سحب (أو يدويًا) وينشر الإجابة بعد إزالة البيانات الحساسة منها في تعليق
أي نموذج متوافق مع OpenAI

1 أضف الملفات

أنشئ مجلد agent/ في مستودعك واحفظ فيه الملفين privacy-filter.ts وagent-github.ts.

2 أضف مفتاح الذكاء الاصطناعي الخاص بك

في GitHub: Settings → Secrets and variables → Actions → أضف AI_API_KEY. اضبط AI_BASE_URL وAI_MODEL لاستخدام أي مزوّد متوافق مع OpenAI.

3 فعّل GitHub Action

احفظ سير العمل باسم .github/workflows/data-analyst-agent.yml. يعمل عند كل طلب سحب (أو يدويًا) وينشر الإجابة بعد إزالة البيانات الحساسة منها في تعليق.

agent/agent-github.ts

يقرأ مستودعك على GitHub، ويخفي البيانات الخاصة، ويستفسر من النموذج، ثم يحفظ الإجابة أو ينشرها.

/**
 * Data analyst agent: reads your GitHub repository, scrubs secrets and personal data, then asks the AI model.
 *
 *   npm install @octokit/rest tsx
 *   GITHUB_TOKEN=… GITHUB_REPOSITORY=owner/repo AI_API_KEY=… npx tsx agent/agent-github.ts
 *
 * Optional: AI_BASE_URL and AI_MODEL (any OpenAI-compatible API), PR_NUMBER or ISSUE_NUMBER
 * (the GitHub Action sets these for you).
 */
import { writeFileSync } from "node:fs";
import { Octokit } from "@octokit/rest";
import { isSensitivePath, restore, scrub, type Vault } from "./privacy-filter";

const AGENT = {
  "label": "Data analyst agent",
  "system": "You are a data analyst agent. Your job: explain data models and write SQL for analytics questions. Text like [EMAIL_1] or [API_KEY_2] is a placeholder for private data that was removed before you saw it. Keep placeholders exactly as written and never guess the real values.",
  "task": "Describe the main tables and write a SQL query for weekly active users.",
  "include": [
    "sql/",
    "migrations/",
    "models/",
    "schema.prisma",
    "dbt_project.yml"
  ],
  "baseUrl": "https://api.openai.com/v1",
  "model": "gpt-5",
  "keyFallback": "",
  "autoPost": true,
  "audit": false
};

const MAX_CHARS = 60_000;
const BINARY = /\.(png|jpe?g|gif|webp|ico|pdf|zip|gz|mp4|mp3|woff2?|lock)$/i;

function env(key: string, fallback?: string): string {
  const value = process.env[key] || fallback;
  if (!value) throw new Error(`Missing environment variable ${key}`);
  return value;
}

const [owner, repo] = env("GITHUB_REPOSITORY").split("/") as [string, string];
const gh = new Octokit({ auth: env("GITHUB_TOKEN") });
const vault: Vault = new Map(); // placeholder → real value; never leaves this machine

const wanted = (path: string) =>
  !isSensitivePath(path) && !BINARY.test(path) && AGENT.include.some((p) => path === p || path.startsWith(p));

async function readFile(path: string): Promise<string> {
  const { data } = await gh.repos.getContent({ owner, repo, path });
  if (Array.isArray(data) || data.type !== "file" || !("content" in data)) return "";
  return Buffer.from(data.content, "base64").toString("utf8");
}

async function collectContext(): Promise<string> {
  const parts: string[] = [];
  const size = () => parts.reduce((n, p) => n + p.length, 0);

  if (process.env.PR_NUMBER) {
    const files = await gh.paginate(gh.pulls.listFiles, { owner, repo, pull_number: Number(process.env.PR_NUMBER), per_page: 100 });
    for (const f of files) if (!isSensitivePath(f.filename) && f.patch) parts.push(`### Changed: ${f.filename}\n${f.patch}`);
  }
  if (process.env.ISSUE_NUMBER) {
    const { data } = await gh.issues.get({ owner, repo, issue_number: Number(process.env.ISSUE_NUMBER) });
    parts.push(`### Issue #${data.number}: ${data.title}\n${data.body ?? ""}`);
  }

  const { data: info } = await gh.repos.get({ owner, repo });
  const { data: tree } = await gh.git.getTree({ owner, repo, tree_sha: info.default_branch, recursive: "true" });
  for (const item of tree.tree) {
    if (size() > MAX_CHARS) break;
    if (item.type !== "blob" || !item.path || (item.size ?? 0) > 200_000 || !wanted(item.path)) continue;
    parts.push(`### ${item.path}\n${await readFile(item.path)}`);
  }
  return parts.join("\n\n").slice(0, MAX_CHARS);
}

async function askModel(context: string) {
  const safe = scrub(context, { vault }); // the model only ever sees safe.text
  const res = await fetch(`${env("AI_BASE_URL", AGENT.baseUrl)}/chat/completions`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${env("AI_API_KEY", AGENT.keyFallback)}` },
    body: JSON.stringify({
      model: env("AI_MODEL", AGENT.model),
      messages: [
        { role: "system", content: AGENT.system },
        { role: "user", content: `${AGENT.task}\n\n${safe.text}` },
      ],
    }),
  });
  if (!res.ok) throw new Error(`AI model error ${res.status}: ${await res.text()}`);
  const json = (await res.json()) as { choices: { message: { content: string } }[] };
  return { reply: json.choices[0]?.message.content ?? "", findings: safe.findings };
}

async function main() {
  const { reply, findings } = await askModel(await collectContext());
  console.log("Privacy filter hid:", findings.map((f) => `${f.count} × ${f.label}`).join(", ") || "nothing");

  writeFileSync("agent-report.md", restore(reply, vault)); // real values, for you only
  writeFileSync("agent-report.safe.md", reply); // placeholders, safe to share
  if (AGENT.audit) writeFileSync("privacy-audit.json", JSON.stringify({ at: new Date().toISOString(), findings }, null, 2));

  const target = Number(process.env.PR_NUMBER || process.env.ISSUE_NUMBER || 0);
  if (target && AGENT.autoPost) {
    // Post the scrubbed version so private values never end up in a public comment.
    await gh.issues.createComment({ owner, repo, issue_number: target, body: reply });
    console.log(`Commented on #${target}`);
  } else {
    console.log("Saved agent-report.md. A person should review it before anything is sent.");
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

شاهد ما سيراه وكيل تحليل البيانات

الصق كودًا أو ملف إعدادات أو رسالة عميل. الجانب الأيسر يعرض ما لديك، والجانب الأيمن يعرض كل ما سيراه نموذج الذكاء الاصطناعي.

يعمل في متصفحك دون رفع أي شيء
ما يراه الذكاء الاصطناعي
// config.ts
export const db = "[CONNECTION_STRING_1]";
const apiKey = "[API_KEY_1]";
STRIPE_SECRET_KEY=[API_KEY_2]

// Ticket from [EMAIL_1], phone [PHONE_1]
تم إخفاء 5 من العناصر الخاصة:
1 × رابط محمي بكلمة مرور
2 × مفتاح API
1 × عنوان البريد الإلكتروني
1 × رقم الهاتف

شيفرة لوكلاء ذكاء اصطناعي آخرين