mirror of
https://github.com/mihail911/modern-software-dev-assignments.git
synced 2026-10-02 03:54:37 +08:00
Week 1 Assignment
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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`!**
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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`!
|
||||
@@ -1,3 +0,0 @@
|
||||
__all__ = []
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
__all__ = []
|
||||
|
||||
|
||||
-116
@@ -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()
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -1,3 +0,0 @@
|
||||
__all__ = []
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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... e.g. - [ ] Set up database - 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>
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
__all__ = []
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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()"
|
||||
@@ -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 +0,0 @@
|
||||
"""Backend package (week4)."""
|
||||
@@ -1 +0,0 @@
|
||||
"""Application package for FastAPI backend (week4)."""
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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 +0,0 @@
|
||||
"""API routers package (week4)."""
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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:")]
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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`)
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()"
|
||||
@@ -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.
|
||||
@@ -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 +0,0 @@
|
||||
"""Backend package (week5)."""
|
||||
@@ -1 +0,0 @@
|
||||
"""Application package for FastAPI backend (week5)."""
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -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 +0,0 @@
|
||||
"""API routers package (week5)."""
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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:")]
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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()"
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -1,2 +0,0 @@
|
||||
# Week 7 backend app package
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()"
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user