diff --git a/.commandcode/settings.json b/.commandcode/settings.json new file mode 100644 index 0000000..60cf4b3 --- /dev/null +++ b/.commandcode/settings.json @@ -0,0 +1,16 @@ +{ + "permissions": { + "allow": [ + "Shell(npm:*)", + "Shell(python -m py_compile memory/api/main.py)", + "Shell(node --check index.js)", + "Shell(node \"%COMMANDCODE_SCRATCHPAD%\\format-check.mjs\")", + "Shell(python -c import sys; sys.path.insert(0, 'memory/api'); from main import result_text, result_field; r = {'kind': 'graph_completion', 'text': 'The PR adds a bot.\\n\\n| File | Change |\\n|---|---|\\n| app.yml | events |', 'dataset_name': 'repo-inline-arc-AgentwaspAi', 'raw': {'value': 'fallback'}, 'source': 'graph'}; print(repr(result_text(r))); print(result_field(r, 'dataset_name')); print(repr(result_text({'raw': {'value': 'fallback works'}}))) 2 >& 1)", + "Shell(findstr:*)", + "Shell(python -c \" def result_field(r, key, default=''): if isinstance(r, dict): return r.get(key, default) or default return getattr(r, key, default) or default def result_text(r): text = result_field(r, 'text') if text: return text raw = result_field(r, 'raw') if isinstance(raw, dict) and raw.get('value'): return raw['value'] return str(r) blob = {'kind': 'graph_completion', 'search_type': 'GRAPH_COMPLETION', 'text': 'The PR adds a bot.\\n\\n| File | Change |\\n|------|--------|\\n| app.yml | events |', 'score': None, 'dataset_name': 'repo-inline-arc-AgentwaspAi', 'raw': {'value': 'x'}, 'source': 'graph'} print(result_text(blob)) print('---') print(result_field(blob, 'dataset_name')) print('--- no-text fallback:', result_text({'raw': {'value': 'from raw'}})) print('--- old behavior would have been:', len(str(blob)), 'chars of repr blob') \")", + "Shell(python \"%COMMANDCODE_SCRATCHPAD%\\extract-check.py\")" + ], + "deny": [], + "defaultMode": "default" + } +} \ No newline at end of file diff --git a/.commandcode/taste/taste.md b/.commandcode/taste/taste.md new file mode 100644 index 0000000..cf02165 --- /dev/null +++ b/.commandcode/taste/taste.md @@ -0,0 +1,7 @@ +# Taste Learnings + +## Output & display quality +- Bot output posted to GitHub (e.g. PR comments) must be clean, properly structured markdown that renders readably — never raw Python dict reprs, JSON blobs, or minified rich text. Confidence: 0.75 + +## Workflow +- When reporting a bug, expects the assistant to trace and identify the root cause ("check where the problem is"), not just patch surface symptoms. Confidence: 0.6 diff --git a/app.yml b/app.yml index e5418ba..d1974ae 100644 --- a/app.yml +++ b/app.yml @@ -19,11 +19,11 @@ default_events: # - create # - delete - deployment - # - deployment_status + - deployment_status # - fork # - gollum - # - issue_comment - # - issues + - issue_comment + - issues # - label # - milestone # - member @@ -36,8 +36,10 @@ default_events: # - project_column # - public - pull_request -# - pull_request_review -# - pull_request_review_comment + - pull_request_review + - pull_request_review_comment + - discussion # added this + - discussion_comment # added this # - push # - release # - repository @@ -70,7 +72,7 @@ default_permissions: # Issues and related comments, assignees, labels, and milestones. # https://developer.github.com/v3/apps/permissions/#permission-on-issues - # issues: write + issues: write # Search repositories, list collaborators, and access repository '. # https://developer.github.com/v3/apps/permissions/#metadata-permissions @@ -123,6 +125,11 @@ default_permissions: # Get notified of, and update, content references. # https://developer.github.com/v3/apps/permissions/ # organization_administration: read + + # Discussions and related comments. + # https://developer.github.com/v3/apps/permissions/#permission-on-discussions + discussions: write + # The name of the GitHub App. Defaults to the name specified in package.json # name: My Probot App diff --git a/memory/cognee.py b/deploy.yml similarity index 100% rename from memory/cognee.py rename to deploy.yml diff --git a/index.js b/index.js index f52187c..89a3f22 100644 --- a/index.js +++ b/index.js @@ -1,56 +1,212 @@ -// Deployments API example -// See: https://developer.github.com/v3/repos/deployments/ to learn more +import fetch from "node-fetch"; + +const BOT_NAME = "@agentwaspai"; +const COGNEE_API = process.env.COGNEE_SERVICE_URL || "http://localhost:8001"; + +const MAX_SECTION_CHARS = 2000; +const MAX_COMMENT_CHARS = 60000; // GitHub hard limit is 65536 + +// GitHub renders raw JSON as an unreadable blob — fence it as code instead +function renderResultText(text) { + let out = (text || "").trim(); + + if (/^[[{]/.test(out)) { + try { + out = JSON.stringify(JSON.parse(out), null, 2); + } catch { /* not valid JSON — fence it anyway */ } + out = `\`\`\`json\n${out}\n\`\`\``; + } + + if (out.length > MAX_SECTION_CHARS) { + out = `${out.slice(0, MAX_SECTION_CHARS)}\n\n_…(truncated)_`; + } + + return out; +} + +function formatRecallReply(question, results) { + if (!results.length) { + return "Nothing found in memory. Try `/review` first."; + } + + const sections = results.map((r, i) => { + const source = r.dataset_name ? ` — _from \`${r.dataset_name}\`_` : ""; + return `**${i + 1}.**${source}\n\n${renderResultText(r.text)}`; + }); + + const reply = [ + "## AgentWasp AI — Memory Recall", + "", + `> **Question:** ${question}`, + "", + sections.join("\n\n---\n\n"), + ].join("\n"); + + return reply.length > MAX_COMMENT_CHARS + ? `${reply.slice(0, MAX_COMMENT_CHARS)}\n\n_…(truncated)_` + : reply; +} -/** - * This is the main entrypoint to your Probot app - * @param {import('probot').Probot} app - */ export default (app) => { - // Your code here - app.log.info("Yay, the app was loaded!"); - app.on( - ["pull_request.opened", "pull_request.synchronize"], - async (context) => { - // Creates a deployment on a pull request event - // Then sets the deployment status to success - // NOTE: this example doesn't actually integrate with a cloud - // provider to deploy your app, it just demos the basic API usage. - app.log.info(context.payload); - - // Probot API note: context.repo() => { username: 'hiimbex', repo: 'testing-things' } - const res = await context.octokit.rest.repos.createDeployment( - context.repo({ - ref: context.payload.pull_request.head.ref, // The ref to deploy. This can be a branch, tag, or SHA. - task: "deploy", // Specifies a task to execute (e.g., deploy or deploy:migrations). - auto_merge: true, // Attempts to automatically merge the default branch into the requested ref, if it is behind the default branch. - required_contexts: [], // The status contexts to verify against commit status checks. If this parameter is omitted, then all unique contexts will be verified before a deployment is created. To bypass checking entirely pass an empty array. Defaults to all unique contexts. - payload: { - schema: "rocks!", - }, // JSON payload with extra information about the deployment. Default: "" - environment: "production", // Name for the target deployment environment (e.g., production, staging, qa) - description: "My Probot App's first deploy!", // Short description of the deployment - transient_environment: false, // Specifies if the given environment is specific to the deployment and will no longer exist at some point in the future. - production_environment: true, // Specifies if the given environment is one that end-users directly interact with. - }), - ); - - const deploymentId = res.data.id; - await context.octokit.rest.repos.createDeploymentStatus( - context.repo({ - deployment_id: deploymentId, - state: "success", // The state of the status. Can be one of error, failure, inactive, pending, or success - log_url: "https://example.com", // The log URL to associate with this status. This URL should contain output to keep the user updated while the task is running or serve as historical information for what happened in the deployment. - description: "My Probot App set a deployment status!", // A short description of the status. - environment_url: "https://example.com", // Sets the URL for accessing your environment. - auto_inactive: true, // Adds a new inactive status to all prior non-transient, non-production environment deployments with the same repository and environment name as the created status's deployment. An inactive status is only added to deployments that had a success state. - }), - ); - }, - ); - - // For more information on building apps: - // https://probot.github.io/docs/ - - // To get your app running against GitHub, see: - // https://probot.github.io/docs/development/ -}; + app.log.info("AgentWasp AI loaded."); + + app.on("issue_comment.created", async (context) => { + if (context.payload.comment.user?.type === "Bot") return; + + const body = context.payload.comment.body.trim(); + const number = context.payload.issue.number; + const repo = context.payload.repository.full_name; + + if (!body.toLowerCase().includes(BOT_NAME.toLowerCase())) return; + + // ── /review ─────────────────────────────────────────────── + if (body.toLowerCase().includes("/review")) { + if (!context.payload.issue.pull_request) { + await context.octokit.rest.issues.createComment( + context.issue({ body: "This command only works on pull requests." }) + ); + return; + } + + try { + const [ + { data: diff }, + { data: files }, + { data: comments }, + { data: reviewComments }, + { data: reviews }, + { data: commits }, + ] = await Promise.all([ + context.octokit.rest.pulls.get({ + ...context.repo(), + pull_number: number, + mediaType: { format: "diff" }, + }), + context.octokit.rest.pulls.listFiles({ + ...context.repo(), + pull_number: number, + }), + context.octokit.rest.issues.listComments({ + ...context.repo(), + issue_number: number, + }), + context.octokit.rest.pulls.listReviewComments({ + ...context.repo(), + pull_number: number, + }), + context.octokit.rest.pulls.listReviews({ + ...context.repo(), + pull_number: number, + }), + context.octokit.rest.pulls.listCommits({ + ...context.repo(), + pull_number: number, + }), + ]); + + app.log.info({ diff, files, comments, reviewComments, reviews, commits }); + + // post raw summary to PR + const summary = ` +**Raw PR Data** +- Files changed: ${files.length} +- Commits: ${commits.length} +- Comments: ${comments.length} +- Review comments: ${reviewComments.length} +- Reviews: ${reviews.length} + +**Files** +${files.map(f => `- \`${f.filename}\` [${f.status}] +${f.additions} -${f.deletions}`).join("\n")} + +**Commits** +${commits.map(c => `- \`${c.sha.slice(0, 7)}\` ${c.commit.message} (${c.commit.author.name})`).join("\n")} + +**Diff preview** +\`\`\`diff +${diff.slice(0, 1000)} +\`\`\` + `.trim(); + + await context.octokit.rest.issues.createComment( + context.issue({ body: summary }) + ); + + // ingest to Cognee + const ingestRes = await fetch(`${COGNEE_API}/ingest`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + repo, + pr_number: number, + diff, + files, + commits, + comments, + review_comments: reviewComments, + reviews, + }), + }); + + if (!ingestRes.ok) throw new Error(`Ingest failed: ${ingestRes.status}`); + + const ingestResult = await ingestRes.json(); + app.log.info(`Ingest response: ${JSON.stringify(ingestResult)}`); + + await context.octokit.rest.issues.createComment( + context.issue({ + body: `PR #${number} data stored in memory and ready for review.`, + }) + ); + + } catch (err) { + app.log.error(`Review failed: ${err.message}`); + await context.octokit.rest.issues.createComment( + context.issue({ body: `Error: ${err.message}` }) + ); + } + return; + } + + // ── /ask ────────────────────────────────────────────────── + if (body.toLowerCase().includes("/ask")) { + const question = body + .replace(new RegExp(BOT_NAME, "gi"), "") + .replace(/\/ask/i, "") + .trim(); + + if (!question || question.length < 3) { + await context.octokit.rest.issues.createComment( + context.issue({ body: "Please provide a question after `/ask`." }) + ); + return; + } + + try { + const recallRes = await fetch(`${COGNEE_API}/recall`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: question, + repo, + pr_number: number, + }), + }); + + if (!recallRes.ok) throw new Error(`Recall failed: ${recallRes.status}`); + + const { results } = await recallRes.json(); + + await context.octokit.rest.issues.createComment( + context.issue({ body: formatRecallReply(question, results) }) + ); + + } catch (err) { + app.log.error(`Ask failed: ${err.message}`); + await context.octokit.rest.issues.createComment( + context.issue({ body: `Error: ${err.message}` }) + ); + } + return; + } + }); +}; \ No newline at end of file diff --git a/intro.js b/intro.js new file mode 100644 index 0000000..febd5ec --- /dev/null +++ b/intro.js @@ -0,0 +1,15 @@ +export const INTRO_MESSAGE = `**AgentWasp AI** ( AI code reviewer with persistent memory ) + +- Automatically reviews every PR using your codebase history +- Remembers past bugs, patterns and review feedback over time +- Answers questions about your code and diffs & updates + +**Commands:** +| Command | Description | +|---|---| +| \`/ask \` | Ask anything about this PR or codebase | +| \`/review\` | Trigger a manual review | +| \`@agentwaspai \` | Mention me anywhere in a comment | + +> Built with [Cognee](https://cognee.ai) · [Contribute](https://github.com/inline-arc/AgentwaspAi) +`; \ No newline at end of file diff --git a/k8s/cognee-deployment.yml b/k8s/cognee-deployment.yml new file mode 100644 index 0000000..b560345 --- /dev/null +++ b/k8s/cognee-deployment.yml @@ -0,0 +1,115 @@ +# k8s/cognee-deployment.yaml +# Runs ONLY the Cognee memory service. +# Your FastAPI app lives in k8s/api-deployment.yaml + +--- +apiVersion: v1 +kind: Namespace +metadata: + name: agentwaspai + +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: cognee-data + namespace: agentwaspai +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cognee + namespace: agentwaspai + labels: + app: cognee +spec: + replicas: 1 + selector: + matchLabels: + app: cognee + template: + metadata: + labels: + app: cognee + spec: + containers: + - name: cognee + image: cognee/cognee:0.5.1 + imagePullPolicy: Always + ports: + - containerPort: 8000 + env: + - name: LLM_API_KEY + valueFrom: + secretKeyRef: + name: agentwaspai-secrets + key: LLM_API_KEY + - name: LLM_MODEL + valueFrom: + secretKeyRef: + name: agentwaspai-secrets + key: LLM_MODEL + - name: EMBEDDING_MODEL + valueFrom: + secretKeyRef: + name: agentwaspai-secrets + key: EMBEDDING_MODEL + - name: EMBEDDING_API_KEY + valueFrom: + secretKeyRef: + name: agentwaspai-secrets + key: EMBEDDING_API_KEY + - name: SYSTEM_ROOT_DIRECTORY + value: /data/cognee_system + - name: DATA_ROOT_DIRECTORY + value: /data/cognee_data + - name: TELEMETRY_DISABLED + value: "true" + volumeMounts: + - name: cognee-storage + mountPath: /data + resources: + requests: + memory: 512Mi + cpu: 250m + limits: + memory: 2Gi + cpu: 1000m + readinessProbe: + httpGet: + path: / + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: / + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 15 + volumes: + - name: cognee-storage + persistentVolumeClaim: + claimName: cognee-data + +--- +apiVersion: v1 +kind: Service +metadata: + name: cognee + namespace: agentwaspai +spec: + selector: + app: cognee + ports: + - protocol: TCP + port: 8000 + targetPort: 8000 + type: ClusterIP \ No newline at end of file diff --git a/memory/__pycache__/cognee.cpython-311.pyc b/memory/__pycache__/cognee.cpython-311.pyc new file mode 100644 index 0000000..3d07e82 Binary files /dev/null and b/memory/__pycache__/cognee.cpython-311.pyc differ diff --git a/memory/__pycache__/main.cpython-311.pyc b/memory/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..c2a6798 Binary files /dev/null and b/memory/__pycache__/main.cpython-311.pyc differ diff --git a/memory/api/__pycache__/main.cpython-311.pyc b/memory/api/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..495fd42 Binary files /dev/null and b/memory/api/__pycache__/main.cpython-311.pyc differ diff --git a/memory/api/main.py b/memory/api/main.py new file mode 100644 index 0000000..d919311 --- /dev/null +++ b/memory/api/main.py @@ -0,0 +1,216 @@ +import os +import cognee +import logging + +from contextlib import asynccontextmanager +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from dotenv import load_dotenv + +load_dotenv() + +logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO")) +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + logger.info("AgentWasp AI starting up") + yield + logger.info("AgentWasp AI shutting down") + + +app = FastAPI(title="AgentWasp AI", lifespan=lifespan) + + +# ── Models ──────────────────────────────────────────────────────── + +class PRDataRequest(BaseModel): + repo: str + pr_number: int + diff: str + files: list + commits: list + comments: list + review_comments: list + reviews: list + + +class RecallRequest(BaseModel): + query: str + repo: str = "" + pr_number: int = 0 + + +# ── Helper — connect to Cognee Cloud ───────────────────────────── + +async def connect(): + await cognee.serve( + url=os.getenv("COGNEE_API_URL"), + api_key=os.getenv("COGNEE_API_KEY"), + ) + logger.info("Connected to Cognee Cloud") + + +def serialize_pr_data(body: PRDataRequest) -> str: + """ + Build a structured markdown document from the raw GitHub PR payload. + + Cognee chunks and embeds this document, and /recall returns those chunks + verbatim. Storing clean markdown here means recalled chunks come back as + readable markdown instead of a minified JSON blob that GitHub comments + cannot render. + """ + lines = [ + f"# PR #{body.pr_number} — {body.repo}", + "", + "## Files Changed", + ] + + for f in body.files: + path = f.get("filename") or f.get("path") or "unknown" + lines.append( + f"- `{path}` [{f.get('status', '?')}] " + f"+{f.get('additions', 0)} -{f.get('deletions', 0)}" + ) + if not body.files: + lines.append("None") + + lines += ["", "## Commits"] + for c in body.commits: + sha = str(c.get("sha", ""))[:7] + commit = c.get("commit", {}) + message = (commit.get("message") or c.get("message") or "").splitlines()[0] + author = (commit.get("author") or {}).get("name") or c.get("author") or "" + lines.append(f"- `{sha}` {message} ({author})".strip()) + if not body.commits: + lines.append("None") + + lines += ["", "## Diff", "```diff", body.diff[:4000], "```"] + + lines += ["", "## Inline Review Comments"] + review_comments = [c for c in body.review_comments if (c.get("user") or {}).get("type") != "Bot"] + for c in review_comments: + author = (c.get("user") or {}).get("login", "unknown") + lines.append(f"- [{c.get('path', '?')} line {c.get('line', '?')}] {author}: {c.get('body', '')}") + if not review_comments: + lines.append("None") + + lines += ["", "## Discussion"] + comments = [c for c in body.comments if (c.get("user") or {}).get("type") != "Bot"] + for c in comments: + author = (c.get("user") or {}).get("login", "unknown") + lines.append(f"- {author}: {c.get('body', '')}") + if not comments: + lines.append("None") + + lines += ["", "## Formal Reviews"] + reviews = [r for r in body.reviews if (r.get("user") or {}).get("type") != "Bot" and r.get("body")] + for r in reviews: + author = (r.get("user") or {}).get("login", "unknown") + lines.append(f"- {author} [{r.get('state', '?')}]: {r.get('body', '')}") + if not reviews: + lines.append("None") + + return "\n".join(lines) + + +# ── Recall result helpers ──────────────────────────────────────── +# +# cognee.recall returns dict-like results: +# {'kind': ..., 'text': '...markdown...', 'raw': {'value': '...'}, ...} +# getattr(r, "text", str(r)) misses on dicts and falls back to str(r), +# which produces a Python-repr blob that GitHub comments can't render. + +def result_field(r, key, default=""): + if isinstance(r, dict): + return r.get(key, default) or default + return getattr(r, key, default) or default + + +def result_text(r) -> str: + text = result_field(r, "text") + if text: + return text + + raw = result_field(r, "raw") + if isinstance(raw, dict) and raw.get("value"): + return raw["value"] + + return str(r) + + +# ── Routes ──────────────────────────────────────────────────────── + +@app.get("/") +def root(): + return {"status": "ok", "service": "AgentWasp AI"} + + +@app.post("/ingest") +async def ingest(body: PRDataRequest): + try: + await connect() + + raw_document = serialize_pr_data(body) + + logger.info(f"Ingesting PR #{body.pr_number} from {body.repo}") + logger.info(f"Document preview:\n{raw_document[:300]}") + + await cognee.remember( + data=raw_document, + dataset_name=f"repo-{body.repo.replace('/', '-')}", + ) + + logger.info(f"PR #{body.pr_number} stored in Cognee Cloud") + + return { + "status": "ok", + "pr_number": body.pr_number, + "repo": body.repo, + } + + except Exception as e: + logger.error(f"Ingest failed: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/recall") +async def recall(body: RecallRequest): + try: + # connect every request + await connect() + + # search the dataset /ingest wrote to — repo-inline-arc-AgentwaspAi + # is the fallback for queries that don't name a repo + dataset = ( + f"repo-{body.repo.replace('/', '-')}" + if body.repo + else "repo-inline-arc-AgentwaspAi" + ) + + results = await cognee.recall( + query_text=body.query, + datasets=[dataset] + ) + + context = [ + { + "text": result_text(r), + "dataset_name": result_field(r, "dataset_name"), + "source": result_field(r, "source"), + } + for r in results + ] + + logger.info(f"Recall returned {len(context)} results for: {body.query}") + + return { + "status": "ok", + "query": body.query, + "results": context, + } + + except Exception as e: + logger.error(f"Recall failed: {e}") + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/memory/cloud.test.py b/memory/cloud.test.py new file mode 100644 index 0000000..e69de29 diff --git a/memory/config/__pycache__/rules.cpython-311.pyc b/memory/config/__pycache__/rules.cpython-311.pyc new file mode 100644 index 0000000..1e326ce Binary files /dev/null and b/memory/config/__pycache__/rules.cpython-311.pyc differ diff --git a/memory/config/rules.py b/memory/config/rules.py new file mode 100644 index 0000000..7e2e660 --- /dev/null +++ b/memory/config/rules.py @@ -0,0 +1,11 @@ +from pathlib import Path + +RULES_DIR = Path(__file__).parent.parent / "rules" + +def load_rules(filename: str) -> str: + return (RULES_DIR / filename).read_text(encoding="utf-8") + +ASK_RULES = load_rules("ask.txt"); +REVIEW_RULES = load_rules("review.txt"); +SYSTEM_RULES = load_rules("rules.txt"); +CLAUDE_RULES = load_rules("claude.md"); \ No newline at end of file diff --git a/memory/llm.py b/memory/llm.py new file mode 100644 index 0000000..e69de29 diff --git a/memory/main.py b/memory/main.py new file mode 100644 index 0000000..089772c --- /dev/null +++ b/memory/main.py @@ -0,0 +1,146 @@ +import asyncio +import os +import cognee + +from dotenv import load_dotenv +from pydantic import BaseModel +from cognee.infrastructure.llm.LLMGateway import LLMGateway +from config.rules import SYSTEM_RULES, REVIEW_RULES, ASK_RULES + +load_dotenv() + + +# ── Response models ─────────────────────────────────────────────── + +class ReviewResponse(BaseModel): + review: str + +class AskResponse(BaseModel): + answer: str + + +# ── /review agent ───────────────────────────────────────────────── +# +# memory_query_fixed — always searches for review rules + past PR patterns +# memory_system_prompt — tells the model how to use the retrieved memory +# save_traces=True — saves every review as memory so future reviews +# learn from what the bot said before +# agent_session_name — stable name so Cognee tracks this agent's history +# across calls in Cognee Cloud Connections view + +@cognee.agent_memory( + agent_session_name="agentwaspai-review", + dataset_name="agent_memory_dataset", + memory_query_fixed="code review rules standards past PR issues bugs", + memory_system_prompt=REVIEW_RULES, + #with_memory=True, + #save_traces=True, + memory_top_k=5, +) +async def review_agent(pr_context: str) -> ReviewResponse: + """ + Decorator prepends relevant memory (rules + past reviews) to the LLM call. + LLMGateway picks it up automatically — no extra code needed here. + pr_context is the full structured PR document built by Probot. + """ + return await LLMGateway.acreate_structured_output( + text_input=pr_context, + system_prompt=REVIEW_RULES, + response_model=ReviewResponse, + ) + + +# ── /ask agent ──────────────────────────────────────────────────── +# +# memory_query_from_method="question" +# — uses the actual question as the Cognee search query +# — so memory retrieval changes per question, not fixed +# with_session_memory=True +# — also pulls in recent Q&A traces from this session +# — so the bot remembers what it said earlier in the same conversation + +@cognee.agent_memory( + agent_session_name="agentwaspai-ask", + dataset_name="agent_memory_dataset", + memory_query_from_method="question", + memory_system_prompt=ASK_RULES, + #with_memory=True, + #with_session_memory=True, # remember what was asked earlier in this PR session + #save_traces=True, + memory_top_k=5, +) +async def ask_agent(question: str, pr_context: str) -> AskResponse: + """ + memory_query_from_method="question" means Cognee searches memory + using the question itself as the query — so if the user asks about + async handling, Cognee returns everything it knows about async issues + in this codebase. + pr_context is passed separately as the current PR data. + """ + return await LLMGateway.acreate_structured_output( + text_input=f"Question: {question}\n\nPR Context:\n{pr_context}", + system_prompt=ASK_RULES, + response_model=AskResponse, + ) + + +# ── Main setup — run once at startup ───────────────────────────── + +async def setup(): + """ + Connect to Cognee Cloud and load rules into memory. + Call this once when the FastAPI service starts. + """ + await cognee.serve( + url=os.getenv("COGNEE_API_URL"), + api_key=os.getenv("COGNEE_API_KEY"), + ) + print("Connected to Cognee Cloud") + + # load rules into memory — these become the base knowledge + # the decorator retrieves from on every call + await cognee.remember( + data=SYSTEM_RULES, + dataset_name="agent_memory_dataset", + ) + print("Rules loaded into memory") + + +# ── Test ────────────────────────────────────────────────────────── + +async def main(): + await setup() + + # test review agent + test_pr = """ + PR #5 — fix: add error handling to getUserById + Author: hiimbex + Files: src/db/users.js + + Diff: + - async function getUserById(id) { + - return await db.users.findUnique({ where: { id } }) + + async function getUserById(id) { + + try { + + return await db.users.findUnique({ where: { id } }) + + } catch (err) { + + throw new DatabaseError(err) + + } + """ + + print("\n--- Review Agent ---") + review = await review_agent(pr_context=test_pr) + print(review.review) + + print("\n--- Ask Agent ---") + answer = await ask_agent( + question="why does getUserById need error handling?", + pr_context=test_pr, + ) + print(answer.answer) + + await cognee.disconnect() + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/memory/rules/ask.txt b/memory/rules/ask.txt new file mode 100644 index 0000000..664fa88 --- /dev/null +++ b/memory/rules/ask.txt @@ -0,0 +1,18 @@ +# AgentWasp AI — Ask Rules + +## Answer Structure +- Answer the question directly in the first sentence +- Provide context or explanation after +- Keep answers under 200 words unless the question requires more +- Do not volunteer a full review when a specific question was asked + +## Code References +- Always use a code block when referencing code +- Always use the correct language tag +- Show the fix or example, not just a description of it + +## Context Handling +- Ground the answer in the actual diff when available +- If referencing past PRs from memory, mention the PR number +- If the question cannot be answered confidently, say so explicitly +- Do not guess or invent an answer \ No newline at end of file diff --git a/memory/rules/claude.md b/memory/rules/claude.md new file mode 100644 index 0000000..3181945 --- /dev/null +++ b/memory/rules/claude.md @@ -0,0 +1,73 @@ +# Cognee Cloud Memory Skill + +This skill connects Claude Code to Cognee Cloud for persistent knowledge graph memory. + +## Prerequisites +- Cognee Cloud account with API key +- Environment variables set: + - `COGNEE_BASE_URL` — your tenant API endpoint + - `COGNEE_API_KEY` — your API key + +**If these variables are not set**, ask the user to run the export commands from the Cognee Cloud console (Connect to Claude Code → Step 1). + +## ALWAYS ping Cognee Cloud first + +Before any other operation in the conversation, ping Cognee Cloud to confirm the env vars are valid and the tenant is reachable. If the ping fails (non-200, network error, or auth error), tell the user immediately and ask them to re-export the credentials from the Cognee Cloud console — do NOT proceed with remember/recall calls against a broken connection. + +```bash +curl -fsS -o /dev/null -w "%{http_code}" \ + "$COGNEE_BASE_URL/api/v1/datasets/" \ + -H "X-Api-Key: $COGNEE_API_KEY" +``` +A `200` means the connection works. A `401` means the API key is wrong; `404`/`5xx` means the tenant URL is wrong or the service is down. + +## Session ID — ALWAYS use one + +At the start of the conversation, generate ONE id (your agent name + a unix timestamp) and reuse it as `session_id` in every call. Sessions group your activity in the Cognee Cloud dashboard and are converted into long-term memory. The ONLY exception: when the user explicitly asks you to store something directly in the knowledge graph, call /remember without a session_id. + +## Operations + +### Remember — Store knowledge +When the user shares important information worth preserving: +```bash +# Default: store as a session entry — always include your session_id +curl -X POST $COGNEE_BASE_URL/api/v1/remember/entry \ + -H "X-Api-Key: $COGNEE_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"entry": {"type": "qa", "question": "", "answer": ""}, "dataset_name": "default_dataset", "session_id": ""}' +``` + +### Store directly in the knowledge graph (ONLY when explicitly asked) +Use this only when the user explicitly asks to store something in the graph / permanent memory. The data must be a FILE upload (inline text is rejected with 422) and must NOT include a session_id: +```bash +TMP=$(mktemp) && printf '%s' "" > "$TMP" +curl -X POST $COGNEE_BASE_URL/api/v1/remember \ + -H "X-Api-Key: $COGNEE_API_KEY" \ + -F "data=@$TMP;type=text/plain" \ + -F "datasetName=default_dataset" +rm -f "$TMP" +``` + +### Recall — Retrieve knowledge +Before answering questions, check if relevant knowledge exists: +```bash +curl -X POST $COGNEE_BASE_URL/api/v1/recall \ + -H "X-Api-Key: $COGNEE_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"query": "", "session_id": ""}' +``` + +For targeted retrieval, add "search_type" to the recall body — one of: GRAPH_COMPLETION (default), CHUNKS, GRAPH_SUMMARY_COMPLETION, HYBRID_COMPLETION. + +### List datasets +```bash +curl -s "$COGNEE_BASE_URL/api/v1/datasets/?session_id=" -H "X-Api-Key: $COGNEE_API_KEY" +``` + +## Behavior +1. At session start, verify COGNEE_BASE_URL and COGNEE_API_KEY are set; if not, ask the user to export them +2. **Ping Cognee Cloud** with the curl above before any other operation. If it doesn't return 200, surface the failure to the user and stop — do not proceed with broken credentials +3. Generate one session id for the conversation (your agent name + a unix timestamp) and pass it as `session_id` in every call — the session appears automatically in the Cognee Cloud dashboard under Sessions and is converted into long-term memory +4. Use recall before answering to check for relevant context +5. Use /remember/entry (with session_id) to store important information; only when the user explicitly asks to store directly in the graph, use /remember (file upload, no session_id) +6. Use default_dataset unless specified otherwi \ No newline at end of file diff --git a/memory/rules/review.txt b/memory/rules/review.txt new file mode 100644 index 0000000..3e9285f --- /dev/null +++ b/memory/rules/review.txt @@ -0,0 +1,38 @@ +# AgentWasp AI — Code Review Rules + +## Review Structure +Every review must follow this structure in this order: + +### Overview +Brief description of what this PR does and which files it touches. + +### Issues +List bugs, security risks, breaking changes, logic errors. +Each issue must include: +- What the problem is +- Why it is a problem +- How to fix it with a code example where relevant + +Use these severity prefixes: +- [Critical] — security risk, data loss, or breaking change +- [Major] — bug or logic error that affects functionality +- [Minor] — style or non-blocking improvement + +### Suggestions +Non-blocking improvements only. Mark each as non-blocking. + +### Positives +What was done well. One to three points maximum. + +### Summary +One paragraph verdict: Approved, Request Changes, or Needs Discussion. +If Request Changes, list exactly what must be resolved before merging. + +## Code Standards +- Flag hardcoded secrets or API keys immediately as Critical +- Check all async functions have proper error handling +- Flag missing input validation on functions accepting external data +- Flag N+1 query patterns in database or API calls +- Flag use of eval() or equivalent dangerous patterns +- Check new code is consistent with patterns already in the codebase +- Flag race conditions in concurrent or async code \ No newline at end of file diff --git a/memory/rules/rules.txt b/memory/rules/rules.txt new file mode 100644 index 0000000..4361ce7 --- /dev/null +++ b/memory/rules/rules.txt @@ -0,0 +1,53 @@ +# AgentWasp AI — Code Review Rules + +## Response Format +- Always respond in markdown +- Use ## headers to separate sections +- Use bullet points for lists of issues +- Use code blocks with correct language tags (js, python, tsx, etc.) +- Every review must end with a Summary section + +## Review Structure +Every code review must follow this exact structure: + +### Overview +Brief description of what this PR does and what files it touches. + +### Issues +List bugs, security risks, breaking changes, or logic errors. +For each issue include: +- What the problem is +- Why it is a problem +- How to fix it + +### Suggestions +Non-blocking improvements, performance notes, or best practices. + +### Positives +What was done well. Keep this brief and specific. + +### Summary +One paragraph verdict: approve, request changes, or needs discussion. +State clearly what must be resolved before merging. + +## Code Standards +- Flag hardcoded secrets, API keys, or credentials immediately +- Check all async functions have proper error handling +- Flag missing input validation on any function that accepts external data +- Flag N+1 query patterns in database calls +- Check that environment variables are used instead of hardcoded values +- Flag any direct use of eval() or dangerous equivalents +- Check that new functions are consistent with patterns already in the codebase + +## Response Tone +- Be direct and specific +- Always explain why something is an issue, not just that it is one +- Always suggest how to fix it +- Do not pad responses with filler or compliments +- Keep reviews concise — flag what matters, skip what does not + +## Language +- English only +- Use correct technical terminology +- No informal language +- No emojis \ No newline at end of file diff --git a/test/index.test.js b/test/index.test.js index 4860988..80157ca 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,40 +1,15 @@ import nock from "nock"; -// Requiring our app implementation import myProbotApp from "../index.js"; import { Probot, ProbotOctokit } from "probot"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -// Requiring our fixtures import payload from "./fixtures/pull_request.opened.json" with { type: "json" }; - import { describe, beforeEach, afterEach, test } from "node:test"; import assert from "node:assert"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const deployment = { - ref: "hiimbex-patch-1", - task: "deploy", - auto_merge: true, - required_contexts: [], - payload: { - schema: "rocks!", - }, - environment: "production", - description: "My Probot App's first deploy!", - transient_environment: false, - production_environment: true, -}; - -const deploymentStatus = { - state: "success", - log_url: "https://example.com", - description: "My Probot App set a deployment status!", - environment_url: "https://example.com", - auto_inactive: true, -}; - const privateKey = fs.readFileSync( path.join(__dirname, "fixtures/mock-cert.pem"), "utf-8", @@ -48,60 +23,86 @@ describe("My Probot app", () => { probot = new Probot({ appId: 123, privateKey, - // disable request throttling and retries for testing Octokit: ProbotOctokit.defaults((instanceOptions) => ({ ...instanceOptions, - retry: { enabled: false }, + retry: { enabled: false }, throttle: { enabled: false }, })), }); - // Load our app into probot probot.load(myProbotApp); }); - test("creates a deployment and a deployment status", async () => { + afterEach(() => { + nock.cleanAll(); + nock.enableNetConnect(); + }); + + + // ── Test 1 — PR opened, context fetched ────────────────────── + + test("fetches PR context when pull request is opened", async () => { const mock = nock("https://api.github.com") - // Test that we correctly return a test token .post("/app/installations/2/access_tokens") - .reply(200, { - token: "test", - permissions: { - deployments: "write", - pull_requests: "read", - }, - }) + .reply(200, { token: "test", permissions: { pull_requests: "read" } }) + + // diff + .get("/repos/hiimbex/testing-things/pulls/1") + .matchHeader("accept", "application/vnd.github.v3.diff") + .reply(200, "diff --git a/file.js b/file.js\n+new line") + + // files + .get("/repos/hiimbex/testing-things/pulls/1/files") + .reply(200, [{ filename: "file.js", status: "modified", additions: 1, deletions: 0, patch: "+new line" }]) + + // comments + .get("/repos/hiimbex/testing-things/issues/1/comments") + .reply(200, []) + + // review comments + .get("/repos/hiimbex/testing-things/pulls/1/comments") + .reply(200, []) + + // reviews + .get("/repos/hiimbex/testing-things/pulls/1/reviews") + .reply(200, []) + + // commits + .get("/repos/hiimbex/testing-things/pulls/1/commits") + .reply(200, [{ sha: "abc1234", commit: { message: "fix: test", author: { name: "hiimbex" } } }]); - // Test that a deployment is created - .post("/repos/hiimbex/testing-things/deployments", (body) => { - assert.deepStrictEqual(body, deployment); - return true; - }) - .reply(200, { id: 123 }) - - // Test that a deployment status is created - .post( - "/repos/hiimbex/testing-things/deployments/123/statuses", - (body) => { - assert.deepStrictEqual(body, deploymentStatus); - return true; - }, - ) - .reply(200); - - // Receive a webhook event await probot.receive({ name: "pull_request", payload }); assert.deepStrictEqual(mock.pendingMocks(), []); }); - afterEach(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); -}); -// For more information about testing with Jest see: -// https://facebook.github.io/jest/ + // ── Test 2 — mention in comment ────────────────────────────── + + test("replies when the bot is mentioned in a comment", async () => { + const issueCommentPayload = { + action: "created", + comment: { + body: "hello @agentwaspai can you help?", + user: { type: "User" }, + }, + issue: { number: 1 }, + repository: { name: "testing-things", owner: { login: "hiimbex" } }, + installation: { id: 2 }, + }; + + const mock = nock("https://api.github.com") + .post("/app/installations/2/access_tokens") + .reply(200, { token: "test", permissions: { issues: "write" } }) + + .post("/repos/hiimbex/testing-things/issues/1/comments", (body) => { + // just confirm a reply was posted — don't assert exact INTRO_MESSAGE text + assert.ok(body.body.length > 0); + return true; + }) + .reply(200, { id: 456 }); + + await probot.receive({ name: "issue_comment", payload: issueCommentPayload }); -// For more information about testing with Nock see: -// https://github.com/nock/nock + assert.deepStrictEqual(mock.pendingMocks(), []); + }); +}); \ No newline at end of file