Week 1 Assignment

This commit is contained in:
isaackann
2026-09-23 12:22:50 -07:00
parent ca2df55b78
commit 90a2fbb8b1
123 changed files with 255 additions and 4742 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Assignments for CS146S: The Modern Software Developer
This is the home of the assignments for [CS146S: The Modern Software Developer](https://themodernsoftware.dev), taught at Stanford University fall 2025.
This is the home of the assignments for [CS146S: The Modern Software Developer](https://themodernsoftware.dev), taught at Stanford University fall 2026.
## Repo Setup
These steps work with Python 3.12.
-4
View File
@@ -1,4 +0,0 @@
## LLM Prompting Playground
Practice core LLM prompting techniques essential to using and understanding coding LLMs. The full assignment description
is provided at [assignment.md](./assignment.md)
+132 -37
View File
@@ -1,52 +1,147 @@
# Week 1 — Prompting Techniques
# Week 1: Trace Dissection of a Real Claude Code Session
You will practice multiple prompting techniques by crafting prompts to complete specific tasks. Each task’s instructions are at the top of its corresponding source file.
## Assignment Overview
## Installation
Make sure you have first done the installation described in the top-level `README.md`.
This week you will put a real Claude Code session behind a proxy, capture the actual HTTP requests it sends, and **dissect them**. You are not building anything new. You are reading someone else's production system at the level of detail where its design decisions become visible.
## Ollama installation
We will be using a tool to run different state-of-the-art LLMs locally on your machine called [Ollama](https://ollama.com/). Use one of the following methods:
### Learning Goals
- macOS (Homebrew):
```bash
brew install --cask ollama
ollama serve
```
- **Trace** a real coding session from a production-grade coding agent and understand its call structure.
- **Identify** how prompting, tool schemas, and model responses interact during a non-trivial coding task.
- **Reflect** on which behaviors you would replicate, and which you would change, in your own custom agents.
- Linux (recommended):
```bash
curl -fsSL https://ollama.com/install.sh | sh
```
## Materials
- Windows:
Download and run the installer from [ollama.com/download](https://ollama.com/download).
- **[Intercepting Claude Code Requests](https://www.ai.moda/en/blog/tutorial-intercepting-claude-code-requests)**: the technique this assignment is built on. Read it first.
- **[Anthropic Messages API reference](https://docs.claude.com/en/api/messages)**: the request shape you will be reading (`system`, `tools`, `messages`).
- **[Claude Code settings reference](https://docs.claude.com/en/docs/claude-code/settings)**: how user-level (`~/.claude/settings.json`) and project-level (`.claude/settings.json`) settings work.
## Setup
**Prerequisite: Claude Code**. Stanford provides access to Claude Code via your SUNet ID. If you haven't activated your account yet, [request access](https://uit.stanford.edu/service/claude), then [install and sign in](https://code.claude.com/docs/en/setup). After your account is approved and you have completed the install, confirm with `claude --version` and record that version in your writeup.
**1. Install mitmproxy:**
- **macOS**: `brew install --cask mitmproxy`.
- **Windows**: run the installer from [mitmproxy.org](https://mitmproxy.org/), which puts `mitmweb` on your `PATH`.
- **Linux, or any platform**: `pip install mitmproxy`, inside the course conda env if you made one.
**2. Start it as a reverse proxy:**
Verify installation:
```bash
ollama -v
mitmweb --listen-host 127.0.0.1 --listen-port 58888 \
--web-open-browser --mode reverse:https://api.anthropic.com \
-w session.flows
```
Before running the test scripts, make sure you have the following models pulled. You only need to do this once (unless you remove the models later):
```bash
ollama run mistral-nemo:12b
ollama run llama3.1:8b
Traffic sent to `127.0.0.1:58888` is forwarded to the real API; the inspection UI opens at `http://localhost:8081`. Reverse mode means you point Claude Code at a plain-HTTP local address, so there is no CA certificate to install. `-w session.flows` saves every flow to disk; run the command from a directory **outside** any git repo so the capture file won't be committed by accident.
**3. Point Claude Code at it**: in the repo you will use for Part I, create a **project-level** `.claude/settings.json`:
```json
{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:58888",
"ENABLE_TOOL_SEARCH": "true"
}
}
```
## Techniques and source files
- K-shot prompting — `week1/k_shot_prompting.py`
- Chain-of-thought — `week1/chain_of_thought.py`
- Tool calling — `week1/tool_calling.py`
- Self-consistency prompting — `week1/self_consistency_prompting.py`
- RAG (Retrieval-Augmented Generation) — `week1/rag.py`
- Reflexion — `week1/reflexion.py`
> ⚠️ **Don't put this in `~/.claude/settings.json`!** Otherwise, any Claude Code session on your machine will land in your capture (and sessions won't work once `mitmweb` is stopped).
**4. Verify**: start a new `claude` session, send anything, and confirm a `POST /v1/messages` flow appears in mitmweb.
**5. Post-Assignment**: when you're done, delete the repo's `.claude/settings.json` (or its `env` block) and stop `mitmweb`.
## Part I: Capture a Session (15 pts)
Run **one** session under the proxy meeting all four requirements:
1. **Multi-file**: touches at least two files.
2. **Fails at least once**: you need an error-recovery sequence. Breaking a test on purpose is the reliable way to get one.
3. **Long enough to plan**: the agent should make an explicit plan, not fire a single tool call: a task list, plan mode, or a plan file.
4. **Your own repo**: a scratch project, not this one.
**Task ideas**, if you'd rather not invent one:
- Delete a function that other modules import, then ask Claude to restore full functionality with the tests passing.
- Add an endpoint plus tests to a small web app, then ask it to make the suite pass on a dependency version you don't have installed.
- Rename a module and have it update every import, then run lint and tests.
- Ask for a feature that requires an unfamiliar library, so the agent has to look up the API before it can write anything.
In the mitmweb UI, select a `POST /v1/messages` flow and download the **request body** as JSON. Keep these locally. You are not submitting them, but every later part is graded on evidence drawn from them, so keep enough to support your answers.
If your mitmweb session terminates, you can work from the saved `session.flows` file: reopen it with `mitmweb -r session.flows`.
### ⚠️ Sanitize what you quote
Your capture contains your own source code, file paths, and credentials. Nothing from it should reach the repo except the excerpts you deliberately quote in `writeup.md`.
- Never paste raw flows or HTTP headers, since that is where `x-api-key` / `authorization` live.
- **Request bodies can contain secrets too.** Anything the agent read (`.env`, config files, command outputs) is replayed in `messages`. Check tool results before quoting them.
- Keep `session.flows` out of every git repo.
- Redact private content in your quotes with a visible marker (`[REDACTED: internal hostname]`), not a silent deletion.
- If an excerpt can't be sanitized without destroying its meaning, re-capture on a throwaway repo.
## Part II: Annotate the System Prompt (25 pts)
Break the `system` block and any messages with `role: "system"` into their sections. For each, answer: **what behavior is this buying, and what failure mode is it defending against?** An annotation, not a summary.
Cover at least:
- **Structure**: the major sections, their order, and why that order.
- **Tone and verbosity**: the specific language controlling response length and format, and why it is worth the tokens.
- **When not to act**: destructive-operation gates, scope limits, refusal conditions.
- **Environment context**: what the agent is told about the machine, repo, and session, and where that lives in the request.
Then, on `<system-reminder>` specifically: where do they appear (system block, messages, or both, citing an example), what two distinct purposes can you evidence, and why inject them mid-conversation rather than stating them once up front?
## Part III: Annotate the Tool Design (25 pts)
**Inventory: numbers, not prose.** How many tools were available, broken down by built-in vs. MCP-provided vs. deferred/searchable? Note any change in the tool set across requests and what triggered it. A good answer reads *"117 tools in the first request: 35 built-in, 82 from three MCP servers"*, not *"there were many tools available."*
**Design analysis.** Pick **two** tools and analyze each as interface design:
- Reproduce the relevant part of the schema.
- Why this parameter set? What is required, what is optional, what is deliberately not exposed?
- What is the description defending against? Tool descriptions in a mature agent are largely accumulated scar tissue. Find a sentence that exists only because a model kept doing the wrong thing, and name that wrong thing.
- What does the tool deliberately *not* do, and what does that imply about the surrounding system?
Pick two tools that differ. Two file-manipulation tools is a weak selection; a file tool paired with an orchestration tool or one with an unusual failure contract is a strong one.
## Part IV: Behavioral Analysis with Evidence (25 pts)
Answer each question from your own trace. Every answer must **cite its evidence** (which request, message index, tool call) and **label itself `[OBSERVED]` or `[INFERRED]`**: observed means you can point at it in your capture, inferred means you are reasoning from definitions without having watched it happen. Both are acceptable; mislabeling is not, and unlabeled answers earn no credit.
- **Error recovery**: walk one failure end to end. What did the agent see, what did it try next, how many turns did recovery take? Quote verbatim.
- **Planning**: a tool, a prompt instruction, emergent behavior, or a combination? What evidence separates those?
- **Plans and task state**: how does one get created and advanced? What does the model see about task state each turn, and where does it live in the request?
- **Subagents**: when does the agent delegate? What does the subagent get told, and what comes back? (An honest `[INFERRED]` is fine if your session never triggered one.)
- **Context management**: as the session grows, what changes in the payloads? How are earlier turns represented later?
## Part V: Reflection (10 pts)
At most one page: **two decisions you would copy** and the problem each solves; **one you would make differently**, engaging with why it might be there; and **one thing the trace changed** about how you will steer a coding agent day to day.
## Deliverables
- Read the task description in each file.
- Design and run prompts (look for all the places labeled `TODO` in the code). That should be the only thing you have to change (i.e. don't tinker with the model).
- Iterate to improve results until the test script passes.
- Save your final prompt(s) and output for each technique.
- Make sure to include in your submission the completed code for each prompting technique file. ***Double check that all `TODO`s have been resolved.***
## Evaluation rubric (60 pts total)
- 10 for each completed prompt across the 6 different prompting techniques
A completed **`week1/writeup.md`** with every `TODO` filled in. Your captured traces stay on your machine.
## Evaluation Rubric (100 pts total)
| Part | Points | What earns full credit |
|---|---|---|
| I. Capture & reproducibility | 15 | All four session requirements met; setup and session documented well enough to reproduce from your writeup alone |
| II. System prompt annotation | 25 | Sections tied to behavior and failure modes; `<system-reminder>` explained with cited examples |
| III. Tool inventory & design | 25 | Concrete counts with a breakdown; two genuinely different tools analyzed as interface design |
| IV. Behavioral analysis | 25 | Every answer cited and correctly labeled; error recovery quoted verbatim |
| V. Reflection | 10 | Specific, argued positions rather than restatement |
Deductions for unsanitized credentials in quoted excerpts, and for claims presented as observation that your trace does not support.
## SUBMISSION INSTRUCTIONS
1. Make sure you have all changes pushed to your remote repository for grading.
2. **Make sure you've added `mihail911`, `isaackann`, and `vdaita` as collaborators on your assignment repository.**
3. Submit via Gradescope.
4. **Don't forget to remove `ANTHROPIC_BASE_URL` from your repo's `.claude/settings.json`!**
-72
View File
@@ -1,72 +0,0 @@
import os
import re
from dotenv import load_dotenv
from ollama import chat
load_dotenv()
NUM_RUNS_TIMES = 5
# TODO: Fill this in!
YOUR_SYSTEM_PROMPT = ""
USER_PROMPT = """
Solve this problem, then give the final answer on the last line as "Answer: <number>".
what is 3^{12345} (mod 100)?
"""
# For this simple example, we expect the final numeric answer only
EXPECTED_OUTPUT = "Answer: 43"
def extract_final_answer(text: str) -> str:
"""Extract the final 'Answer: ...' line from a verbose reasoning trace.
- Finds the LAST line that starts with 'Answer:' (case-insensitive)
- Normalizes to 'Answer: <number>' when a number is present
- Falls back to returning the matched content if no number is detected
"""
matches = re.findall(r"(?mi)^\s*answer\s*:\s*(.+)\s*$", text)
if matches:
value = matches[-1].strip()
# Prefer a numeric normalization when possible (supports integers/decimals)
num_match = re.search(r"-?\d+(?:\.\d+)?", value.replace(",", ""))
if num_match:
return f"Answer: {num_match.group(0)}"
return f"Answer: {value}"
return text.strip()
def test_your_prompt(system_prompt: str) -> bool:
"""Run up to NUM_RUNS_TIMES and return True if any output matches EXPECTED_OUTPUT.
Prints "SUCCESS" when a match is found.
"""
for idx in range(NUM_RUNS_TIMES):
print(f"Running test {idx + 1} of {NUM_RUNS_TIMES}")
response = chat(
model="llama3.1:8b",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": USER_PROMPT},
],
options={"temperature": 0.3},
)
output_text = response.message.content
final_answer = extract_final_answer(output_text)
if final_answer.strip() == EXPECTED_OUTPUT.strip():
print("SUCCESS")
return True
else:
print(f"Expected output: {EXPECTED_OUTPUT}")
print(f"Actual output: {final_answer}")
return False
if __name__ == "__main__":
test_your_prompt(YOUR_SYSTEM_PROMPT)
-14
View File
@@ -1,14 +0,0 @@
API Reference
Base URL: https://api.example.com/v1
Authentication:
Provide header X-API-Key: <your key>
Endpoints:
GET /users/{id}
- Returns 200 with JSON: {"id": <string>, "name": <string>}
-46
View File
@@ -1,46 +0,0 @@
import os
from dotenv import load_dotenv
from ollama import chat
load_dotenv()
NUM_RUNS_TIMES = 5
# TODO: Fill this in!
YOUR_SYSTEM_PROMPT = ""
USER_PROMPT = """
Reverse the order of letters in the following word. Only output the reversed word, no other text:
httpstatus
"""
EXPECTED_OUTPUT = "sutatsptth"
def test_your_prompt(system_prompt: str) -> bool:
"""Run the prompt up to NUM_RUNS_TIMES and return True if any output matches EXPECTED_OUTPUT.
Prints "SUCCESS" when a match is found.
"""
for idx in range(NUM_RUNS_TIMES):
print(f"Running test {idx + 1} of {NUM_RUNS_TIMES}")
response = chat(
model="mistral-nemo:12b",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": USER_PROMPT},
],
options={"temperature": 0.5},
)
output_text = response.message.content.strip()
if output_text.strip() == EXPECTED_OUTPUT.strip():
print("SUCCESS")
return True
else:
print(f"Expected output: {EXPECTED_OUTPUT}")
print(f"Actual output: {output_text}")
return False
if __name__ == "__main__":
test_your_prompt(YOUR_SYSTEM_PROMPT)
-123
View File
@@ -1,123 +0,0 @@
import os
import re
from typing import List, Callable
from dotenv import load_dotenv
from ollama import chat
load_dotenv()
NUM_RUNS_TIMES = 5
DATA_FILES: List[str] = [
os.path.join(os.path.dirname(__file__), "data", "api_docs.txt"),
]
def load_corpus_from_files(paths: List[str]) -> List[str]:
corpus: List[str] = []
for p in paths:
if os.path.exists(p):
try:
with open(p, "r", encoding="utf-8") as f:
corpus.append(f.read())
except Exception as exc:
corpus.append(f"[load_error] {p}: {exc}")
else:
corpus.append(f"[missing_file] {p}")
return corpus
# Load corpus from external files (simple API docs). If missing, fall back to inline snippet
CORPUS: List[str] = load_corpus_from_files(DATA_FILES)
QUESTION = (
"Write a Python function `fetch_user_name(user_id: str, api_key: str) -> str` that calls the documented API "
"to fetch a user by id and returns only the user's name as a string."
)
# TODO: Fill this in!
YOUR_SYSTEM_PROMPT = ""
# For this simple example
# For this coding task, validate by required snippets rather than exact string
REQUIRED_SNIPPETS = [
"def fetch_user_name(",
"requests.get",
"/users/",
"X-API-Key",
"return",
]
def YOUR_CONTEXT_PROVIDER(corpus: List[str]) -> List[str]:
"""TODO: Select and return the relevant subset of documents from CORPUS for this task.
For example, return [] to simulate missing context, or [corpus[0]] to include the API docs.
"""
return []
def make_user_prompt(question: str, context_docs: List[str]) -> str:
if context_docs:
context_block = "\n".join(f"- {d}" for d in context_docs)
else:
context_block = "(no context provided)"
return (
f"Context (use ONLY this information):\n{context_block}\n\n"
f"Task: {question}\n\n"
"Requirements:\n"
"- Use the documented Base URL and endpoint.\n"
"- Send the documented authentication header.\n"
"- Raise for non-200 responses.\n"
"- Return only the user's name string.\n\n"
"Output: A single fenced Python code block with the function and necessary imports.\n"
)
def extract_code_block(text: str) -> str:
"""Extract the last fenced Python code block, or any fenced code block, else return text."""
# Try ```python ... ``` first
m = re.findall(r"```python\n([\s\S]*?)```", text, flags=re.IGNORECASE)
if m:
return m[-1].strip()
# Fallback to any fenced code block
m = re.findall(r"```\n([\s\S]*?)```", text)
if m:
return m[-1].strip()
return text.strip()
def test_your_prompt(system_prompt: str, context_provider: Callable[[List[str]], List[str]]) -> bool:
"""Run up to NUM_RUNS_TIMES and return True if any output matches EXPECTED_OUTPUT."""
context_docs = context_provider(CORPUS)
user_prompt = make_user_prompt(QUESTION, context_docs)
for idx in range(NUM_RUNS_TIMES):
print(f"Running test {idx + 1} of {NUM_RUNS_TIMES}")
response = chat(
model="llama3.1:8b",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
options={"temperature": 0.0},
)
output_text = response.message.content
code = extract_code_block(output_text)
missing = [s for s in REQUIRED_SNIPPETS if s not in code]
if not missing:
print(output_text)
print("SUCCESS")
return True
else:
print("Missing required snippets:")
for s in missing:
print(f" - {s}")
print("Generated code:\n" + code)
return False
if __name__ == "__main__":
test_your_prompt(YOUR_SYSTEM_PROMPT, YOUR_CONTEXT_PROVIDER)
-153
View File
@@ -1,153 +0,0 @@
import os
import re
from typing import Callable, List, Tuple
from dotenv import load_dotenv
from ollama import chat
load_dotenv()
NUM_RUNS_TIMES = 1
SYSTEM_PROMPT = """
You are a coding assistant. Output ONLY a single fenced Python code block that defines
the function is_valid_password(password: str) -> bool. No prose or comments.
Keep the implementation minimal.
"""
# TODO: Fill this in!
YOUR_REFLEXION_PROMPT = ""
# Ground-truth test suite used to evaluate generated code
SPECIALS = set("!@#$%^&*()-_")
TEST_CASES: List[Tuple[str, bool]] = [
("Password1!", True), # valid
("password1!", False), # missing uppercase
("Password!", False), # missing digit
("Password1", False), # missing special
]
def extract_code_block(text: str) -> str:
m = re.findall(r"```python\n([\s\S]*?)```", text, flags=re.IGNORECASE)
if m:
return m[-1].strip()
m = re.findall(r"```\n([\s\S]*?)```", text)
if m:
return m[-1].strip()
return text.strip()
def load_function_from_code(code_str: str) -> Callable[[str], bool]:
namespace: dict = {}
exec(code_str, namespace) # noqa: S102 (executing controlled code from model for exercise)
func = namespace.get("is_valid_password")
if not callable(func):
raise ValueError("No callable is_valid_password found in generated code")
return func
def evaluate_function(func: Callable[[str], bool]) -> Tuple[bool, List[str]]:
failures: List[str] = []
for pw, expected in TEST_CASES:
try:
result = bool(func(pw))
except Exception as exc:
failures.append(f"Input: {pw} → raised exception: {exc}")
continue
if result != expected:
# Compute diagnostic based on ground-truth rules
reasons = []
if len(pw) < 8:
reasons.append("length < 8")
if not any(c.islower() for c in pw):
reasons.append("missing lowercase")
if not any(c.isupper() for c in pw):
reasons.append("missing uppercase")
if not any(c.isdigit() for c in pw):
reasons.append("missing digit")
if not any(c in SPECIALS for c in pw):
reasons.append("missing special")
if any(c.isspace() for c in pw):
reasons.append("has whitespace")
failures.append(
f"Input: {pw} → expected {expected}, got {result}. Failing checks: {', '.join(reasons) or 'unknown'}"
)
return (len(failures) == 0, failures)
def generate_initial_function(system_prompt: str) -> str:
response = chat(
model="llama3.1:8b",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": "Provide the implementation now."},
],
options={"temperature": 0.2},
)
return extract_code_block(response.message.content)
def your_build_reflexion_context(prev_code: str, failures: List[str]) -> str:
"""TODO: Build the user message for the reflexion step using prev_code and failures.
Return a string that will be sent as the user content alongside the reflexion system prompt.
"""
return ""
def apply_reflexion(
reflexion_prompt: str,
build_context: Callable[[str, List[str]], str],
prev_code: str,
failures: List[str],
) -> str:
reflection_context = build_context(prev_code, failures)
print(f"REFLECTION CONTEXT: {reflection_context}, {reflexion_prompt}")
response = chat(
model="llama3.1:8b",
messages=[
{"role": "system", "content": reflexion_prompt},
{"role": "user", "content": reflection_context},
],
options={"temperature": 0.2},
)
return extract_code_block(response.message.content)
def run_reflexion_flow(
system_prompt: str,
reflexion_prompt: str,
build_context: Callable[[str, List[str]], str],
) -> bool:
# 1) Generate initial function
initial_code = generate_initial_function(system_prompt)
print("Initial code:\n" + initial_code)
func = load_function_from_code(initial_code)
passed, failures = evaluate_function(func)
if passed:
print("SUCCESS (initial implementation passed all tests)")
return True
else:
print(f"FAILURE (initial implementation failed some tests): {failures}")
# 2) Single reflexion iteration
improved_code = apply_reflexion(reflexion_prompt, build_context, initial_code, failures)
print("\nImproved code:\n" + improved_code)
improved_func = load_function_from_code(improved_code)
passed2, failures2 = evaluate_function(improved_func)
if passed2:
print("SUCCESS")
return True
print("Tests still failing after reflexion:")
for f in failures2:
print("- " + f)
return False
if __name__ == "__main__":
run_reflexion_flow(SYSTEM_PROMPT, YOUR_REFLEXION_PROMPT, your_build_reflexion_context)
-86
View File
@@ -1,86 +0,0 @@
import os
import re
from collections import Counter
from dotenv import load_dotenv
from ollama import chat
load_dotenv()
NUM_RUNS_TIMES = 5
# TODO: Fill this in! Try to get as close to 100% correctness across all runs as possible.
YOUR_SYSTEM_PROMPT = ""
USER_PROMPT = """
Solve this problem, then give the final answer on the last line as "Answer: <number>".
Henry made two stops during his 60-mile bike trip. He first stopped after 20
miles. His second stop was 15 miles before the end of the trip. How many miles
did he travel between his first and second stops?
"""
EXPECTED_OUTPUT = "Answer: 25"
def extract_final_answer(text: str) -> str:
"""Extract the final 'Answer: ...' line from a verbose reasoning trace.
- Finds the LAST line that starts with 'Answer:' (case-insensitive)
- Normalizes to 'Answer: <number>' when a number is present
- Falls back to returning the matched content if no number is detected
"""
matches = re.findall(r"(?mi)^\s*answer\s*:\s*(.+)\s*$", text)
if matches:
value = matches[-1].strip()
num_match = re.search(r"-?\d+(?:\.\d+)?", value.replace(",", ""))
if num_match:
return f"Answer: {num_match.group(0)}"
return f"Answer: {value}"
return text.strip()
def test_your_prompt(system_prompt: str) -> bool:
"""Run the prompt NUM_RUNS_TIMES, majority-vote on the extracted 'Answer: ...' lines.
Prints "SUCCESS" if the majority answer equals EXPECTED_OUTPUT.
"""
answers: list[str] = []
for idx in range(NUM_RUNS_TIMES):
print(f"Running test {idx + 1} of {NUM_RUNS_TIMES}")
response = chat(
model="llama3.1:8b",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": USER_PROMPT},
],
options={"temperature": 1},
)
output_text = response.message.content
final_answer = extract_final_answer(output_text)
print(f"Run {idx + 1} answer: {final_answer}")
answers.append(final_answer.strip())
if not answers:
print("No answers produced.")
return False
counts = Counter(answers)
majority_answer, majority_count = counts.most_common(1)[0]
print(f"Majority answer: {majority_answer} ({majority_count}/{len(answers)})")
if majority_answer.strip() == EXPECTED_OUTPUT.strip():
print("SUCCESS")
return True
# Print distribution for debugging when majority does not match expected
print(f"Expected output: {EXPECTED_OUTPUT}")
print("Answer distribution:")
for answer, count in counts.most_common():
print(f" {answer}: {count}")
return False
if __name__ == "__main__":
test_your_prompt(YOUR_SYSTEM_PROMPT)
-168
View File
@@ -1,168 +0,0 @@
import ast
import json
import os
from typing import Any, Dict, List, Optional, Tuple, Callable
from dotenv import load_dotenv
from ollama import chat
load_dotenv()
NUM_RUNS_TIMES = 3
# ==========================
# Tool implementation (the "executor")
# ==========================
def _annotation_to_str(annotation: Optional[ast.AST]) -> str:
if annotation is None:
return "None"
try:
return ast.unparse(annotation) # type: ignore[attr-defined]
except Exception:
# Fallback best-effort
if isinstance(annotation, ast.Name):
return annotation.id
return type(annotation).__name__
def _list_function_return_types(file_path: str) -> List[Tuple[str, str]]:
with open(file_path, "r", encoding="utf-8") as f:
source = f.read()
tree = ast.parse(source)
results: List[Tuple[str, str]] = []
for node in tree.body:
if isinstance(node, ast.FunctionDef):
return_str = _annotation_to_str(node.returns)
results.append((node.name, return_str))
# Sort for stable output
results.sort(key=lambda x: x[0])
return results
def output_every_func_return_type(file_path: str = None) -> str:
"""Tool: Return a newline-delimited list of "name: return_type" for each top-level function."""
path = file_path or __file__
if not os.path.isabs(path):
# Try file relative to this script if not absolute
candidate = os.path.join(os.path.dirname(__file__), path)
if os.path.exists(candidate):
path = candidate
pairs = _list_function_return_types(path)
return "\n".join(f"{name}: {ret}" for name, ret in pairs)
# Sample functions to ensure there is something to analyze
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> str:
return f"Hello, {name}!"
# Tool registry for dynamic execution by name
TOOL_REGISTRY: Dict[str, Callable[..., str]] = {
"output_every_func_return_type": output_every_func_return_type,
}
# ==========================
# Prompt scaffolding
# ==========================
# TODO: Fill this in!
YOUR_SYSTEM_PROMPT = ""
def resolve_path(p: str) -> str:
if os.path.isabs(p):
return p
here = os.path.dirname(__file__)
c1 = os.path.join(here, p)
if os.path.exists(c1):
return c1
# Try sibling of project root if needed
return p
def extract_tool_call(text: str) -> Dict[str, Any]:
"""Parse a single JSON object from the model output."""
text = text.strip()
# Some models wrap JSON in code fences; attempt to strip
if text.startswith("```") and text.endswith("```"):
text = text.strip("`")
if text.lower().startswith("json\n"):
text = text[5:]
try:
obj = json.loads(text)
return obj
except json.JSONDecodeError:
raise ValueError("Model did not return valid JSON for the tool call")
def run_model_for_tool_call(system_prompt: str) -> Dict[str, Any]:
response = chat(
model="llama3.1:8b",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": "Call the tool now."},
],
options={"temperature": 0.3},
)
content = response.message.content
return extract_tool_call(content)
def execute_tool_call(call: Dict[str, Any]) -> str:
name = call.get("tool")
if not isinstance(name, str):
raise ValueError("Tool call JSON missing 'tool' string")
func = TOOL_REGISTRY.get(name)
if func is None:
raise ValueError(f"Unknown tool: {name}")
args = call.get("args", {})
if not isinstance(args, dict):
raise ValueError("Tool call JSON 'args' must be an object")
# Best-effort path resolution if a file_path arg is present
if "file_path" in args and isinstance(args["file_path"], str):
args["file_path"] = resolve_path(args["file_path"]) if str(args["file_path"]) != "" else __file__
elif "file_path" not in args:
# Provide default for tools expecting file_path
args["file_path"] = __file__
return func(**args)
def compute_expected_output() -> str:
# Ground-truth expected output based on the actual file contents
return output_every_func_return_type(__file__)
def test_your_prompt(system_prompt: str) -> bool:
"""Run once: require the model to produce a valid tool call; compare tool output to expected."""
expected = compute_expected_output()
for _ in range(NUM_RUNS_TIMES):
try:
call = run_model_for_tool_call(system_prompt)
except Exception as exc:
print(f"Failed to parse tool call: {exc}")
continue
print(call)
try:
actual = execute_tool_call(call)
except Exception as exc:
print(f"Tool execution failed: {exc}")
continue
if actual.strip() == expected.strip():
print(f"Generated tool call: {call}")
print(f"Generated output: {actual}")
print("SUCCESS")
return True
else:
print("Expected output:\n" + expected)
print("Actual output:\n" + actual)
return False
if __name__ == "__main__":
test_your_prompt(YOUR_SYSTEM_PROMPT)
+122
View File
@@ -0,0 +1,122 @@
# Week 1 Write-up
## Part I: Capture
**Setup** (enough for a reader to reproduce your capture):
```
claude --version: TODO
mitmproxy version: TODO
proxy command: TODO
settings file: TODO (path + env block)
```
**The session.** What task, against what repo, and how many `POST /v1/messages` requests did it produce?
> TODO
| Requirement | Evidence |
|---|---|
| Touched ≥ 2 files | TODO |
| Failed at least once | TODO |
| Long enough to plan | TODO |
| Your own repo | TODO |
**What you redacted** from the excerpts quoted below, and why:
> TODO
## Part II: System Prompt Annotation
**a. Structure.** Major sections in order, one line each on what it does, and why this order.
> TODO
**b. Tone and verbosity.** Quote the controlling instructions, then say what failure mode they defend against.
```
TODO
```
> TODO
**c. When not to act.** Quote the destructive-operation gates, scope limits, or refusal conditions, and what each buys.
```
TODO
```
> TODO
**d. Environment context.** What the agent is told about machine/repo/session, and where it lives in the request (`system` field or a `role: "system"` message).
> TODO
**e. `<system-reminder>`.** Where they appear (cite an example), two distinct purposes you can evidence, and why they are injected mid-conversation rather than stated once.
```
TODO
```
> TODO
## Part III: Tool Design Annotation
**Inventory.** Did the set change across requests? If so, what triggered it?
| Built-in | MCP | Deferred | **Total** | Changed mid-session? |
|---|---|---|---|---|
| TODO | TODO | TODO | **TODO** | TODO |
**Two tools.** Pick tools that differ from each other.
| | Tool 1 | Tool 2 |
|---|---|---|
| Name | TODO | TODO |
| Key schema fields | TODO | TODO |
| Required vs. optional vs. not exposed, and why | TODO | TODO |
| Description is defending against… (quote + the wrong behavior) | TODO | TODO |
| Deliberately does *not* do… and what that implies | TODO | TODO |
Why these two?
> TODO
## Part IV: Behavioral Analysis
**Every answer must be labeled `[OBSERVED]` or `[INFERRED]` and cite its evidence. Unlabeled answers earn no credit.**
**a. Error recovery**: `TODO: label` · evidence: `TODO`
What the agent saw, verbatim:
```
TODO
```
What it tried next, and turns to recover:
> TODO
**b. Planning**: `TODO: label` · evidence: `TODO`
> TODO
**c. Plans and task state**: `TODO: label` · evidence: `TODO` \
How does one get created and advanced? What does the model see about task state each turn, and where does it live in the request:
> TODO
**d. Subagents**: `TODO: label` · evidence: `TODO` \
When the agent delegates, what the subagent is told, and what comes back:
> TODO
**e. Context management**: `TODO: label` · evidence: `TODO` \
What changed in the payloads as the session grew:
> TODO
## Part V: Reflection
**Two decisions you would copy**, and the problem each solves:
1. TODO
2. TODO
**One you would make differently** (engage with why it might be there):
> TODO
**One thing the trace changed** about how you will steer a coding agent:
> TODO
## Submission
1. `Command (⌘) + F` for `TODO`. No results means you're done.
2. Confirm no credentials or `x-api-key` headers made it into your quoted excerpts.
3. Push all changes to your remote repository and submit via Gradescope.
4. Don't forget to remove `ANTHROPIC_BASE_URL` from your repo's `.claude/settings.json`!
-3
View File
@@ -1,3 +0,0 @@
__all__ = []
-3
View File
@@ -1,3 +0,0 @@
__all__ = []
-116
View File
@@ -1,116 +0,0 @@
from __future__ import annotations
import sqlite3
from pathlib import Path
from typing import Optional
BASE_DIR = Path(__file__).resolve().parents[1]
DATA_DIR = BASE_DIR / "data"
DB_PATH = DATA_DIR / "app.db"
def ensure_data_directory_exists() -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)
def get_connection() -> sqlite3.Connection:
ensure_data_directory_exists()
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
return connection
def init_db() -> None:
ensure_data_directory_exists()
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
"""
)
cursor.execute(
"""
CREATE TABLE IF NOT EXISTS action_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id INTEGER,
text TEXT NOT NULL,
done INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (note_id) REFERENCES notes(id)
);
"""
)
connection.commit()
def insert_note(content: str) -> int:
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute("INSERT INTO notes (content) VALUES (?)", (content,))
connection.commit()
return int(cursor.lastrowid)
def list_notes() -> list[sqlite3.Row]:
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute("SELECT id, content, created_at FROM notes ORDER BY id DESC")
return list(cursor.fetchall())
def get_note(note_id: int) -> Optional[sqlite3.Row]:
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute(
"SELECT id, content, created_at FROM notes WHERE id = ?",
(note_id,),
)
row = cursor.fetchone()
return row
def insert_action_items(items: list[str], note_id: Optional[int] = None) -> list[int]:
with get_connection() as connection:
cursor = connection.cursor()
ids: list[int] = []
for item in items:
cursor.execute(
"INSERT INTO action_items (note_id, text) VALUES (?, ?)",
(note_id, item),
)
ids.append(int(cursor.lastrowid))
connection.commit()
return ids
def list_action_items(note_id: Optional[int] = None) -> list[sqlite3.Row]:
with get_connection() as connection:
cursor = connection.cursor()
if note_id is None:
cursor.execute(
"SELECT id, note_id, text, done, created_at FROM action_items ORDER BY id DESC"
)
else:
cursor.execute(
"SELECT id, note_id, text, done, created_at FROM action_items WHERE note_id = ? ORDER BY id DESC",
(note_id,),
)
return list(cursor.fetchall())
def mark_action_item_done(action_item_id: int, done: bool) -> None:
with get_connection() as connection:
cursor = connection.cursor()
cursor.execute(
"UPDATE action_items SET done = ? WHERE id = ?",
(1 if done else 0, action_item_id),
)
connection.commit()
-30
View File
@@ -1,30 +0,0 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, Optional
from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from .db import init_db
from .routers import action_items, notes
from . import db
init_db()
app = FastAPI(title="Action Item Extractor")
@app.get("/", response_class=HTMLResponse)
def index() -> str:
html_path = Path(__file__).resolve().parents[1] / "frontend" / "index.html"
return html_path.read_text(encoding="utf-8")
app.include_router(notes.router)
app.include_router(action_items.router)
static_dir = Path(__file__).resolve().parents[1] / "frontend"
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
-3
View File
@@ -1,3 +0,0 @@
__all__ = []
-50
View File
@@ -1,50 +0,0 @@
from __future__ import annotations
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, HTTPException
from .. import db
from ..services.extract import extract_action_items
router = APIRouter(prefix="/action-items", tags=["action-items"])
@router.post("/extract")
def extract(payload: Dict[str, Any]) -> Dict[str, Any]:
text = str(payload.get("text", "")).strip()
if not text:
raise HTTPException(status_code=400, detail="text is required")
note_id: Optional[int] = None
if payload.get("save_note"):
note_id = db.insert_note(text)
items = extract_action_items(text)
ids = db.insert_action_items(items, note_id=note_id)
return {"note_id": note_id, "items": [{"id": i, "text": t} for i, t in zip(ids, items)]}
@router.get("")
def list_all(note_id: Optional[int] = None) -> List[Dict[str, Any]]:
rows = db.list_action_items(note_id=note_id)
return [
{
"id": r["id"],
"note_id": r["note_id"],
"text": r["text"],
"done": bool(r["done"]),
"created_at": r["created_at"],
}
for r in rows
]
@router.post("/{action_item_id}/done")
def mark_done(action_item_id: int, payload: Dict[str, Any]) -> Dict[str, Any]:
done = bool(payload.get("done", True))
db.mark_action_item_done(action_item_id, done)
return {"id": action_item_id, "done": done}
-34
View File
@@ -1,34 +0,0 @@
from __future__ import annotations
from typing import Any, Dict, List
from fastapi import APIRouter, HTTPException
from .. import db
router = APIRouter(prefix="/notes", tags=["notes"])
@router.post("")
def create_note(payload: Dict[str, Any]) -> Dict[str, Any]:
content = str(payload.get("content", "")).strip()
if not content:
raise HTTPException(status_code=400, detail="content is required")
note_id = db.insert_note(content)
note = db.get_note(note_id)
return {
"id": note["id"],
"content": note["content"],
"created_at": note["created_at"],
}
@router.get("/{note_id}")
def get_single_note(note_id: int) -> Dict[str, Any]:
row = db.get_note(note_id)
if row is None:
raise HTTPException(status_code=404, detail="note not found")
return {"id": row["id"], "content": row["content"], "created_at": row["created_at"]}
-89
View File
@@ -1,89 +0,0 @@
from __future__ import annotations
import os
import re
from typing import List
import json
from typing import Any
from ollama import chat
from dotenv import load_dotenv
load_dotenv()
BULLET_PREFIX_PATTERN = re.compile(r"^\s*([-*•]|\d+\.)\s+")
KEYWORD_PREFIXES = (
"todo:",
"action:",
"next:",
)
def _is_action_line(line: str) -> bool:
stripped = line.strip().lower()
if not stripped:
return False
if BULLET_PREFIX_PATTERN.match(stripped):
return True
if any(stripped.startswith(prefix) for prefix in KEYWORD_PREFIXES):
return True
if "[ ]" in stripped or "[todo]" in stripped:
return True
return False
def extract_action_items(text: str) -> List[str]:
lines = text.splitlines()
extracted: List[str] = []
for raw_line in lines:
line = raw_line.strip()
if not line:
continue
if _is_action_line(line):
cleaned = BULLET_PREFIX_PATTERN.sub("", line)
cleaned = cleaned.strip()
# Trim common checkbox markers
cleaned = cleaned.removeprefix("[ ]").strip()
cleaned = cleaned.removeprefix("[todo]").strip()
extracted.append(cleaned)
# Fallback: if nothing matched, heuristically split into sentences and pick imperative-like ones
if not extracted:
sentences = re.split(r"(?<=[.!?])\s+", text.strip())
for sentence in sentences:
s = sentence.strip()
if not s:
continue
if _looks_imperative(s):
extracted.append(s)
# Deduplicate while preserving order
seen: set[str] = set()
unique: List[str] = []
for item in extracted:
lowered = item.lower()
if lowered in seen:
continue
seen.add(lowered)
unique.append(item)
return unique
def _looks_imperative(sentence: str) -> bool:
words = re.findall(r"[A-Za-z']+", sentence)
if not words:
return False
first = words[0]
# Crude heuristic: treat these as imperative starters
imperative_starters = {
"add",
"create",
"implement",
"fix",
"update",
"write",
"check",
"verify",
"refactor",
"document",
"design",
"investigate",
}
return first.lower() in imperative_starters
-78
View File
@@ -1,78 +0,0 @@
# Week 2 – Action Item Extractor
This week, we will be expanding upon a minimal FastAPI + SQLite app that converts free‑form notes into enumerated action items.
***We recommend reading this entire document before getting started.***
Tip: To preview this markdown file
- On Mac, press `Command (⌘) + Shift + V`
- On Windows/Linux, press `Ctrl + Shift + V`
## Getting Started
### Cursor Set Up
Follow these instructions to set up Cursor and open your project:
1. Redeem your free year of Cursor Pro: https://cursor.com/students
2. Download Cursor: https://cursor.com/download
3. To enable the Cursor command line tool, open Cursor and press `Command (⌘) + Shift+ P` for Mac users (or `Ctrl + Shift + P` for non-Mac users) to open the Command Palette. Type: `Shell Command: Install 'cursor' command`. Select it and hit Enter.
4. Open a new terminal window, navigate to your project root, and run: `cursor .`
### Current Application
Here's how you can start running the current starter application:
1. Activate your conda environment.
```
conda activate cs146s
```
2. From the project root, run the server:
```
poetry run uvicorn week2.app.main:app --reload
```
3. Open a web browser and navigate to http://127.0.0.1:8000/.
4. Familiarize yourself with the current state of the application. Make sure you can successfully input notes and produce the extracted action item checklist.
## Exercises
For each exercise, use Cursor to help you implement the specified improvements to the current action item extractor application.
As you work through the assignment, use `writeup.md` to document your progress. Be sure to include the prompts you use, as well as any changes made by you or Cursor. We will be grading based on the contents of the write-up. Please also include comments throughout your code to document your changes.
### TODO 1: Scaffold a New Feature
Analyze the existing `extract_action_items()` function in `week2/app/services/extract.py`, which currently extracts action items using predefined heuristics.
Your task is to implement an **LLM-powered** alternative, `extract_action_items_llm()`, that utilizes Ollama to perform action item extraction via a large language model.
Some tips:
- To produce structured outputs (i.e. JSON array of strings), refer to this documentation: https://ollama.com/blog/structured-outputs
- To browse available Ollama models, refer to this documentation: https://ollama.com/library. Note that larger models will be more resource-intensive, so start small. To pull and run a model: `ollama run {MODEL_NAME}`
### TODO 2: Add Unit Tests
Write unit tests for `extract_action_items_llm()` covering multiple inputs (e.g., bullet lists, keyword-prefixed lines, empty input) in `week2/tests/test_extract.py`.
### TODO 3: Refactor Existing Code for Clarity
Perform a refactor of the code in the backend, focusing in particular on well-defined API contracts/schemas, database layer cleanup, app lifecycle/configuration, error handling.
### TODO 4: Use Agentic Mode to Automate Small Tasks
1. Integrate the LLM-powered extraction as a new endpoint. Update the frontend to include an "Extract LLM" button that, when clicked, triggers the extraction process via the new endpoint.
2. Expose one final endpoint to retrieve all notes. Update the frontend to include a "List Notes" button that, when clicked, fetches and displays them.
### TODO 5: Generate a README from the Codebase
***Learning Goal:***
*Students learn how AI can introspect a codebase and produce documentation automatically, showcasing Cursor’s ability to parse code context and translate it into human‑readable form.*
Use Cursor to analyze the current codebase and generate a well-structured `README.md` file. The README should include, at a minimum:
- A brief overview of the project
- How to set up and run the project
- API endpoints and functionality
- Instructions for running the test suite
## Deliverables
Fill out `week2/writeup.md` according to the instructions provided. Make sure all your changes are documented in your codebase.
## Evaluation rubric (100 pts total)
- 20 points per part 1-5 (10 for the generated code and 10 for each prompt).
-74
View File
@@ -1,74 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Action Item Extractor</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif; margin: 2rem auto; max-width: 800px; padding: 0 1rem; }
h1 { font-size: 1.5rem; }
textarea { width: 100%; min-height: 160px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace; }
button { padding: 0.5rem 1rem; }
.items { margin-top: 1rem; }
.item { display: flex; align-items: center; gap: 0.5rem; padding: 0.25rem 0; }
.muted { color: #6b7280; font-size: 0.875rem; }
.row { display: flex; gap: 0.5rem; align-items: center; }
</style>
</head>
<body>
<h1>Action Item Extractor</h1>
<p class="muted">Paste notes and extract actionable items. Minimal raw HTML frontend.</p>
<label for="text">Notes</label>
<textarea id="text" placeholder="Paste notes here...&#10;e.g.&#10;- [ ] Set up database&#10;- Implement extract endpoint"></textarea>
<div class="row">
<label class="row"><input id="save_note" type="checkbox" checked /> Save as note</label>
<button id="extract">Extract</button>
</div>
<div class="items" id="items"></div>
<script>
const $ = (sel) => document.querySelector(sel);
const itemsEl = $('#items');
const btn = $('#extract');
btn.addEventListener('click', async () => {
const text = $('#text').value;
const save = $('#save_note').checked;
itemsEl.textContent = 'Extracting...';
try {
const res = await fetch('/action-items/extract', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, save_note: save }),
});
if (!res.ok) throw new Error('Request failed');
const data = await res.json();
if (!data.items || data.items.length === 0) {
itemsEl.innerHTML = '<p class="muted">No action items found.</p>';
return;
}
itemsEl.innerHTML = data.items.map(it => (
`<div class="item"><input type="checkbox" data-id="${it.id}" /> <span>${it.text}</span></div>`
)).join('');
itemsEl.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', async (e) => {
const id = e.target.getAttribute('data-id');
await fetch(`/action-items/${id}/done`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ done: e.target.checked }),
});
});
});
} catch (err) {
console.error(err);
itemsEl.textContent = 'Error extracting items';
}
});
</script>
</body>
</html>
-3
View File
@@ -1,3 +0,0 @@
__all__ = []
-19
View File
@@ -1,19 +0,0 @@
import os
import pytest
from ..app.services.extract import extract_action_items
def test_extract_bullets_and_checkboxes():
text = """
Notes from meeting:
- [ ] Set up database
* implement API extract endpoint
1. Write tests
Some narrative sentence.
""".strip()
items = extract_action_items(text)
assert "Set up database" in items
assert "implement API extract endpoint" in items
assert "Write tests" in items
-83
View File
@@ -1,83 +0,0 @@
# Week 2 Write-up
Tip: To preview this markdown file
- On Mac, press `Command (⌘) + Shift + V`
- On Windows/Linux, press `Ctrl + Shift + V`
## INSTRUCTIONS
Fill out all of the `TODO`s in this file.
## SUBMISSION DETAILS
Name: **TODO** \
SUNet ID: **TODO** \
Citations: **TODO**
This assignment took me about **TODO** hours to do.
## YOUR RESPONSES
For each exercise, please include what prompts you used to generate the answer, in addition to the location of the generated response. Make sure to clearly add comments in your code documenting which parts are generated.
### Exercise 1: Scaffold a New Feature
Prompt:
```
TODO
```
Generated Code Snippets:
```
TODO: List all modified code files with the relevant line numbers.
```
### Exercise 2: Add Unit Tests
Prompt:
```
TODO
```
Generated Code Snippets:
```
TODO: List all modified code files with the relevant line numbers.
```
### Exercise 3: Refactor Existing Code for Clarity
Prompt:
```
TODO
```
Generated/Modified Code Snippets:
```
TODO: List all modified code files with the relevant line numbers. (We anticipate there may be multiple scattered changes here – just produce as comprehensive of a list as you can.)
```
### Exercise 4: Use Agentic Mode to Automate a Small Task
Prompt:
```
TODO
```
Generated Code Snippets:
```
TODO: List all modified code files with the relevant line numbers.
```
### Exercise 5: Generate a README from the Codebase
Prompt:
```
TODO
```
Generated Code Snippets:
```
TODO: List all modified code files with the relevant line numbers.
```
## SUBMISSION INSTRUCTIONS
1. Hit a `Command (⌘) + F` (or `Ctrl + F`) to find any remaining `TODO`s in this file. If no results are found, congratulations – you've completed all required fields.
2. Make sure you have all changes pushed to your remote repository for grading.
3. Submit via Gradescope.
-52
View File
@@ -1,52 +0,0 @@
# Week 3 — Build a Custom MCP Server
Design and implement a Model Context Protocol (MCP) server that wraps a real external API. You may:
- Run it **locally** (STDIO transport) and integrate with an MCP client (like Claude Desktop).
- Or run it **remotely** (HTTP transport) and call it from a model agent or client. This is harder but earns extra credit.
Bonus points for adding authentication (API keys or OAuth2) aligned with the MCP Authorization spec.
## Learning goals
- Understand core MCP capabilities: tools, resources, prompts.
- Implement tool definitions with typed parameters and robust error handling.
- Follow logging and transport best practices (no stdout for STDIO servers).
- Optionally implement authorization flows for HTTP transports.
## Requirements
1. Choose an external API and document which endpoints you’ll use. Examples: weather, GitHub issues, Notion pages, movie/TV databases, calendar, task managers, finance/crypto, travel, sports stats.
2. Expose at least two MCP tools
3. Implement basic resilience:
- Graceful errors for HTTP failures, timeouts, and empty results.
- Respect API rate limits (e.g., simple backoff or user-facing warning).
4. Packaging and docs:
- Provide clear setup instructions, environment variables, and run commands.
- Include an example invocation flow (what to type/click in the client to trigger the tools).
5. Choose one deployment mode:
- Local: STDIO server, runnable from your machine and discoverable by Claude Desktop or an AI IDE like Cursor.
- Remote: HTTP server accessible over the network, callable by an MCP-aware client or an agent runtime. Extra credit if deployed and reachable.
6. (Optional) Bonus: Authentication
- API key support via environment variable and client configuration; or
- OAuth2-style bearer tokens for HTTP transport, validating token audience and never passing tokens through to upstream APIs.
## Deliverables
- Source code under `week3/` (suggested: `week3/server/` with a clear entrypoint like `main.py` or `app.py`).
- `week3/README.md` with:
- Prerequisites, environment setup, and run instructions (local and/or remote).
- How to configure the MCP client (Claude Desktop example for local) or agent runtime for remote.
- Tool reference: names, parameters, example inputs/outputs, and expected behaviors.
## Evaluation rubric (90 pts total)
- Functionality (35): Implements 2+ tools, correct API integration, meaningful outputs.
- Reliability (20): Input validation, error handling, logging, rate-limit awareness.
- Developer experience (20): Clear setup/docs, easy to run locally; sensible folder structure.
- Code quality (15): Readable code, descriptive names, minimal complexity, type hints where applicable.
- Extra credit (10):
- +5 Remote HTTP MCP server, callable by an agent/client such as the OpenAI/Claude SDK.
- +5 Auth implemented correctly (API key or OAuth2 with audience validation).
## Helpful references
- MCP Server Quickstart: [modelcontextprotocol.io/quickstart/server](https://modelcontextprotocol.io/quickstart/server).
*NOTE: You may not submit this exact example.*
- MCP Authorization (HTTP): [modelcontextprotocol.io/specification/2025-06-18/basic/authorization](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
- Remote MCP on Cloudflare (Agents): [developers.cloudflare.com/agents/guides/remote-mcp-server/](https://developers.cloudflare.com/agents/guides/remote-mcp-server/). Use the modelcontextprotocol inspector tool to debug your server locally before deploying.
- https://vercel.com/docs/mcp/deploy-mcp-servers-to-vercel If you choose to do a remote MCP deployment, Vercel is a good option with a free tier.
-17
View File
@@ -1,17 +0,0 @@
.PHONY: run test format lint seed
run:
PYTHONPATH=. uvicorn backend.app.main:app --reload --host $${HOST:-127.0.0.1} --port $${PORT:-8000}
test:
PYTHONPATH=. pytest -q backend/tests
format:
black .
ruff check . --fix
lint:
ruff check .
seed:
PYTHONPATH=. python -c "from backend.app.db import apply_seed_if_needed; apply_seed_if_needed()"
-159
View File
@@ -1,159 +0,0 @@
# Week 4 — The Autonomous Coding Agent IRL
> ***We recommend reading this entire document before getting started.***
This week, your task is to build at least **2 automations** within the context of this repository using any combination of the following **Claude Code** features:
- Custom slash commands (checked into `.claude/commands/*.md`)
- `CLAUDE.md` files for repository or context guidance
- Claude SubAgents (role-specialized agents working together)
- MCP servers integrated into Claude Code
Your automations should meaningfully improve a developer workflow – for example, by streamlining tests, documentation, refactors, or data-related tasks. You will then use the automations you create to expand upon the starter application found in `week4/`.
## Learn about Claude Code
To gain a deeper understanding of Claude Code and explore your automation options, please read through the following two resources:
1. **Claude Code best practices:** [anthropic.com/engineering/claude-code-best-practices](https://www.anthropic.com/engineering/claude-code-best-practices)
2. **SubAgents overview:** [docs.anthropic.com/en/docs/claude-code/sub-agents](https://docs.anthropic.com/en/docs/claude-code/sub-agents)
## Explore the Starter Application
Minimal full‑stack starter application designed to be a **"developer's command center"**.
- FastAPI backend with SQLite (SQLAlchemy)
- Static frontend (no Node toolchain needed)
- Minimal tests (pytest)
- Pre-commit (black + ruff)
- Tasks to practice agent-driven workflows
Use this application as your playground to experiment with the Claude automations you build.
### Structure
```
backend/ # FastAPI app
frontend/ # Static UI served by FastAPI
data/ # SQLite DB + seed
docs/ # TASKS for agent-driven workflows
```
### Quickstart
1) Activate your conda environment.
```bash
conda activate cs146s
```
2) (Optional) Install pre-commit hooks
```bash
pre-commit install
```
3) Run the app (from `week4/` directory)
```bash
make run
```
4) Open `http://localhost:8000` for the frontend and `http://localhost:8000/docs` for the API docs.
5) Play around with the starter application to get a feel for its current features and functionality.
### Testing
Run the tests (from `week4/` directory)
```bash
make test
```
### Formatting/Linting
```bash
make format
make lint
```
## Part I: Build Your Automation (Choose 2 or more)
Now that you’re familiar with the starter application, your next step is to build automations to enhance or extend it. Below are several automation options you can choose from. You can mix and match across categories.
As you build your automations, document your changes in the `writeup.md` file. Leave the *"How you used the automation to enhance the starter application"* section empty for now - you will be returning to this in Part II of the assignment.
### A) Claude custom slash commands
Slash commands are a feature for repeated workflows, letting you create reusable workflows in Markdown files inside `.claude/commands/`. Claude exposes these via `/`.
- Example 1: Test runner with coverage
- Name: `tests.md`
- Intent: Run `pytest -q backend/tests --maxfail=1 -x` and, if green, run coverage.
- Inputs: Optional marker or path.
- Output: Summarize failures and suggest next steps.
- Example 2: Docs sync
- Name: `docs-sync.md`
- Intent: Read `/openapi.json`, update `docs/API.md`, and list route deltas.
- Output: Diff-like summary and TODOs.
- Example 3: Refactor harness
- Name: `refactor-module.md`
- Intent: Rename a module (e.g., `services/extract.py` → `services/parser.py`), update imports, run lint/tests.
- Output: A checklist of modified files and verification steps.
>*Tips: Keep commands focused, use `$ARGUMENTS`, and prefer idempotent steps. Consider allowlisting safe tools and using headless mode for repeatability.*
### B) `CLAUDE.md` guidance files
The `CLAUDE.md` file is automatically read when starting a conversation, allowing you to provide repository-specific instructions, context, or guidance that influence Claude's behavior. Create a `CLAUDE.md` in the repo root (and optionally in `week4/` subfolders) to guide Claude’s behavior.
- Example 1: Code navigation and entry points
- Include: How to run the app, where routers live (`backend/app/routers`), where tests live, how the DB is seeded.
- Example 2: Style and safety guardrails
- Include: Tooling expectations (black/ruff), safe commands to run, commands to avoid, and lint/test gates.
- Example 3: Workflow snippets
- Include: “When asked to add an endpoint, first write a failing test, then implement, then run pre-commit.”
> *Tips: Iterate on `CLAUDE.md` like a prompt, keep it concise and actionable, and document custom tools/scripts you expect Claude to use.*
### C) SubAgents (role-specialized)
SubAgents are specialized AI assistants configured to handle specific tasks with their own system prompts, tools, and context. Design two or more cooperating agents, each responsible for a distinct step in a single workflow.
- Example 1: TestAgent + CodeAgent
- Flow: TestAgent writes/updates tests for a change → CodeAgent implements code to pass tests → TestAgent verifies.
- Example 2: DocsAgent + CodeAgent
- Flow: CodeAgent adds a new API route → DocsAgent updates `API.md` and `TASKS.md` and checks drift against `/openapi.json`.
- Example 3: DBAgent + RefactorAgent
- Flow: DBAgent proposes a schema change (adjust `data/seed.sql`) → RefactorAgent updates models/schemas/routers and fixes lints.
>*Tips: Use checklists/scratchpads, reset context (`/clear`) between roles, and run agents in parallel for independent tasks.*
## Part II: Put Your Automations to Work
Now that you’ve built 2+ automations, let's put them to use! In the `writeup.md` under section *"How you used the automation to enhance the starter application"*, describe how you leveraged each automation to improve or extend the app’s functionality.
e.g. If you implemented the custom slash command `/generate-test-cases`, explain how you used it to interact with and test the starter application.
## Deliverables
1) Two or more automations, which may include:
- Slash commands in `.claude/commands/*.md`
- `CLAUDE.md` files
- SubAgent prompts/configuration (documented clearly, files/scripts if any)
2) A write-up `writeup.md` under `week4/` that includes:
- Design inspiration (e.g. cite the best-practices and/or sub-agents docs)
- Design of each automation, including goals, inputs/outputs, steps
- How to run it (exact commands), expected outputs, and rollback/safety notes
- Before vs. after (i.e. manual workflow vs. automated workflow)
- How you used the automation to enhance the starter application
## SUBMISSION INSTRUCTIONS
1. Make sure you have all changes pushed to your remote repository for grading.
2. **Make sure you've added both brentju and febielin as collaborators on your assignment repository.**
2. Submit via Gradescope.
-1
View File
@@ -1 +0,0 @@
"""Backend package (week4)."""
-1
View File
@@ -1 +0,0 @@
"""Application package for FastAPI backend (week4)."""
-56
View File
@@ -1,56 +0,0 @@
import os
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from dotenv import load_dotenv
from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session, sessionmaker
load_dotenv()
DEFAULT_DB_PATH = os.getenv("DATABASE_PATH", "./data/app.db")
engine = create_engine(f"sqlite:///{DEFAULT_DB_PATH}", connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db() -> Iterator[Session]:
session: Session = SessionLocal()
try:
yield session
session.commit()
except Exception: # noqa: BLE001
session.rollback()
raise
finally:
session.close()
@contextmanager
def get_session() -> Iterator[Session]:
session = SessionLocal()
try:
yield session
session.commit()
except Exception: # noqa: BLE001
session.rollback()
raise
finally:
session.close()
def apply_seed_if_needed() -> None:
db_path = Path(DEFAULT_DB_PATH)
db_path.parent.mkdir(parents=True, exist_ok=True)
newly_created = not db_path.exists()
if newly_created:
db_path.touch()
seed_file = Path("./data/seed.sql")
if newly_created and seed_file.exists():
with engine.begin() as conn:
sql = seed_file.read_text()
if sql.strip():
for statement in [s.strip() for s in sql.split(";") if s.strip()]:
conn.execute(text(statement))
-34
View File
@@ -1,34 +0,0 @@
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from .db import apply_seed_if_needed, engine
from .models import Base
from .routers import action_items as action_items_router
from .routers import notes as notes_router
app = FastAPI(title="Modern Software Dev Starter (Week 4)")
# Ensure data dir exists
Path("data").mkdir(parents=True, exist_ok=True)
# Mount static frontend
app.mount("/static", StaticFiles(directory="frontend"), name="static")
@app.on_event("startup")
def startup_event() -> None:
Base.metadata.create_all(bind=engine)
apply_seed_if_needed()
@app.get("/")
async def root() -> FileResponse:
return FileResponse("frontend/index.html")
# Routers
app.include_router(notes_router.router)
app.include_router(action_items_router.router)
-20
View File
@@ -1,20 +0,0 @@
from sqlalchemy import Boolean, Column, Integer, String, Text
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class Note(Base):
__tablename__ = "notes"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
content = Column(Text, nullable=False)
class ActionItem(Base):
__tablename__ = "action_items"
id = Column(Integer, primary_key=True, index=True)
description = Column(Text, nullable=False)
completed = Column(Boolean, default=False, nullable=False)
-1
View File
@@ -1 +0,0 @@
"""API routers package (week4)."""
-36
View File
@@ -1,36 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session
from ..db import get_db
from ..models import ActionItem
from ..schemas import ActionItemCreate, ActionItemRead
router = APIRouter(prefix="/action-items", tags=["action_items"])
@router.get("/", response_model=list[ActionItemRead])
def list_items(db: Session = Depends(get_db)) -> list[ActionItemRead]:
rows = db.execute(select(ActionItem)).scalars().all()
return [ActionItemRead.model_validate(row) for row in rows]
@router.post("/", response_model=ActionItemRead, status_code=201)
def create_item(payload: ActionItemCreate, db: Session = Depends(get_db)) -> ActionItemRead:
item = ActionItem(description=payload.description, completed=False)
db.add(item)
db.flush()
db.refresh(item)
return ActionItemRead.model_validate(item)
@router.put("/{item_id}/complete", response_model=ActionItemRead)
def complete_item(item_id: int, db: Session = Depends(get_db)) -> ActionItemRead:
item = db.get(ActionItem, item_id)
if not item:
raise HTTPException(status_code=404, detail="Action item not found")
item.completed = True
db.add(item)
db.flush()
db.refresh(item)
return ActionItemRead.model_validate(item)
-47
View File
@@ -1,47 +0,0 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session
from ..db import get_db
from ..models import Note
from ..schemas import NoteCreate, NoteRead
router = APIRouter(prefix="/notes", tags=["notes"])
@router.get("/", response_model=list[NoteRead])
def list_notes(db: Session = Depends(get_db)) -> list[NoteRead]:
rows = db.execute(select(Note)).scalars().all()
return [NoteRead.model_validate(row) for row in rows]
@router.post("/", response_model=NoteRead, status_code=201)
def create_note(payload: NoteCreate, db: Session = Depends(get_db)) -> NoteRead:
note = Note(title=payload.title, content=payload.content)
db.add(note)
db.flush()
db.refresh(note)
return NoteRead.model_validate(note)
@router.get("/search/", response_model=list[NoteRead])
def search_notes(q: Optional[str] = None, db: Session = Depends(get_db)) -> list[NoteRead]:
if not q:
rows = db.execute(select(Note)).scalars().all()
else:
rows = (
db.execute(select(Note).where((Note.title.contains(q)) | (Note.content.contains(q))))
.scalars()
.all()
)
return [NoteRead.model_validate(row) for row in rows]
@router.get("/{note_id}", response_model=NoteRead)
def get_note(note_id: int, db: Session = Depends(get_db)) -> NoteRead:
note = db.get(Note, note_id)
if not note:
raise HTTPException(status_code=404, detail="Note not found")
return NoteRead.model_validate(note)
-28
View File
@@ -1,28 +0,0 @@
from pydantic import BaseModel
class NoteCreate(BaseModel):
title: str
content: str
class NoteRead(BaseModel):
id: int
title: str
content: str
class Config:
from_attributes = True
class ActionItemCreate(BaseModel):
description: str
class ActionItemRead(BaseModel):
id: int
description: str
completed: bool
class Config:
from_attributes = True
-3
View File
@@ -1,3 +0,0 @@
def extract_action_items(text: str) -> list[str]:
lines = [line.strip("- ") for line in text.splitlines() if line.strip()]
return [line for line in lines if line.endswith("!") or line.lower().startswith("todo:")]
-39
View File
@@ -1,39 +0,0 @@
import os
import tempfile
from collections.abc import Generator
import pytest
from backend.app.db import get_db
from backend.app.main import app
from backend.app.models import Base
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture()
def client() -> Generator[TestClient, None, None]:
db_fd, db_path = tempfile.mkstemp()
os.close(db_fd)
engine = create_engine(f"sqlite:///{db_path}", connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
def override_get_db():
session = TestingSessionLocal()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as c:
yield c
os.unlink(db_path)
-16
View File
@@ -1,16 +0,0 @@
def test_create_and_complete_action_item(client):
payload = {"description": "Ship it"}
r = client.post("/action-items/", json=payload)
assert r.status_code == 201, r.text
item = r.json()
assert item["completed"] is False
r = client.put(f"/action-items/{item['id']}/complete")
assert r.status_code == 200
done = r.json()
assert done["completed"] is True
r = client.get("/action-items/")
assert r.status_code == 200
items = r.json()
assert len(items) == 1
-13
View File
@@ -1,13 +0,0 @@
from backend.app.services.extract import extract_action_items
def test_extract_action_items():
text = """
This is a note
- TODO: write tests
- Ship it!
Not actionable
""".strip()
items = extract_action_items(text)
assert "TODO: write tests" in items
assert "Ship it!" in items
-19
View File
@@ -1,19 +0,0 @@
def test_create_and_list_notes(client):
payload = {"title": "Test", "content": "Hello world"}
r = client.post("/notes/", json=payload)
assert r.status_code == 201, r.text
data = r.json()
assert data["title"] == "Test"
r = client.get("/notes/")
assert r.status_code == 200
items = r.json()
assert len(items) >= 1
r = client.get("/notes/search/")
assert r.status_code == 200
r = client.get("/notes/search/", params={"q": "Hello"})
assert r.status_code == 200
items = r.json()
assert len(items) >= 1
-19
View File
@@ -1,19 +0,0 @@
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS action_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL,
completed BOOLEAN NOT NULL DEFAULT 0
);
INSERT INTO notes (title, content) VALUES
('Welcome', 'This is a starter note. TODO: explore the app!'),
('Demo', 'Click around and add a note. Ship feature!');
INSERT INTO action_items (description, completed) VALUES
('Try pre-commit', 0),
('Run tests', 0);
-34
View File
@@ -1,34 +0,0 @@
# Tasks for Repo
## 1) Enable pre-commit and fix the repo
- Install hooks: `pre-commit install`
- Run: `pre-commit run --all-files`
- Fix any formatting/lint issues (black/ruff)
## 2) Add search endpoint for notes
- Add/extend `GET /notes/search?q=...` (case-insensitive) using SQLAlchemy filters
- Update `frontend/app.js` to use the search query
- Add tests in `backend/tests/test_notes.py`
## 3) Complete action item flow
- Implement `PUT /action-items/{id}/complete` (already scaffolded)
- Update UI to reflect completion (already wired) and extend test coverage
## 4) Improve extraction logic
- Extend `backend/app/services/extract.py` to parse tags like `#tag` and return them
- Add tests for the new parsing behavior
- (Optional) Expose `POST /notes/{id}/extract` that turns notes into action items
## 5) Notes CRUD enhancements
- Add `PUT /notes/{id}` to edit a note (title/content)
- Add `DELETE /notes/{id}` to delete a note
- Update `frontend/app.js` to support edit/delete; add tests
## 6) Request validation and error handling
- Add simple validation rules (e.g., min lengths) to `schemas.py`
- Return informative 400/404 errors where appropriate; add tests for validation failures
## 7) Docs drift check (manual for now)
- Create/maintain a simple `API.md` describing endpoints and payloads
- After each change, verify docs match actual OpenAPI (`/openapi.json`)
-66
View File
@@ -1,66 +0,0 @@
async function fetchJSON(url, options) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
async function loadNotes() {
const list = document.getElementById('notes');
list.innerHTML = '';
const notes = await fetchJSON('/notes/');
for (const n of notes) {
const li = document.createElement('li');
li.textContent = `${n.title}: ${n.content}`;
list.appendChild(li);
}
}
async function loadActions() {
const list = document.getElementById('actions');
list.innerHTML = '';
const items = await fetchJSON('/action-items/');
for (const a of items) {
const li = document.createElement('li');
li.textContent = `${a.description} [${a.completed ? 'done' : 'open'}]`;
if (!a.completed) {
const btn = document.createElement('button');
btn.textContent = 'Complete';
btn.onclick = async () => {
await fetchJSON(`/action-items/${a.id}/complete`, { method: 'PUT' });
loadActions();
};
li.appendChild(btn);
}
list.appendChild(li);
}
}
window.addEventListener('DOMContentLoaded', () => {
document.getElementById('note-form').addEventListener('submit', async (e) => {
e.preventDefault();
const title = document.getElementById('note-title').value;
const content = document.getElementById('note-content').value;
await fetchJSON('/notes/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
e.target.reset();
loadNotes();
});
document.getElementById('action-form').addEventListener('submit', async (e) => {
e.preventDefault();
const description = document.getElementById('action-desc').value;
await fetchJSON('/action-items/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description }),
});
e.target.reset();
loadActions();
});
loadNotes();
loadActions();
});
-35
View File
@@ -1,35 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Modern Software Dev Starter</title>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<main>
<h1>Modern Software Dev Starter</h1>
<section>
<h2>Notes</h2>
<form id="note-form">
<input id="note-title" placeholder="Title" required />
<input id="note-content" placeholder="Content" required />
<button type="submit">Add</button>
</form>
<ul id="notes"></ul>
</section>
<section>
<h2>Action Items</h2>
<form id="action-form">
<input id="action-desc" placeholder="Description" required />
<button type="submit">Add</button>
</form>
<ul id="actions"></ul>
</section>
</main>
<script src="/static/app.js"></script>
</body>
</html>
-8
View File
@@ -1,8 +0,0 @@
body{font-family:system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;margin:0;padding:0;background:#fafafa;color:#111}
main{max-width:900px;margin:2rem auto;padding:0 1rem}
h1{font-size:1.8rem}
section{background:#fff;border:1px solid #eee;border-radius:8px;padding:1rem;margin:1rem 0}
form{display:flex;gap:.5rem;margin-bottom:.5rem}
input{flex:1;padding:.5rem;border:1px solid #ccc;border-radius:4px}
button{padding:.5rem .8rem;border:1px solid #ccc;border-radius:4px;background:#f5f5f5;cursor:pointer}
ul{list-style:disc;padding-left:1.25rem}
-15
View File
@@ -1,15 +0,0 @@
repos:
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.8
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
-70
View File
@@ -1,70 +0,0 @@
# Week 4 Write-up
Tip: To preview this markdown file
- On Mac, press `Command (⌘) + Shift + V`
- On Windows/Linux, press `Ctrl + Shift + V`
## INSTRUCTIONS
Fill out all of the `TODO`s in this file.
## SUBMISSION DETAILS
Name: **TODO** \
SUNet ID: **TODO** \
Citations: **TODO**
This assignment took me about **TODO** hours to do.
## YOUR RESPONSES
### Automation #1
a. Design inspiration (e.g. cite the best-practices and/or sub-agents docs)
> TODO
b. Design of each automation, including goals, inputs/outputs, steps
> TODO
c. How to run it (exact commands), expected outputs, and rollback/safety notes
> TODO
d. Before vs. after (i.e. manual workflow vs. automated workflow)
> TODO
e. How you used the automation to enhance the starter application
> TODO
### Automation #2
a. Design inspiration (e.g. cite the best-practices and/or sub-agents docs)
> TODO
b. Design of each automation, including goals, inputs/outputs, steps
> TODO
c. How to run it (exact commands), expected outputs, and rollback/safety notes
> TODO
d. Before vs. after (i.e. manual workflow vs. automated workflow)
> TODO
e. How you used the automation to enhance the starter application
> TODO
### *(Optional) Automation #3*
*If you choose to build additional automations, feel free to detail them here!*
a. Design inspiration (e.g. cite the best-practices and/or sub-agents docs)
> TODO
b. Design of each automation, including goals, inputs/outputs, steps
> TODO
c. How to run it (exact commands), expected outputs, and rollback/safety notes
> TODO
d. Before vs. after (i.e. manual workflow vs. automated workflow)
> TODO
e. How you used the automation to enhance the starter application
> TODO
-17
View File
@@ -1,17 +0,0 @@
.PHONY: run test format lint seed
run:
PYTHONPATH=. uvicorn backend.app.main:app --reload --host $${HOST:-127.0.0.1} --port $${PORT:-8000}
test:
PYTHONPATH=. pytest -q backend/tests
format:
black .
ruff check . --fix
lint:
ruff check .
seed:
PYTHONPATH=. python -c "from backend.app.db import apply_seed_if_needed; apply_seed_if_needed()"
-59
View File
@@ -1,59 +0,0 @@
# Week 5
Minimal full‑stack starter for experimenting with autonomous coding agents.
- FastAPI backend with SQLite (SQLAlchemy)
- Static frontend (no Node toolchain needed)
- Minimal tests (pytest)
- Pre-commit (black + ruff)
- Tasks to practice agent-driven workflows
## Quickstart
1) Create and activate a virtualenv, then install dependencies
```bash
cd /Users/mihaileric/Documents/code/modern-software-dev-assignments
python -m venv .venv && source .venv/bin/activate
pip install -e .[dev]
```
2) (Optional) Install pre-commit hooks
```bash
pre-commit install
```
3) Run the app (from `week5/`)
```bash
cd week5 && make run
```
Open `http://localhost:8000` for the frontend and `http://localhost:8000/docs` for the API docs.
## Structure
```
backend/ # FastAPI app
frontend/ # Static UI served by FastAPI
data/ # SQLite DB + seed
docs/ # TASKS for agent-driven workflows
```
## Tests
```bash
cd week5 && make test
```
## Formatting/Linting
```bash
cd week5 && make format
cd week5 && make lint
```
## Configuration
Copy `.env.example` to `.env` (in `week5/`) to override defaults like the database path.
-118
View File
@@ -1,118 +0,0 @@
# Week 5 — Agentic Development with Warp
Use the app in `week5/` as your playground. This week mirrors the prior assignment but emphasizes the Warp agentic development environment and multi‑agent workflows.
## Learn about Warp
- Warp Agentic Development Environment: [warp.dev](https://www.warp.dev/)
- [Warp University](https://www.warp.dev/university?slug=university)
## Explore the Starter Application
Minimal full‑stack starter application.
- FastAPI backend with SQLite (SQLAlchemy)
- Static frontend (no Node toolchain needed)
- Minimal tests (pytest)
- Pre-commit (black + ruff)
- Tasks to practice agent-driven workflows
Use this application as your playground to experiment with the Warp automations you build.
### Structure
```
backend/ # FastAPI app
frontend/ # Static UI served by FastAPI
data/ # SQLite DB + seed
docs/ # TASKS for agent-driven workflows
```
### Quickstart
1) Activate your conda environment.
```bash
conda activate cs146s
```
2) (Optional) Install pre-commit hooks
```bash
pre-commit install
```
3) Run the app (from `week5/` directory)
```bash
make run
```
4) Open `http://localhost:8000` for the frontend and `http://localhost:8000/docs` for the API docs.
5) Play around with the starter application to get a feel for its current features and functionality.
### Testing
Run the tests (from `week5/` directory)
```bash
make test
```
### Formatting/Linting
```bash
make format
make lint
```
## Part I: Build Your Automation (Choose 2 or more)
Select tasks from `week5/docs/TASKS.md` to implement. Your implementation must leverage Warp in both of the following ways (more details below):
- A) Use Warp Drive features — such as saved prompts, rules, or MCP servers.
- (B) Incorporate multi-agent workflows within Warp.
Keep your changes focused on backend, frontend, logic, or tests inside `week5/`.
For each selected task, note its difficulty level.
### A) Warp Drive saved prompts, rules, MCP servers (REQUIRED: at least one)
Create one or more shareable Warp Drive prompts, rules, or MCP server integrations tailored to this repo. Examples:
- Test runner with coverage and flaky‑test re‑run
- Docs sync: generate/update `docs/API.md` from `/openapi.json`, list route deltas
- Refactor harness: rename a module, update imports, run lint/tests
- Release helper: bump versions, run checks, prepare a changelog snippet
- Integrate the Git MCP server to have Warp interact with Git autonomously (creating branches, commits, PR notes, etc)
>*Tips: keep workflows focused, pass arguments, make them idempotent, and prefer headless/non‑interactive steps where possible.*
### B) Multi‑agent workflows in Warp (REQUIRED: at least one)
Run a multi‑agent session where separate agents in different Warp tabs handle independent tasks concurrently.
- Perform multiple self-contained tasks from `TASKS.md` in separate Warp tabs using concurrent agents. Challenge: how many agents can you have working simultaneously?
>*Tips: [git worktree](https://git-scm.com/docs/git-worktree) may be helpful here to keep agents from clobbering over each other.*
## Part II: Put Your Automations to Work
Now that you’ve built 2+ automations, let's put them to use! In the `writeup.md` under section *"How you used the automation (what pain point it resolves or accelerates)"*, describe how you leveraged each automation to improve some workflow.
## Constraints and scope
Work strictly in `week5/` (backend, frontend, logic, tests). Avoid changing other weeks unless the automation explicitly requires it and you document why.
## Deliverables
1) Two or more Warp automations, which may include:
- Warp Drive workflows/rules (share links and/or exported definitions) and any helper scripts
- Any supplemental prompts/playbooks used to coordinate multiple agents
2) A write‑up `writeup.md` under `week5/` that includes:
- Design of each automation, including goals, inputs/outputs, steps
- Before vs. after (i.e. manual workflow vs. automated workflow)
- Autonomy levels used for each completed task (which code permissions, why, and how you supervised)
- (if applicable) Multi‑agent notes: roles, coordination strategy, and concurrency wins/risks/failures
- How you used the automation (what pain point it resolves or accelerates)
## SUBMISSION INSTRUCTIONS
1. Make sure you have all changes pushed to your remote repository for grading.
2. **Make sure you've added both brentju and febielin as collaborators on your assignment repository.**
2. Submit via Gradescope.
-1
View File
@@ -1 +0,0 @@
"""Backend package (week5)."""
-1
View File
@@ -1 +0,0 @@
"""Application package for FastAPI backend (week5)."""
-56
View File
@@ -1,56 +0,0 @@
import os
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from dotenv import load_dotenv
from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session, sessionmaker
load_dotenv()
DEFAULT_DB_PATH = os.getenv("DATABASE_PATH", "./data/app.db")
engine = create_engine(f"sqlite:///{DEFAULT_DB_PATH}", connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db() -> Iterator[Session]:
session: Session = SessionLocal()
try:
yield session
session.commit()
except Exception: # noqa: BLE001
session.rollback()
raise
finally:
session.close()
@contextmanager
def get_session() -> Iterator[Session]:
session = SessionLocal()
try:
yield session
session.commit()
except Exception: # noqa: BLE001
session.rollback()
raise
finally:
session.close()
def apply_seed_if_needed() -> None:
db_path = Path(DEFAULT_DB_PATH)
db_path.parent.mkdir(parents=True, exist_ok=True)
newly_created = not db_path.exists()
if newly_created:
db_path.touch()
seed_file = Path("./data/seed.sql")
if newly_created and seed_file.exists():
with engine.begin() as conn:
sql = seed_file.read_text()
if sql.strip():
for statement in [s.strip() for s in sql.split(";") if s.strip()]:
conn.execute(text(statement))
-34
View File
@@ -1,34 +0,0 @@
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from .db import apply_seed_if_needed, engine
from .models import Base
from .routers import action_items as action_items_router
from .routers import notes as notes_router
app = FastAPI(title="Modern Software Dev Starter (Week 5)")
# Ensure data dir exists
Path("data").mkdir(parents=True, exist_ok=True)
# Mount static frontend
app.mount("/static", StaticFiles(directory="frontend"), name="static")
@app.on_event("startup")
def startup_event() -> None:
Base.metadata.create_all(bind=engine)
apply_seed_if_needed()
@app.get("/")
async def root() -> FileResponse:
return FileResponse("frontend/index.html")
# Routers
app.include_router(notes_router.router)
app.include_router(action_items_router.router)
-20
View File
@@ -1,20 +0,0 @@
from sqlalchemy import Boolean, Column, Integer, String, Text
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class Note(Base):
__tablename__ = "notes"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
content = Column(Text, nullable=False)
class ActionItem(Base):
__tablename__ = "action_items"
id = Column(Integer, primary_key=True, index=True)
description = Column(Text, nullable=False)
completed = Column(Boolean, default=False, nullable=False)
-1
View File
@@ -1 +0,0 @@
"""API routers package (week5)."""
-36
View File
@@ -1,36 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session
from ..db import get_db
from ..models import ActionItem
from ..schemas import ActionItemCreate, ActionItemRead
router = APIRouter(prefix="/action-items", tags=["action_items"])
@router.get("/", response_model=list[ActionItemRead])
def list_items(db: Session = Depends(get_db)) -> list[ActionItemRead]:
rows = db.execute(select(ActionItem)).scalars().all()
return [ActionItemRead.model_validate(row) for row in rows]
@router.post("/", response_model=ActionItemRead, status_code=201)
def create_item(payload: ActionItemCreate, db: Session = Depends(get_db)) -> ActionItemRead:
item = ActionItem(description=payload.description, completed=False)
db.add(item)
db.flush()
db.refresh(item)
return ActionItemRead.model_validate(item)
@router.put("/{item_id}/complete", response_model=ActionItemRead)
def complete_item(item_id: int, db: Session = Depends(get_db)) -> ActionItemRead:
item = db.get(ActionItem, item_id)
if not item:
raise HTTPException(status_code=404, detail="Action item not found")
item.completed = True
db.add(item)
db.flush()
db.refresh(item)
return ActionItemRead.model_validate(item)
-47
View File
@@ -1,47 +0,0 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session
from ..db import get_db
from ..models import Note
from ..schemas import NoteCreate, NoteRead
router = APIRouter(prefix="/notes", tags=["notes"])
@router.get("/", response_model=list[NoteRead])
def list_notes(db: Session = Depends(get_db)) -> list[NoteRead]:
rows = db.execute(select(Note)).scalars().all()
return [NoteRead.model_validate(row) for row in rows]
@router.post("/", response_model=NoteRead, status_code=201)
def create_note(payload: NoteCreate, db: Session = Depends(get_db)) -> NoteRead:
note = Note(title=payload.title, content=payload.content)
db.add(note)
db.flush()
db.refresh(note)
return NoteRead.model_validate(note)
@router.get("/search/", response_model=list[NoteRead])
def search_notes(q: Optional[str] = None, db: Session = Depends(get_db)) -> list[NoteRead]:
if not q:
rows = db.execute(select(Note)).scalars().all()
else:
rows = (
db.execute(select(Note).where((Note.title.contains(q)) | (Note.content.contains(q))))
.scalars()
.all()
)
return [NoteRead.model_validate(row) for row in rows]
@router.get("/{note_id}", response_model=NoteRead)
def get_note(note_id: int, db: Session = Depends(get_db)) -> NoteRead:
note = db.get(Note, note_id)
if not note:
raise HTTPException(status_code=404, detail="Note not found")
return NoteRead.model_validate(note)
-28
View File
@@ -1,28 +0,0 @@
from pydantic import BaseModel
class NoteCreate(BaseModel):
title: str
content: str
class NoteRead(BaseModel):
id: int
title: str
content: str
class Config:
from_attributes = True
class ActionItemCreate(BaseModel):
description: str
class ActionItemRead(BaseModel):
id: int
description: str
completed: bool
class Config:
from_attributes = True
-3
View File
@@ -1,3 +0,0 @@
def extract_action_items(text: str) -> list[str]:
lines = [line.strip("- ") for line in text.splitlines() if line.strip()]
return [line for line in lines if line.endswith("!") or line.lower().startswith("todo:")]
-39
View File
@@ -1,39 +0,0 @@
import os
import tempfile
from collections.abc import Generator
import pytest
from backend.app.db import get_db
from backend.app.main import app
from backend.app.models import Base
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture()
def client() -> Generator[TestClient, None, None]:
db_fd, db_path = tempfile.mkstemp()
os.close(db_fd)
engine = create_engine(f"sqlite:///{db_path}", connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
def override_get_db():
session = TestingSessionLocal()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as c:
yield c
os.unlink(db_path)
-16
View File
@@ -1,16 +0,0 @@
def test_create_and_complete_action_item(client):
payload = {"description": "Ship it"}
r = client.post("/action-items/", json=payload)
assert r.status_code == 201, r.text
item = r.json()
assert item["completed"] is False
r = client.put(f"/action-items/{item['id']}/complete")
assert r.status_code == 200
done = r.json()
assert done["completed"] is True
r = client.get("/action-items/")
assert r.status_code == 200
items = r.json()
assert len(items) == 1
-13
View File
@@ -1,13 +0,0 @@
from backend.app.services.extract import extract_action_items
def test_extract_action_items():
text = """
This is a note
- TODO: write tests
- Ship it!
Not actionable
""".strip()
items = extract_action_items(text)
assert "TODO: write tests" in items
assert "Ship it!" in items
-19
View File
@@ -1,19 +0,0 @@
def test_create_and_list_notes(client):
payload = {"title": "Test", "content": "Hello world"}
r = client.post("/notes/", json=payload)
assert r.status_code == 201, r.text
data = r.json()
assert data["title"] == "Test"
r = client.get("/notes/")
assert r.status_code == 200
items = r.json()
assert len(items) >= 1
r = client.get("/notes/search/")
assert r.status_code == 200
r = client.get("/notes/search/", params={"q": "Hello"})
assert r.status_code == 200
items = r.json()
assert len(items) >= 1
-19
View File
@@ -1,19 +0,0 @@
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS action_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL,
completed BOOLEAN NOT NULL DEFAULT 0
);
INSERT INTO notes (title, content) VALUES
('Welcome', 'This is a starter note. TODO: explore the app!'),
('Demo', 'Click around and add a note. Ship feature!');
INSERT INTO action_items (description, completed) VALUES
('Try pre-commit', 0),
('Run tests', 0);
-86
View File
@@ -1,86 +0,0 @@
# Tasks for Repo
## 1) Migrate frontend to Vite + React (complex)
- Scaffold a Vite + React app in `week5/frontend/` (or a subfolder like `week5/frontend/ui/`).
- Replace the current static assets with a built bundle served by FastAPI:
- Build to `week5/frontend/dist/`.
- Update FastAPI static mount to serve `dist` and root (`/`) to `index.html` from `dist`.
- Wire existing endpoints in React:
- Notes list, create, delete, edit.
- Action items list, create, complete.
- Update `Makefile` with targets: `web-install`, `web-dev`, `web-build`, and ensure `make run` builds the web bundle automatically (or documents the workflow).
- Add component/unit tests (React Testing Library) for at least two components and integration tests in `backend/tests` for API compatibility.
## 2) Notes search with pagination and sorting (medium)
- Implement `GET /notes/search?q=...&page=1&page_size=10&sort=created_desc|title_asc`.
- Use case‑insensitive matching on title/content.
- Return a payload with `items`, `total`, `page`, `page_size`.
- Add SQLAlchemy query composition for filters, ordering, and pagination.
- Update React UI with a search input, result count, and next/prev pagination controls.
- Add tests in `backend/tests/test_notes.py` for query edge cases and pagination.
## 3) Full Notes CRUD with optimistic UI updates (medium)
- Add `PUT /notes/{id}` and `DELETE /notes/{id}`.
- In the frontend, update state optimistically while handling error rollbacks.
- Validate payloads in `schemas.py` (min lengths, max lengths where reasonable).
- Add tests for success and validation errors.
## 4) Action items: filters and bulk complete (medium)
- Add `GET /action-items?completed=true|false` to filter by completion.
- Add `POST /action-items/bulk-complete` that accepts a list of IDs and marks them completed in a transaction.
- Update the frontend with filter toggles and a bulk action UI.
- Add tests to cover filters, bulk behavior, and transactional rollback on error.
## 5) Tags feature with many‑to‑many relation (complex)
- Add `Tag` model and a join table `note_tags` (many‑to‑many between `Note` and `Tag`).
- Endpoints:
- `GET /tags`, `POST /tags`, `DELETE /tags/{id}`
- `POST /notes/{id}/tags` to attach, `DELETE /notes/{id}/tags/{tag_id}` to detach
- Update extraction (see next task) to auto‑create/attach tags from `#hashtags`.
- Update the UI to display tags as chips and filter notes by tag.
- Add tests for model relations and endpoint behavior.
## 6) Improve extraction logic and endpoints (medium)
- Extend `backend/app/services/extract.py` to parse:
- `#hashtags` → tags
- `- [ ] task text` → action items
- Add `POST /notes/{id}/extract`:
- Returns structured extraction results and optionally persists new tags/action items when `apply=true`.
- Add tests for extraction parsing and the `apply=true` persistence path.
## 7) Robust error handling and response envelopes (easy‑medium)
- Add validation with Pydantic models (min length constraints, non‑empty strings).
- Add global exception handlers to return consistent JSON envelopes:
- `{ "ok": false, "error": { "code": "NOT_FOUND", "message": "..." } }`
- Success responses: `{ "ok": true, "data": ... }`
- Update tests to assert envelope shapes for both success and error cases.
## 8) List endpoint pagination for all collections (easy)
- Add `page` and `page_size` to `GET /notes` and `GET /action-items`.
- Return `items` and `total` for each.
- Update the frontend to paginate lists; add tests for boundaries (empty last page, too‑large page size).
## 9) Query performance and indexes (easy‑medium)
- Add SQLite indexes where beneficial (e.g., `notes.title`, join tables for tags).
- Verify improved query plans and ensure no regressions through tests that seed larger datasets.
## 10) Test coverage improvements (easy)
- Add tests covering:
- 400/404 scenarios for each endpoint
- Concurrency/transactional behavior for bulk operations
- Frontend integration tests for search, pagination, and optimistic updates (can be mocked or lightweight)
## 11) Deployable on Vercel (medium–complex)
- Frontend on Vite + React:
- Add a `package.json` with `build` and `preview` scripts and configure Vite to output to `frontend/dist` (or `frontend/ui/dist`).
- Add a `vercel.json` that sets the project root to `week5/frontend` and `outputDirectory` to `dist`.
- Inject `VITE_API_BASE_URL` at build time to point to the API.
- API on Vercel (Option A, serverless FastAPI):
- Create `week5/api/index.py` that imports the FastAPI `app` from `backend/app/main.py`.
- Ensure Python dependencies are available to Vercel (use `pyproject.toml` or a `requirements.txt` for the function).
- Configure CORS to allow the Vercel frontend origin.
- Update `vercel.json` to route `/api/*` to the Python function and serve the React app for other routes.
- API elsewhere (Option B):
- Deploy backend to a service like Fly.io or Render.
- Configure the Vercel frontend to consume the external API via `VITE_API_BASE_URL` and set up any needed rewrites/proxies.
- Add a short deploy guide to `README.md` including environment variables, build commands, and rollback.
-66
View File
@@ -1,66 +0,0 @@
async function fetchJSON(url, options) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
async function loadNotes() {
const list = document.getElementById('notes');
list.innerHTML = '';
const notes = await fetchJSON('/notes/');
for (const n of notes) {
const li = document.createElement('li');
li.textContent = `${n.title}: ${n.content}`;
list.appendChild(li);
}
}
async function loadActions() {
const list = document.getElementById('actions');
list.innerHTML = '';
const items = await fetchJSON('/action-items/');
for (const a of items) {
const li = document.createElement('li');
li.textContent = `${a.description} [${a.completed ? 'done' : 'open'}]`;
if (!a.completed) {
const btn = document.createElement('button');
btn.textContent = 'Complete';
btn.onclick = async () => {
await fetchJSON(`/action-items/${a.id}/complete`, { method: 'PUT' });
loadActions();
};
li.appendChild(btn);
}
list.appendChild(li);
}
}
window.addEventListener('DOMContentLoaded', () => {
document.getElementById('note-form').addEventListener('submit', async (e) => {
e.preventDefault();
const title = document.getElementById('note-title').value;
const content = document.getElementById('note-content').value;
await fetchJSON('/notes/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
e.target.reset();
loadNotes();
});
document.getElementById('action-form').addEventListener('submit', async (e) => {
e.preventDefault();
const description = document.getElementById('action-desc').value;
await fetchJSON('/action-items/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description }),
});
e.target.reset();
loadActions();
});
loadNotes();
loadActions();
});
-35
View File
@@ -1,35 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Modern Software Dev Starter</title>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<main>
<h1>Modern Software Dev Starter</h1>
<section>
<h2>Notes</h2>
<form id="note-form">
<input id="note-title" placeholder="Title" required />
<input id="note-content" placeholder="Content" required />
<button type="submit">Add</button>
</form>
<ul id="notes"></ul>
</section>
<section>
<h2>Action Items</h2>
<form id="action-form">
<input id="action-desc" placeholder="Description" required />
<button type="submit">Add</button>
</form>
<ul id="actions"></ul>
</section>
</main>
<script src="/static/app.js"></script>
</body>
</html>
-8
View File
@@ -1,8 +0,0 @@
body{font-family:system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;margin:0;padding:0;background:#fafafa;color:#111}
main{max-width:900px;margin:2rem auto;padding:0 1rem}
h1{font-size:1.8rem}
section{background:#fff;border:1px solid #eee;border-radius:8px;padding:1rem;margin:1rem 0}
form{display:flex;gap:.5rem;margin-bottom:.5rem}
input{flex:1;padding:.5rem;border:1px solid #ccc;border-radius:4px}
button{padding:.5rem .8rem;border:1px solid #ccc;border-radius:4px;background:#f5f5f5;cursor:pointer}
ul{list-style:disc;padding-left:1.25rem}
-15
View File
@@ -1,15 +0,0 @@
repos:
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.8
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
-72
View File
@@ -1,72 +0,0 @@
# Week 5 Write-up
Tip: To preview this markdown file
- On Mac, press `Command (⌘) + Shift + V`
- On Windows/Linux, press `Ctrl + Shift + V`
## INSTRUCTIONS
Fill out all of the `TODO`s in this file.
## SUBMISSION DETAILS
Name: **TODO** \
SUNet ID: **TODO** \
Citations: **TODO**
This assignment took me about **TODO** hours to do.
## YOUR RESPONSES
### Automation A: Warp Drive saved prompts, rules, MCP servers
a. Design of each automation, including goals, inputs/outputs, steps
> TODO
b. Before vs. after (i.e. manual workflow vs. automated workflow)
> TODO
c. Autonomy levels used for each completed task (what code permissions, why, and how you supervised)
> TODO
d. (if applicable) Multi‑agent notes: roles, coordination strategy, and concurrency wins/risks/failures
> TODO
e. How you used the automation (what pain point it resolves or accelerates)
> TODO
### Automation B: Multi‑agent workflows in Warp
a. Design of each automation, including goals, inputs/outputs, steps
> TODO
b. Before vs. after (i.e. manual workflow vs. automated workflow)
> TODO
c. Autonomy levels used for each completed task (what code permissions, why, and how you supervised)
> TODO
d. (if applicable) Multi‑agent notes: roles, coordination strategy, and concurrency wins/risks/failures
> TODO
e. How you used the automation (what pain point it resolves or accelerates)
> TODO
### (Optional) Automation C: Any Additional Automations
a. Design of each automation, including goals, inputs/outputs, steps
> TODO
b. Before vs. after (i.e. manual workflow vs. automated workflow)
> TODO
c. Autonomy levels used for each completed task (what code permissions, why, and how you supervised)
> TODO
d. (if applicable) Multi‑agent notes: roles, coordination strategy, and concurrency wins/risks/failures
> TODO
e. How you used the automation (what pain point it resolves or accelerates)
> TODO
-19
View File
@@ -1,19 +0,0 @@
.PHONY: run test format lint seed
run:
PYTHONPATH=. uvicorn backend.app.main:app --reload --host $${HOST:-127.0.0.1} --port $${PORT:-8000}
test:
PYTHONPATH=. pytest -q backend/tests
format:
black .
ruff check . --fix
lint:
ruff check .
seed:
PYTHONPATH=. python -c "from backend.app.db import apply_seed_if_needed; apply_seed_if_needed()"
-61
View File
@@ -1,61 +0,0 @@
# Week 6 — Scan and Fix Vulnerabilities with Semgrep
## Assignment Overview
Run static analysis against the provided app in `week6/` using **Semgrep**. Triage findings and remediate a minimum of 3 security issues. In your write-up, explain what issues Semgrep surfaced and how you fixed them.
## Learn about Semgrep
Semgrep is an open-source, static analysis tool that searches code, finds bugs, and enforces secure guardrails and coding standards.
1. Click [here](https://github.com/semgrep/semgrep/blob/develop/README.md) to learn about Semgrep.
2. Follow the installation instructions in the link above. It is up to you whether you prefer to use the **Semgrep Appsec Platform** or the **CLI tool**.
## Scan tasks
### What you will scan
- Backend Python (FastAPI): `week6/backend/`
- Frontend JavaScript: `week6/frontend/`
- Dependencies: `week6/requirements.txt`
- Config/env (for secrets): files within `week6/`
### Run a general security scan plus focused scans for secrets and dependencies.
From the **assignment repository root**, run the following command to apply a curated CI-style bundle that includes both code and secrets rules:
```bash
semgrep ci --subdir week6
```
## Task
1. Pick any 3 issues identified by Semgrep and fix them using an AI coding tool of your choice.
2. Show precise edits and explain the mitigation (e.g., parameterized SQL, safer APIs, stronger crypto, sanitized DOM writes, restricted CORS, dependency upgrades).
3. Important: Ensure the app still runs and tests still pass after your fixes.
## Deliverables
### 1. Brief findings overview
- Summarize the categories Semgrep reported (SAST/Secrets/SCA).
- Note any false positives or noisy rules you chose to ignore and why.
### 2. Three fixes (before → after)
For each fixed issue:
- File and line(s)
- Rule/category Semgrep flagged
- Brief risk description
- Your change (short code diff or explanation, AI coding tool usage)
- Why this mitigates the issue
## Tips
- Prefer minimal, targeted changes that address the root cause.
- Re‑run Semgrep after each fix to confirm the finding is resolved and no new ones were introduced.
- For dependencies, document upgraded versions and link to advisories if you used supply-chain scanning.
## Submission Instructions
1. Make sure you have all changes pushed to your remote repository for grading.
2. Make sure you've added both brentju and febielin as collaborators on your assignment repository.
2. Submit via Gradescope.
View File
-2
View File
@@ -1,2 +0,0 @@
# Week 7 backend app package
-58
View File
@@ -1,58 +0,0 @@
import os
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from dotenv import load_dotenv
from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session, sessionmaker
load_dotenv()
DEFAULT_DB_PATH = os.getenv("DATABASE_PATH", "./data/app.db")
engine = create_engine(f"sqlite:///{DEFAULT_DB_PATH}", connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db() -> Iterator[Session]:
session: Session = SessionLocal()
try:
yield session
session.commit()
except Exception: # noqa: BLE001
session.rollback()
raise
finally:
session.close()
@contextmanager
def get_session() -> Iterator[Session]:
session = SessionLocal()
try:
yield session
session.commit()
except Exception: # noqa: BLE001
session.rollback()
raise
finally:
session.close()
def apply_seed_if_needed() -> None:
db_path = Path(DEFAULT_DB_PATH)
db_path.parent.mkdir(parents=True, exist_ok=True)
newly_created = not db_path.exists()
if newly_created:
db_path.touch()
seed_file = Path("./data/seed.sql")
if newly_created and seed_file.exists():
with engine.begin() as conn:
sql = seed_file.read_text()
if sql.strip():
for statement in [s.strip() for s in sql.split(";") if s.strip()]:
conn.execute(text(statement))
-46
View File
@@ -1,46 +0,0 @@
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from .db import apply_seed_if_needed, engine
from .models import Base
from .routers import action_items as action_items_router
from .routers import notes as notes_router
app = FastAPI(title="Modern Software Dev Starter (Week 7)", version="0.1.0")
# Ensure data dir exists
Path("data").mkdir(parents=True, exist_ok=True)
# Mount static frontend
app.mount("/static", StaticFiles(directory="frontend"), name="static")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Compatibility with FastAPI lifespan events; keep on_event for simplicity here
@app.on_event("startup")
def startup_event() -> None:
Base.metadata.create_all(bind=engine)
apply_seed_if_needed()
@app.get("/")
async def root() -> FileResponse:
return FileResponse("frontend/index.html")
# Routers
app.include_router(notes_router.router)
app.include_router(action_items_router.router)
-31
View File
@@ -1,31 +0,0 @@
from datetime import datetime
from sqlalchemy import Boolean, Column, DateTime, Integer, String, Text
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class TimestampMixin:
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
class Note(Base, TimestampMixin):
__tablename__ = "notes"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
content = Column(Text, nullable=False)
class ActionItem(Base, TimestampMixin):
__tablename__ = "action_items"
id = Column(Integer, primary_key=True, index=True)
description = Column(Text, nullable=False)
completed = Column(Boolean, default=False, nullable=False)
-72
View File
@@ -1,72 +0,0 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import asc, desc, select
from sqlalchemy.orm import Session
from ..db import get_db
from ..models import ActionItem
from ..schemas import ActionItemCreate, ActionItemPatch, ActionItemRead
router = APIRouter(prefix="/action-items", tags=["action_items"])
@router.get("/", response_model=list[ActionItemRead])
def list_items(
db: Session = Depends(get_db),
completed: Optional[bool] = None,
skip: int = 0,
limit: int = Query(50, le=200),
sort: str = Query("-created_at"),
) -> list[ActionItemRead]:
stmt = select(ActionItem)
if completed is not None:
stmt = stmt.where(ActionItem.completed.is_(completed))
sort_field = sort.lstrip("-")
order_fn = desc if sort.startswith("-") else asc
if hasattr(ActionItem, sort_field):
stmt = stmt.order_by(order_fn(getattr(ActionItem, sort_field)))
else:
stmt = stmt.order_by(desc(ActionItem.created_at))
rows = db.execute(stmt.offset(skip).limit(limit)).scalars().all()
return [ActionItemRead.model_validate(row) for row in rows]
@router.post("/", response_model=ActionItemRead, status_code=201)
def create_item(payload: ActionItemCreate, db: Session = Depends(get_db)) -> ActionItemRead:
item = ActionItem(description=payload.description, completed=False)
db.add(item)
db.flush()
db.refresh(item)
return ActionItemRead.model_validate(item)
@router.put("/{item_id}/complete", response_model=ActionItemRead)
def complete_item(item_id: int, db: Session = Depends(get_db)) -> ActionItemRead:
item = db.get(ActionItem, item_id)
if not item:
raise HTTPException(status_code=404, detail="Action item not found")
item.completed = True
db.add(item)
db.flush()
db.refresh(item)
return ActionItemRead.model_validate(item)
@router.patch("/{item_id}", response_model=ActionItemRead)
def patch_item(item_id: int, payload: ActionItemPatch, db: Session = Depends(get_db)) -> ActionItemRead:
item = db.get(ActionItem, item_id)
if not item:
raise HTTPException(status_code=404, detail="Action item not found")
if payload.description is not None:
item.description = payload.description
if payload.completed is not None:
item.completed = payload.completed
db.add(item)
db.flush()
db.refresh(item)
return ActionItemRead.model_validate(item)
-132
View File
@@ -1,132 +0,0 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import asc, desc, select, text
from sqlalchemy.orm import Session
from ..db import get_db
from ..models import Note
from ..schemas import NoteCreate, NotePatch, NoteRead
router = APIRouter(prefix="/notes", tags=["notes"])
@router.get("/", response_model=list[NoteRead])
def list_notes(
db: Session = Depends(get_db),
q: Optional[str] = None,
skip: int = 0,
limit: int = Query(50, le=200),
sort: str = Query("-created_at", description="Sort by field, prefix with - for desc"),
) -> list[NoteRead]:
stmt = select(Note)
if q:
stmt = stmt.where((Note.title.contains(q)) | (Note.content.contains(q)))
sort_field = sort.lstrip("-")
order_fn = desc if sort.startswith("-") else asc
if hasattr(Note, sort_field):
stmt = stmt.order_by(order_fn(getattr(Note, sort_field)))
else:
stmt = stmt.order_by(desc(Note.created_at))
rows = db.execute(stmt.offset(skip).limit(limit)).scalars().all()
return [NoteRead.model_validate(row) for row in rows]
@router.post("/", response_model=NoteRead, status_code=201)
def create_note(payload: NoteCreate, db: Session = Depends(get_db)) -> NoteRead:
note = Note(title=payload.title, content=payload.content)
db.add(note)
db.flush()
db.refresh(note)
return NoteRead.model_validate(note)
@router.patch("/{note_id}", response_model=NoteRead)
def patch_note(note_id: int, payload: NotePatch, db: Session = Depends(get_db)) -> NoteRead:
note = db.get(Note, note_id)
if not note:
raise HTTPException(status_code=404, detail="Note not found")
if payload.title is not None:
note.title = payload.title
if payload.content is not None:
note.content = payload.content
db.add(note)
db.flush()
db.refresh(note)
return NoteRead.model_validate(note)
@router.get("/{note_id}", response_model=NoteRead)
def get_note(note_id: int, db: Session = Depends(get_db)) -> NoteRead:
note = db.get(Note, note_id)
if not note:
raise HTTPException(status_code=404, detail="Note not found")
return NoteRead.model_validate(note)
@router.get("/unsafe-search", response_model=list[NoteRead])
def unsafe_search(q: str, db: Session = Depends(get_db)) -> list[NoteRead]:
sql = text(
f"""
SELECT id, title, content, created_at, updated_at
FROM notes
WHERE title LIKE '%{q}%' OR content LIKE '%{q}%'
ORDER BY created_at DESC
LIMIT 50
"""
)
rows = db.execute(sql).all()
results: list[NoteRead] = []
for r in rows:
results.append(
NoteRead(
id=r.id,
title=r.title,
content=r.content,
created_at=r.created_at,
updated_at=r.updated_at,
)
)
return results
@router.get("/debug/hash-md5")
def debug_hash_md5(q: str) -> dict[str, str]:
import hashlib
return {"algo": "md5", "hex": hashlib.md5(q.encode()).hexdigest()}
@router.get("/debug/eval")
def debug_eval(expr: str) -> dict[str, str]:
result = str(eval(expr)) # noqa: S307
return {"result": result}
@router.get("/debug/run")
def debug_run(cmd: str) -> dict[str, str]:
import subprocess
completed = subprocess.run(cmd, shell=True, capture_output=True, text=True) # noqa: S602,S603
return {"returncode": str(completed.returncode), "stdout": completed.stdout, "stderr": completed.stderr}
@router.get("/debug/fetch")
def debug_fetch(url: str) -> dict[str, str]:
from urllib.request import urlopen
with urlopen(url) as res: # noqa: S310
body = res.read(1024).decode(errors="ignore")
return {"snippet": body}
@router.get("/debug/read")
def debug_read(path: str) -> dict[str, str]:
try:
content = open(path, "r").read(1024)
except Exception as exc: # noqa: BLE001
raise HTTPException(status_code=400, detail=str(exc))
return {"snippet": content}
-46
View File
@@ -1,46 +0,0 @@
from datetime import datetime
from pydantic import BaseModel
class NoteCreate(BaseModel):
title: str
content: str
class NoteRead(BaseModel):
id: int
title: str
content: str
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class NotePatch(BaseModel):
title: str | None = None
content: str | None = None
class ActionItemCreate(BaseModel):
description: str
class ActionItemRead(BaseModel):
id: int
description: str
completed: bool
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class ActionItemPatch(BaseModel):
description: str | None = None
completed: bool | None = None
-14
View File
@@ -1,14 +0,0 @@
def extract_action_items(text: str) -> list[str]:
lines = [line.strip("- ") for line in text.splitlines() if line.strip()]
results: list[str] = []
for line in lines:
normalized = line.lower()
if normalized.startswith("todo:") or normalized.startswith("action:"):
results.append(line)
elif line.endswith("!"):
results.append(line)
return results
API_TOKEN = "sk_live_51HACKED_EXAMPLE_DO_NOT_USE_abcdefghijklmnopqrstuvwxyz"
View File
-41
View File
@@ -1,41 +0,0 @@
import os
import tempfile
from collections.abc import Generator
import pytest
from backend.app.db import get_db
from backend.app.main import app
from backend.app.models import Base
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture()
def client() -> Generator[TestClient, None, None]:
db_fd, db_path = tempfile.mkstemp()
os.close(db_fd)
engine = create_engine(f"sqlite:///{db_path}", connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base.metadata.create_all(bind=engine)
def override_get_db():
session = TestingSessionLocal()
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as c:
yield c
os.unlink(db_path)
-24
View File
@@ -1,24 +0,0 @@
def test_create_complete_list_and_patch_action_item(client):
payload = {"description": "Ship it"}
r = client.post("/action-items/", json=payload)
assert r.status_code == 201, r.text
item = r.json()
assert item["completed"] is False
assert "created_at" in item and "updated_at" in item
r = client.put(f"/action-items/{item['id']}/complete")
assert r.status_code == 200
done = r.json()
assert done["completed"] is True
r = client.get("/action-items/", params={"completed": True, "limit": 5, "sort": "-created_at"})
assert r.status_code == 200
items = r.json()
assert len(items) >= 1
r = client.patch(f"/action-items/{item['id']}", json={"description": "Updated"})
assert r.status_code == 200
patched = r.json()
assert patched["description"] == "Updated"
-17
View File
@@ -1,17 +0,0 @@
from backend.app.services.extract import extract_action_items
def test_extract_action_items():
text = """
This is a note
- TODO: write tests
- ACTION: review PR
- Ship it!
Not actionable
""".strip()
items = extract_action_items(text)
assert "TODO: write tests" in items
assert "ACTION: review PR" in items
assert "Ship it!" in items
-25
View File
@@ -1,25 +0,0 @@
def test_create_list_and_patch_notes(client):
payload = {"title": "Test", "content": "Hello world"}
r = client.post("/notes/", json=payload)
assert r.status_code == 201, r.text
data = r.json()
assert data["title"] == "Test"
assert "created_at" in data and "updated_at" in data
r = client.get("/notes/")
assert r.status_code == 200
items = r.json()
assert len(items) >= 1
r = client.get("/notes/", params={"q": "Hello", "limit": 10, "sort": "-created_at"})
assert r.status_code == 200
items = r.json()
assert len(items) >= 1
note_id = data["id"]
r = client.patch(f"/notes/{note_id}", json={"title": "Updated"})
assert r.status_code == 200
patched = r.json()
assert patched["title"] == "Updated"
-25
View File
@@ -1,25 +0,0 @@
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ','now')) NOT NULL,
updated_at DATETIME DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ','now')) NOT NULL
);
CREATE TABLE IF NOT EXISTS action_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL,
completed BOOLEAN NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ','now')) NOT NULL,
updated_at DATETIME DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ','now')) NOT NULL
);
INSERT INTO notes (title, content) VALUES
('Welcome', 'This is a starter note. TODO: explore the app!'),
('Demo', 'Click around and add a note. Ship feature!');
INSERT INTO action_items (description, completed) VALUES
('Try pre-commit', 0),
('Run tests', 0);
-92
View File
@@ -1,92 +0,0 @@
async function fetchJSON(url, options) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
async function loadNotes(params = {}) {
const list = document.getElementById('notes');
list.innerHTML = '';
const query = new URLSearchParams(params);
const notes = await fetchJSON('/notes/?' + query.toString());
for (const n of notes) {
const li = document.createElement('li');
li.innerHTML = `<strong>${n.title}</strong>: ${n.content}`;
list.appendChild(li);
}
}
async function loadActions(params = {}) {
const list = document.getElementById('actions');
list.innerHTML = '';
const query = new URLSearchParams(params);
const items = await fetchJSON('/action-items/?' + query.toString());
for (const a of items) {
const li = document.createElement('li');
li.textContent = `${a.description} [${a.completed ? 'done' : 'open'}]`;
if (!a.completed) {
const btn = document.createElement('button');
btn.textContent = 'Complete';
btn.onclick = async () => {
await fetchJSON(`/action-items/${a.id}/complete`, { method: 'PUT' });
loadActions(params);
};
li.appendChild(btn);
} else {
const btn = document.createElement('button');
btn.textContent = 'Reopen';
btn.onclick = async () => {
await fetchJSON(`/action-items/${a.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: false }),
});
loadActions(params);
};
li.appendChild(btn);
}
list.appendChild(li);
}
}
window.addEventListener('DOMContentLoaded', () => {
document.getElementById('note-form').addEventListener('submit', async (e) => {
e.preventDefault();
const title = document.getElementById('note-title').value;
const content = document.getElementById('note-content').value;
await fetchJSON('/notes/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
e.target.reset();
loadNotes();
});
document.getElementById('note-search-btn').addEventListener('click', async () => {
const q = document.getElementById('note-search').value;
loadNotes({ q });
});
document.getElementById('action-form').addEventListener('submit', async (e) => {
e.preventDefault();
const description = document.getElementById('action-desc').value;
await fetchJSON('/action-items/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description }),
});
e.target.reset();
loadActions();
});
document.getElementById('filter-completed').addEventListener('change', (e) => {
const checked = e.target.checked;
loadActions({ completed: checked });
});
loadNotes();
loadActions();
});
-44
View File
@@ -1,44 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Modern Software Dev Starter</title>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<main>
<h1>Modern Software Dev Starter (Week 7)</h1>
<section>
<h2>Notes</h2>
<form id="note-form">
<input id="note-title" placeholder="Title" required />
<input id="note-content" placeholder="Content" required />
<button type="submit">Add</button>
</form>
<div style="margin:.25rem 0;">
<input id="note-search" placeholder="Search" />
<button id="note-search-btn">Search</button>
</div>
<ul id="notes"></ul>
</section>
<section>
<h2>Action Items</h2>
<form id="action-form">
<input id="action-desc" placeholder="Description" required />
<button type="submit">Add</button>
</form>
<div style="margin:.25rem 0;">
<label><input type="checkbox" id="filter-completed" /> Show completed only</label>
</div>
<ul id="actions"></ul>
</section>
</main>
<script src="/static/app.js"></script>
</body>
</html>
-10
View File
@@ -1,10 +0,0 @@
body{font-family:system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;margin:0;padding:0;background:#fafafa;color:#111}
main{max-width:900px;margin:2rem auto;padding:0 1rem}
h1{font-size:1.8rem}
section{background:#fff;border:1px solid #eee;border-radius:8px;padding:1rem;margin:1rem 0}
form{display:flex;gap:.5rem;margin-bottom:.5rem}
input{flex:1;padding:.5rem;border:1px solid #ccc;border-radius:4px}
button{padding:.5rem .8rem;border:1px solid #ccc;border-radius:4px;background:#f5f5f5;cursor:pointer}
ul{list-style:disc;padding-left:1.25rem}
-17
View File
@@ -1,17 +0,0 @@
repos:
- repo: https://github.com/psf/black
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.8
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
-9
View File
@@ -1,9 +0,0 @@
fastapi==0.65.2
uvicorn==0.11.8
sqlalchemy==1.3.23
pydantic==1.5.1
requests==2.19.1
PyYAML==5.1
Jinja2==2.10.1
MarkupSafe==1.1.0
Werkzeug==0.14.1
-68
View File
@@ -1,68 +0,0 @@
# Week 6 Write-up
Tip: To preview this markdown file
- On Mac, press `Command (⌘) + Shift + V`
- On Windows/Linux, press `Ctrl + Shift + V`
## Instructions
Fill out all of the `TODO`s in this file.
## Submission Details
Name: **TODO** \
SUNet ID: **TODO** \
Citations: **TODO**
This assignment took me about **TODO** hours to do.
## Brief findings overview
> TODO
## Fix #1
a. File and line(s)
> TODO
b. Rule/category Semgrep flagged
> TODO
c. Brief risk description
> TODO
d. Your change (short code diff or explanation, AI coding tool usage)
> TODO
e. Why this mitigates the issue
> TODO
## Fix #2
a. File and line(s)
> TODO
b. Rule/category Semgrep flagged
> TODO
c. Brief risk description
> TODO
d. Your change (short code diff or explanation, AI coding tool usage)
> TODO
e. Why this mitigates the issue
> TODO
## Fix #3
a. File and line(s)
> TODO
b. Rule/category Semgrep flagged
> TODO
c. Brief risk description
> TODO
d. Your change (short code diff or explanation, AI coding tool usage)
> TODO
e. Why this mitigates the issue
> TODO
-19
View File
@@ -1,19 +0,0 @@
.PHONY: run test format lint seed
run:
PYTHONPATH=. uvicorn backend.app.main:app --reload --host $${HOST:-127.0.0.1} --port $${PORT:-8000}
test:
PYTHONPATH=. pytest -q backend/tests
format:
black .
ruff check . --fix
lint:
ruff check .
seed:
PYTHONPATH=. python -c "from backend.app.db import apply_seed_if_needed; apply_seed_if_needed()"
-65
View File
@@ -1,65 +0,0 @@
# Week 7
Slightly enhanced full‑stack starter (copied from Week 5) with a few backend improvements.
- FastAPI backend with SQLite (SQLAlchemy)
- Static frontend (no Node toolchain needed)
- Minimal tests (pytest)
- Pre-commit (black + ruff)
- Enhancements over Week 5:
- Timestamps on models (`created_at`, `updated_at`)
- Pagination and sorting for list endpoints
- Optional filters (e.g., filter action items by completion)
- PATCH endpoints for partial updates
## Quickstart
1) Create and activate a virtualenv, then install dependencies
```bash
cd /Users/mihaileric/Documents/code/modern-software-dev-assignments
python -m venv .venv && source .venv/bin/activate
pip install -e .[dev]
```
2) (Optional) Install pre-commit hooks
```bash
pre-commit install
```
3) Run the app (from `week6/`)
```bash
cd week7 && make run
```
Open `http://localhost:8000` for the frontend and `http://localhost:8000/docs` for the API docs.
## Structure
```
backend/ # FastAPI app
frontend/ # Static UI served by FastAPI
data/ # SQLite DB + seed
docs/ # TASKS for agent-driven workflows
```
## Tests
```bash
cd week7 && make test
```
## Formatting/Linting
```bash
cd week7 && make format
cd week7 && make lint
```
## Configuration
Copy `.env.example` to `.env` (in `week7/`) to override defaults like the database path.
-55
View File
@@ -1,55 +0,0 @@
# Week 7 – Exploring AI Code Review Using Graphite
## Assignment Overview
In this assignment, you will practice agent-driven development and AI-assisted code review on a more advanced codebase. You will implement the tasks in `week7/docs/TASKS.md`, validate your work with tests and manual review, and compare your own review notes with AI-generated code reviews.
## Get Started with Graphite
1. Sign up for Graphite: https://app.graphite.dev/signup
2. Upon sign up, you can claim your 30-day free trial.
3. After the 30 days, you can use code **CS146S** to claim free Graphite under their education program.
## What to do
Implement the tasks from `week7/docs/TASKS.md` using an AI coding tool of your choice (e.g. Cursor, Copilot, Claude, etc.).
### For each task:
1. Create a separate branch.
2. Implement the task with your AI tool using a 1-shot prompt.
3. Manually review the changes line-by-line. Fix issues you notice and add explanatory commit messages where helpful. You may also pair with a classmate to review each other’s code instead of reviewing your own changes.
4. Open a Pull Request (PR) for the task. Ensure your PRs include:
- Description of the problem and your approach.
- Summary of testing performed (include commands and results) and any added/updated tests.
- Notable tradeoffs, limitations, or follow-ups.
5. Use Graphite Diamond to generate an AI-assisted code review on the PR.
6. Document the results of your PR in the `writeup.md`.
## Deliverables
In your `writeup.md`, we are looking for the follwoing:
- Four PRs, one per completed task, each with:
- Clear PR description
- Links to relevant commits/issues.
- Graphite Diamond AI review comments visible on the PR
- A brief reflection addressing the following:
- The types of comments you typically made in your manual reviews (e.g., correctness, performance, security, naming, test gaps, API shape, UX, docs).
- A comparison of **your** comments vs. **Graphite’s** AI-generated comments for each PR.
- When the AI reviews were better/worse than yours (cite specific examples)
- Your comfort level trusting AI reviews going forward and any heuristics for when to rely on them.
## Evaluation criteria (100 points total)
- 20 points per completed task
- Technical correctness and completeness of each task.
- Code quality: readability, naming, structure, error handling, and tests.
- Thoughtfulness and depth of manual review notes
- Graphite Diamond AI generated code review
- 20 points for the brief reflection
- Insightful comparison between your review and Graphite’s AI review
- Description of your personal comfort level with AI Reviews
## Submission Instructions
1. Make sure you have all changes pushed to your remote repository for grading.
2. Make sure you've added both brentju and febielin as collaborators on your assignment repository.
2. Submit via Gradescope.
View File

Some files were not shown because too many files have changed in this diff Show More