# Project export: Sammy Support Crew

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2024
- Tagline: Need help navigating the UCSC website and its many resources? Never fear, Sammy and his Support Crew are here! Talk with Sammy or his friends to answer your questions on the spot, no effort required!
- Devpost: https://devpost.com/software/project-r6g8l5khcx92
- GitHub: https://github.com/suaviloquence/cruzhacks24
- Demo: https://sammy.b.mcarr.one/
- Video: https://www.youtube.com/embed/N2a_zd1Rp0Y?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Best AI Hack)
- Team: 2 GitHub contributor(s) — m (9 commits), teahibiscus (7 commits)

## Devpost submission (written by the team)

### Inspiration

College life is very hard to navigate, especially coming straight out of high school. On top of that, with a website as broad as UCSC’s, it can be difficult to find the exact solution to your problem. As freshmen at UCSC, we often got overwhelmed navigating the vast amount of resources ourselves, and sometimes even the offices don’t know how to redirect you. That’s what the Sammy Support Crew was made for. To help you sift through the massive amount of resources offered to you by UCSC, narrow them down to a few, and guide you to the right person or office to talk to.”

### What it does

Sammy Support Crew is an AI tool that analyzes the many campus resources inputted in the database according to what the user requests, analyzes the resources, and gives back the links, offices, and other information that will answer the request. The five AI bots (Sammy, Sunny, Delbert, Henry, and Beatrice) take the user's information and suggest quick actions based on their analysis of the information. There are five bots that the user can speak to, and the user can choose who they want to talk to by clicking on the mascot. The five are: Sammy the Slug Secretary, Sunny the Social Squirrel, Delbert the Diligent Deer, Henry the Healthy Hammerhead, Beatrice the Bus-driving Barracuda. Each has their own field of knowledge except Sammy, who answers general questions or redirects the user to the right bot if they do not know who to talk to. Sunny the Social Squirrel handles the student life and engagement resources, as well as the financial aid and college directory. Delbert the Diligent Deer handles all the academic resources. Henry the Healthy Hammerhead handles all the health resources, as well as the discrimination and crisis offices. Beatrice the Bus-driving Barracuda handles all the information about transportation and transit services, as well as parking permits. Together, they aim to guide the user to the right resource they need, and will suggest the other bots for further questions if they are talking to the wrong specialized bot.

### How we built it

A lot of our time was spent collecting and organizing data about the myriad different resources and organizations available at UCSC, as well as all the different actions that they can perform. While some of our group was doing this, we prototyped the front and back ends, and using the OpenAI Assistants API helped for comfortable integration of an AI chatbot, which we trained on our data. All avatar sprites were designed and created on Adobe Fresco, and then rendered in Medibang Paint Pro.

### Challenges we ran into

One of our biggest challenges was compiling all the resources from the UCSC website. As we said before, there are a lot of resources with information hiding in the smallest of places. It was a very time consuming task to complete the database, which sacrificed our time for other aspects of the tool. Instead of having one person complete it on their own, we all helped to complete it together to get it done quicker, and allow ourselves to continue in the project.

### Accomplishments we're proud of

Completing all the sprites for the individual avatars was something that we (Amber, really) worried about not being able to accomplish. Thankfully, they all came out nicely, and we think they make Sammy Support Crew friendly and approachable. We are also very proud of how the website looks now. At our last hackathon, we felt that we could’ve done a better job for UI/UX design, and finishing Sammy Support Crew made us a lot happier with how aesthetically pleasing it looks.

### What we learned

One thing we definitely learned was the power of organization. It’s hard to pump out the perfect app in the constrained amount of time of a hackathon, so we realized it was essential for each member to have a task and accomplish it. We thought we wouldn’t be able to finish Sammy Support Crew in time, but through project management techniques and tools like Trello, we were able to finish it before losing our minds. We also learned a lot more about resources at UCSC. Taking on this project required us to dive deep into UCSC’s vast network of organizations while collecting data, and we discovered many resources we did not know about before, such as the ombudsman. We also each had our own individual learning experience: Sritha - I had never coded in TypeScript or JavaScript before, so taking on this project fast-tracked me into learning a new programming language. I also learned more about creating an API, since this is only the second time I have worked with one. Amber - I learned a lot about the importance of color selection and readability in UI design. Even if some colors look nice together, readability and effectiveness of the overall project is a higher priority. I also became more accustomed with Adobe Fresco during the creation of the different avatars in Sammy Support Crew. Max - I got to really experience the power of the OpenAI API by using their model with our appendix of data, which makes development super seamless, especially under the time constraints of the hackathon. I also worked a lot more with placeholders and other techniques to keep me unblocked and productive and able to work while depending on others’ tasks. (concurrency!) Manuel - There is a lot of groundwork you have to lay, information that needs to be ready and available when our tool tries to extrapolate it from our bank. I learned it’s important to gather all the data it’s required to pull from, and formatting it so that the AI can learn and regurgitate said info

### What's next

for The Sammy Support Crew The next step for Sammy Support Crew is to continue fixing the tiny errors and make it more sustainable in the long-run. We also imagine creating something with a similar concept for each of the other UC campuses, since we understand this tool could be beneficial for students at other universities. In terms of the technical, we plan on using more AI to even better identify quick actions for the user to take, as well as collecting more training data. Additionally, expanding beyond the hackathon, we would like to add user accounts both for security and keeping a record of past conversations, as well as additional security features on the back end.

## README (from the GitHub repository)

# Sammy Support Crew

[Devpost - CruzHacks '24 Best AI](https://devpost.com/software/project-r6g8l5khcx92)

## About

Sammy Support Crew is a network of animal chatbots that help you find resources at UC Santa Cruz!


## Detected evidence (automated analysis)

Indexed codebase: 13 recognized source files, 68 KB.
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Svelte (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (24 of 24)

```
.gitignore
app/.gitignore
app/.npmrc
app/package.json
app/README.md
app/src/app.d.ts
app/src/app.html
app/src/lib/index.ts
app/src/routes/+layout.ts
app/src/routes/+page.svelte
app/svelte.config.js
app/tsconfig.json
app/vite.config.ts
assistant.id
database.json
README.md
sammy_support_crew.txt
server/bots.ts
server/database.json
server/index.ts
server/offices.ts
server/package.json
server/tsconfig.json
static/index.html
```

### Dependencies

- app/package.json: @sveltejs/adapter-auto@^3.0.0, @sveltejs/adapter-static@^3.0.1, @sveltejs/kit@^2.0.0, @sveltejs/vite-plugin-svelte@^3.0.0, svelte@^4.2.7, svelte-check@^3.6.0, tslib@^2.4.1, typescript@^5.0.0, vite@^5.0.3
- server/package.json: @types/express@^4.17.21, @types/node@^20.11.5, body-parser@^1.20.2, dotenv@^16.3.2, express@^4.18.2, nodemon@^3.0.3, openai@^4.24.7, ts-node@^10.9.2, typescript@^5.3.3

### Recent commits (newest first)

- Update README.md
- final with readme
- final?
- bot keywords!
- sync
- fix offices
- Update offices.ts
- Update offices.ts
- Update offices.ts
- Update offices.ts
- offices.ts
- Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
- :3
- Update index.ts
- added bot description/constants
- psh
- push
- server skeleton
- Initial commit

## Key source files (fetched from GitHub, selected and truncated for size)

### server/package.json

```
{
	"name": "server",
	"version": "0.1.0",
	"main": "bin/index.js",
	"repository": "gh/suaviloquence/cruzhacks24",
	"author": "team SCAM",
	"license": "AGPL-3.0-only",
	"private": false,
	"dependencies": {
		"body-parser": "^1.20.2",
		"dotenv": "^16.3.2",
		"express": "^4.18.2",
		"openai": "^4.24.7"
	},
	"devDependencies": {
		"@types/express": "^4.17.21",
		"@types/node": "^20.11.5",
		"nodemon": "^3.0.3",
		"ts-node": "^10.9.2",
		"typescript": "^5.3.3"
	},
	"scripts": {
		"dev": "nodemon index.ts"
	}
}

```

### app/package.json

```
{
	"name": "app",
	"version": "0.0.1",
	"private": true,
	"scripts": {
		"dev": "vite dev",
		"build": "vite build",
		"preview": "vite preview",
		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
		"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
	},
	"devDependencies": {
		"@sveltejs/adapter-auto": "^3.0.0",
		"@sveltejs/kit": "^2.0.0",
		"@sveltejs/vite-plugin-svelte": "^3.0.0",
		"svelte": "^4.2.7",
		"svelte-check": "^3.6.0",
		"tslib": "^2.4.1",
		"typescript": "^5.0.0",
		"vite": "^5.0.3"
	},
	"type": "module",
	"dependencies": {
		"@sveltejs/adapter-static": "^3.0.1"
	}
}

```

### server/index.ts

```typescript
import express from "express";
import fs from "fs/promises";
import fs_sync from "fs";
import dotenv from "dotenv";
import OpenAI from "openai";
import { Assistant } from "openai/resources/beta/assistants/assistants";
import { Thread } from "openai/resources/beta/threads/threads";
import { promisify } from "util";
import { BOTS, type Bot } from "./bots";
import { OFFICES } from "./offices";

const sleep = promisify(setTimeout);


dotenv.config({
	path: "../.env"
});
const app = express();

interface Message {
	sender: string,
	bot_info?: Bot,
	text: string,
}

interface Action {
	text: string,
	change_bot?: string,
	link?: string,
	email?: string,
	phone?: string,
	send_user_msg?: string,
}

interface Conversation {
	id: number,
	handle: Thread,
	// TODO: user data
	mostRecentBot: string,
	messages: Message[],
	actions: Action[],
}

// TODO: improvement area: use redis or some other more concurrent-safe store
const convos: Map<number, Conversation> = new Map();
let id = 0;


const OPENAI_KEY = process.env.OPENAI_KEY;
if (!OPENAI_KEY) {
	console.error("OPENAI key env not set!!!!!!! im crying and going to sleep now");
	process.exit(2);
}

let assistant: Assistant;

const openai = new OpenAI({
	apiKey: OPENAI_KEY,
});

app.use(express.static("../app/build"));
app.use(express.json());

app.get("/", (req, res) => {
	res.send("heyyyyy :3")
});

app.post("/api/conversations", async (req, res) => {
	const thread = await openai.beta.threads.create();
	id++;
	const convo: Conversation = {
		id: id,
		handle: thread,
		mostRecentBot: "sammy",
		messages: [],
		actions: [{
			text: "How can you help me?",
			send_user_msg: "What is the Crew and how can you help me?",
		}],
	}
	convos.set(id, convo);
	res.json({ id });
});



app.put("/api/conversation/:id", async (req, res) => {
	console.dir(req.body)
	const { msg, bot } = req.body;
	console.dir(bot);
	const convo = convos.get(Number.parseInt(req.params.id));
	if (!convo) return res.sendStatus(404);
	if (!msg) return res.sendStatus(400);

	// TODO
	if (bot) {
		if (!(bot in BOTS)) return res.sendStatus(404);
		convo.mostRecentBot = bot;
	}

	let instructions = BOTS[convo.mostRecentBot].instructions;

	const message = await openai.beta.threads.messages.create(convo.handle.id, {
		role: "user",
		content: msg,
	});
	convo.messages = [{ sender: "user", text: msg }, ...convo.messages];

	let run = await openai.beta.threads.runs.create(convo.handle.id, {
		assistant_id: assistant.id,
		instructions,
	});

	while (run.status !== "completed") {
		run = await openai.beta.threads.runs.retrieve(convo.handle.id, run.id);
		// TODO
		await sleep(200);
	}

	const messages = await openai.beta.threads.messages.list(convo.handle.id);
	let actions: Set<Action> = new Set();
	for (const msg of messages.data) {
		if (msg.role === "user") break;
		for (const ct of msg.content) {
			if (ct.type !== "text") {
				console.dir(ct);
				continue;
			}

			convo.messages = [{ sender: convo.mostRecentBot, bot_info: BOTS[convo.mostRecentBot], text: ct.text.value.replace(/【\d+†source】/, "") }, ...convo.messages];
			const search_text = ct.text.value.toLowerCase();
			for (const bot in BOTS) {
				if (bot === convo.mostRecentBot) continue;
				for (const kwd of BOTS[bot].keywords) {
					if (search_text.includes(kwd)) {
						actions.add({
							text: `Talk with ${BOTS[bot].name}`,
							change_bot: bot,
						});
						break;
					}
				}
			}

			for (const office in OFFICES) {
				const o = OFFICES[office];
				// TODO
				const reduce = () => Math.random() < 0.4;
				for (const kwd of o.keywords) {
					if (search_text.includes(kwd)) {
						console.dir(o.link);
						if ("link" in o && reduce()) {
							actions.add({
								text: `Visit ${office}`,
								link: o.link
							});
						}
						if ("contact" in o && reduce()) {
							if ("email" in o.contact) {
								actions.add({ text: `Email ${office}`, email: o.contact.email });
							} else if ("phone" in o.contact) {
								actions.add({ text: `Call ${office}`, phone: o.contact.phone });
							}
						}

						break;
					}
				}
			}
		}
	}
	convo.actions = [...actions];
	res.json({ messages: convo.messages, bot: BOTS[convo.mostRecentBot], current_bot: convo.mostRecentBot, actions: [...actions] });
});

app.get("/api/conversations/:id", async (req, res) => {
	const convo = convos.get(Number.parseInt(req.params.id));
	if (!convo) return res.sendStatus(404);

	res.json({ messages: convo.messages, bot: BOTS[convo.mostRecentBot], current_bot: convo.mostRecentBot, actions: convo.actions });
})


app.get("/api/bot/:id", async (req, res) => {
	console.dir(req.params.id, req.params.id in BOTS);
	if (req.params.id in BOTS) {
		const { avatar, name, color } = BOTS[req.params.id];
		res.json({ avatar, name, color });
	} else {
		return res.sendStatus(404);
	}
});
app.get("/api/bots", async (req, res) => {
	res.json(BOTS);
});

app.get("/api/resources/:id", async (req, res) => {
	// TODO
});



app.listen(3639, async () => {
	console.log("Server running on port 3639");
	try {
		const assistant_id = (await fs.readFile("../assistant.id")).toString();
		assistant = await openai.beta.assistants.retrieve(assistant_id);
		console.log("Loaded assistant from id file");

		const ids = await openai.beta.assistants.list();
		for (const a of ids.data) {
			console.dir(a);
			if (a.id !== assistant_id)
				await openai.beta.assistants.del(a.id);
		}
	} catch (e) {
		console.warn(e);
		await fs.writeFile("../database.json", JSON.stringify(OFFICES));
		const db = await openai.files.create({
			file: fs_sync.createReadStream("../database.json"),
			purpose: "assistants",
		});
		console.log("Uploaded database file")


		const context = await openai.files.create({
			file: fs_sync.createReadStream("../sammy_support_crew.txt"),
			purpose: "assistants",
		});

		assistant = await openai.beta.assistants.create({
			name: "Secretary Sammy",
			model: "gpt-4-1106-preview",
			instructions: BOTS["sammy"].instructions,
			tools: [{ type: "retri
[truncated — 145 more characters]
```

### app/src/routes/+layout.ts

```typescript
export const ssr = false;
export const prerender = true;
```

### app/src/lib/index.ts

```typescript
// place files you want to import through the `$lib` alias in this folder.

```

### app/vite.config.ts

```typescript
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [sveltekit()]
});

```

### app/svelte.config.js

```javascript
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	// Consult https://kit.svelte.dev/docs/integrations#preprocessors
	// for more information about preprocessors
	preprocess: vitePreprocess(),

	kit: {
		// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
		// If your environment is not supported or you settled on a specific environment, switch out the adapter.
		// See https://kit.svelte.dev/docs/adapters for more information about adapters.
		adapter: adapter()
	}
};

export default config;

```

### static/index.html

```html
<!DOCTYPE html>
<html>

<head>
	<title>Chatter</title>
	<script>
		let id;
		async function cre8() {
			const sj = await (await fetch("/api/conversations", {
				method: "POST"
			})).json();
			console.dir(sj);
			id = sj.id;
		}

		async function send() {
			msg = document.getElementById("txt").value;
			console.dir(msg);
			const res = await fetch(`/api/conversations/${id}`, {
				method: "PUT",
				headers: { "Content-Type": "application/json" },
				body: JSON.stringify({ msg })
			})

			const json = await res.json();
			console.dir(json);
			return false;
		}
	</script>
</head>

<body>
	<button onclick="cre8()">Start Convo</button>
	<form onsubmit="send()" action="#">
		<input id="txt" type="text" required />
		<button type="submit">Send</button>
	</form>
</body>

</html>
```

### server/bots.ts

```typescript
export interface Bot {
	name: string;
	color: string;
	instructions: string;
	white: boolean;
	keywords: string[];
	avatar: string;
}

export const BOTS: Record<string, Bot> = {
	sammy: {
		name: "Sammy the Slug Secretary",
		color: "#fdc700",
		white: false,
		instructions: "You are Secretary Sammy, a chatbot that helps direct UCSC students to the right resources on campus.  Please answer with no markdown and as concisely as possible while still being welcoming and friendly.",
		avatar: "/sammy.png",
		keywords: ["sammy"]
	},
	sunny: {
		name: "Sunny the Social Squirrel",
		color: "#fb8842",
		white: false,
		instructions: "You are Sunny the Social Squirrel.  You give information on student life and engagement resources at UC Santa Cruz, such as the Ethnic Resource Centers, SOAR and SoMECA, the Disability Resource Center, STARS, the campus directory, and the financial aid office. You also provide financial aid information, such as the Financial Aid Office, and have information about the different colleges via the College Directory.",
		keywords: ["sunny", "ombud", "soar", "someca", "erc", "ethnic resource", "el centro", "aapirc", "airc", "aarcc", "cantu", "drc", "stars", "finaid", "financial aid", "financial bills", "financial aid offer", "cowell", "cowell college", "stevenson", "stevenson college", "crown", "crown college", "merrill", "merrill colelge", "porter college", "porter", "kresge", "kresge college", "oakes", "oakes college", "rachel carson college", "college nine", "jrl", "john r lewis college", "cantú"],
		avatar: "/sunny.png",
	},
	delbert: {
		name: "Delbert the Diligent Deer",
		color: "#32612d",
		white: true,
		instructions: "You are Delbert the Diligent Deer, who provides information on UCSC's many academic resources, such as the Office of the Registrar, Learning Support Services (LSS), Division of Undergraduate Education, Division of Graduate Studies, Institutional Research, Assessment, and Policy Studies (IRAPS)",
		keywords: ["delbert", "grades", "learning", "tutoring", "transcript", "academic", "academics", "lss", "undergraduate", "education", "babygirl", "drc", "office of the registrar", "division of undergraduate education", "division of graduate studies", "institutional research, assessment, and policy studies", "iraps", "learning support services"],
		avatar: "/delbert.png",
	},
	henry: {
		name: "Henry the Healthy Hammerhead",
		color: "#88ccee",
		white: false,
		instructions: "You are Henry the Healthy Hammerhead. You provide information about health and counseling resources at UCSC, such as Counseling and Psychological Services (CAPS), UCSC Campus Mobile Crisis Team, and Student Health Center, and promote sexual health with resources like the Student Health Outreach and Promotion (SHOP), The Cove, and  Slug Love. You also give information and resources for students encountering sexual violence or discrimination, such as the Title IX Office and the Center for Advocacy, Resources and Empowerment (CARE).",
		keywords: ["henry", "health", "counseling", "caps", "emergency", "ill", "sex", "condoms", "slug love", "student health center", "shop", "title ix", "sexual assault", "discrimination", "health records", "sexual harassment", "gender-based harassment", "medical", "medical appointment", "STI", "immunization", "psychiatry", "anxiety", "mental health", "crisis"],
		avatar: "/henry.png",
	},
	beatrice: {
		name: "Beatrice the Bus-driving Barracuda",
		color: "#63838d",
		white: true,
		instructions: "You are Beatrice the Bus-driving Barracuda. You provide information about transportation and bus routes. Some resources include: Transportation and Parking Services (TAPS).",
		keywords: ["beatrice", "car permit", "parking permit", "bus routes", "parking", "transit services", "parking", "bicycle", "bus passes", "taps", "transportation and parking services", "daily permits"],
		avatar: "/beatrice.png",
	},
};
```

### app/src/app.d.ts

```typescript
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
	namespace App {
		// interface Error {}
		// interface Locals {}
		// interface PageData {}
		// interface PageState {}
		// interface Platform {}
	}
}

export {};

```

[2 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]