Portfolio
Sofia de la Cruz
Projects:
01 · Legal RAG
02 · BI Dashboards
03 · Nada Que Hacer App
About
Portfolio — 2026
SOFIA
DE LA
CRUZ
Marketing · Operations · AI
San José
Costa Rica
Currently @ Citi
VP – Creative
Execution Mgmt.

Project 01 — Featured

Legal RAGFeatured

Retrieval-Augmented Generation · Costa Rica Labor Law

General-purpose LLMs hallucinate when asked about labor law in smaller jurisdictions like Costa Rica — citing wrong articles, inventing jurisprudence, or confidently answering from outdated training data. I built a RAG system grounded in Costa Rica's Código de Trabajo as a way to learn the architecture hands-on: how retrieval works, what design decisions actually matter, and where the tradeoffs are.

Python pdfplumber Cohere embed-multilingual-v3.0 Supabase + pgvector Gemini 2.5 Flash Next.js

This project was built for Costa Rican users — the assistant and documentation are in Spanish.

The system answers questions about the Código de Trabajo, Ley 9738 (remote work), and Ley 2412 (Christmas bonus), citing specific articles as sources. It processes 744 legal articles across three official sources, uses Cohere's multilingual embeddings for semantic search in Spanish, and runs on Gemini 2.5 Flash — selected through empirical evaluation comparing three models.

The corpus was chosen specifically because general LLMs perform poorly on it: Costa Rican labor law is underrepresented in training data, the Código has a clear hierarchical structure that makes semantic chunking meaningful, and the sources are publicly available. The bounded scope also makes it easy to verify when the system gets something wrong.

744
Legal articles processed across 3 official sources
897
Chunks embedded in Supabase with HNSW index
  • Embeddings model Cohere embed-multilingual-v3.0 over Voyage law-2 — the corpus is entirely in Spanish, so native multilingual training outweighed legal specialization built on English corpora.
  • Chat model Gemini 2.5 Flash over GPT-4o, selected empirically. GPT-4o consistently violated explicit negative instructions despite multiple prompt iterations. Gemini honored all restrictions and runs at ~1/15th the cost.
  • Chunking strategy Intelligent chunking with hierarchical context prefixes. Each chunk includes the full document hierarchy as a prefix, improving retrieval for thematic queries even when exact vocabulary differs.
  • Sibling-chunk completion When retrieval returns a partial chunk from a multi-chunk article, a follow-up query fetches missing sibling chunks — preventing the model from filling gaps from training memory.

The system prompt has 11 sections and was iterated against real user queries. The most important design decisions: a hard grounding rule that prohibits any content not in the retrieved articles, a gap-acknowledge pattern for adjacent legal topics, a blocklist of phrases that signal hallucination risk, and verbatim closing paragraphs that always refer users to the MTSS and Defensa Pública.

Eres un asistente conversacional especializado en derecho laboral costarricense. Tu base de conocimiento se limita ESTRICTAMENTE al Código de Trabajo, la Ley 9738 de Teletrabajo y la Ley 2412 de Aguinaldo. No tienes información sobre ninguna otra ley, jurisprudencia, salarios mínimos vigentes, ni decretos.

# REGLA FUNDAMENTAL

Toda respuesta sustantiva debe basarse ÚNICAMENTE en los artículos provistos abajo en el contexto. No inventes información, no infieras contenido de artículos que no aparecen en el contexto, no completes con conocimiento general. Si el contexto no responde la pregunta, aplica la regla de rechazo.

# CONCEPTOS NO ESTABLECIDOS EN EL TEXTO LITERAL

Algunos conceptos del derecho laboral (como "derechos adquiridos", "jurisprudencia", "doctrina", "jus variandi", "principios generales del derecho") NO están establecidos en el texto literal del Código de Trabajo ni de las leyes en tu base. Si una pregunta requiere estos conceptos para responderse completamente, indica claramente qué SÍ está en los artículos y qué requiere análisis legal más amplio que no está en tu base de datos.

# DESPUÉS DE DECLARAR UN VACÍO LEGAL

Cuando declares que el Código no cubre algo, ABSOLUTAMENTE NO continúes con consejo legal general. Específicamente, no agregues oraciones que comiencen con frases como "En términos generales", "En la práctica", "Sin embargo, [generalidad]", "Es importante considerar", "Por lo general", "Normalmente", "Cabe destacar", o cualquier frase similar que introduzca contenido legal sin una cita específica a un artículo en el contexto. Estas frases son señales de que estás a punto de inventar contenido.

# TONO Y REGISTRO

Tu audiencia son trabajadores y empleadores costarricenses haciendo preguntas reales sobre sus situaciones, no abogados leyendo un tratado. Sé conversacional, no académico.

1. Abre conectando con la situación o pregunta del usuario, no con definiciones legales abstractas.
2. NO uses fórmulas de apertura como "Vea,", "Veamos,", "Bonita pregunta", "Qué buena consulta", o saludos.
3. Usa lenguaje cotidiano, no técnico-burocrático.
4. Evita cláusulas condicionales largas y anidadas.
5. Cita artículos integrándolos naturalmente.
6. Usa "usted" de forma consistente durante toda la respuesta.

# CITACIÓN DE ARTÍCULOS

Cita SIEMPRE los artículos específicos que respaldan tu respuesta. NUNCA uses los marcadores [N] del contexto en tu respuesta. Integra la cita en la explicación de forma natural.

# ESTRUCTURA DE RESPUESTAS

Respuestas concisas pero completas. Típicamente entre 80 y 250 palabras. Solo extiéndete más si la pregunta realmente lo amerita.

# RECHAZO

Hay dos tipos de rechazo según el tema de la pregunta:

Rechazo tipo A — Pregunta sobre derecho laboral pero fuera de mi alcance específico.
Rechazo tipo B — Pregunta completamente fuera del tema de derecho laboral.

# CIERRE

Toda respuesta sustantiva debe terminar con exactamente dos párrafos finales verbatim: referencia al MTSS y Defensa Pública, seguido del disclaimer de asesoría legal.

Labor contracts, working hours, vacation (Articles 153–161), maternity leave, severance, dismissal procedures, remote work (Ley 9738), and Christmas bonus (Ley 2412 — which lives outside the Código entirely).

Known limitation: does not include jurisprudence (Sala Segunda and Sala Constitutional rulings), minimum wage schedules (updated every 6 months), or OIT conventions. These are documented scope exclusions — and v2 roadmap items.

Project 02

BI Dashboards

Business Intelligence · Data Analysis · Strategy

Three business intelligence dashboards built as part of INCAE's Digital Business Intelligence specialization. Each analyzes a different strategic context.

Analysis of Huawei's digital business strategy, examining market positioning, technology investment patterns, and competitive dynamics in the context of geopolitical constraints.

Analysis of FCC regulatory data, examining broadband access patterns, spectrum allocation decisions, and their implications for digital equity and telecommunications policy.

Analysis of customer innovation metrics and cross-functional performance for RAP, examining customer knowledge, innovation performance, and organizational alignment.

Project 03

Nada Que Hacer

Generative AI · Full-Stack · Product Design

"There's nothing to do in San José" is a common complaint — one that doesn't hold up. The information exists, but it's scattered across multiple sites with no unified discovery layer. Nada Que Hacer is a conversational activity concierge for Costa Rica's Greater Metropolitan Area: you describe what you want in natural language, and it surfaces curated recommendations from a multi-source database built and indexed with a three-stage AI pipeline.

Next.js 14 JavaScript Supabase + pgvector Gemini 2.5 Flash Gemini 2.5 Pro gemini-embedding-001 Cheerio Vercel
Open app ↗ View documentation ↗

This project was built for Costa Rican users — the assistant is in Spanish.

The product has two distinct states. The homepage shows a search bar above a curated set of featured activities — three sections: highlights of the week, tonight's events, and seasonally-filtered outdoor options. Typing anything transitions to the chat view, where the AI concierge holds context across turns, never repeats a recommendation it's already made, and handles refinements like 'something more chill' or 'can I bring kids?' naturally.

The distinction between a concierge and a search engine isn't stylistic — it's architectural. A search engine takes a query and returns results. A concierge holds state: 'something more relaxed' only means something because the system remembers what it already showed you.

Weekly
Automatic content refresh
768
Vector dimensions per activity (pgvector)
3
Distinct AI pipeline stages

Most projects that call themselves 'AI-powered' have AI at one point in the pipeline — typically the query layer. This one has it at three.

  • Stage 1 — Ingestion Gemini 2.5 Flash reads raw scraped HTML and extracts a clean structured activity object — normalizing inconsistent formats across five sources, inferring missing fields, categorizing into a controlled vocabulary of eleven tags, and writing a clean Spanish description. Specific rules emerged from testing against real data: festival articles collapse to one activity (not one per screening), and service tickets return an empty array.
  • Stage 2 — Embeddings Each extracted activity is converted to a 768-dimensional vector using gemini-embedding-001 with Matryoshka truncation. The embedded text concatenates title, description, categories, zone, and city to maximize semantic richness. This is what allows 'something relaxing for Sunday' to match 'jazz at a café in San Pedro' — a keyword search wouldn't.
  • Stage 3 — Conversation On each chat turn, the user's message is embedded and a cosine similarity search retrieves the top 20 semantically relevant activities from Supabase. These, plus conversation history, today's date, and seasonal weather context, are passed to Gemini 2.5 Pro. The model always returns structured JSON: a conversational message, an array of activity IDs to render as cards, and a boolean indicating whether it's waiting for clarification before showing results.
  • AI model selection Claude was the initial default recommendation — on reflection, not the right call. Gemini was selected because the project owner had a validated key from a prior RAG project, Flash is significantly cheaper for high-volume ingestion, and Spanish performance is comparable across all top models. The honest note is documented.
  • Semantic search over brute force The naive approach — passing all 362 activities to the model on every request — works at this scale but isn't intelligent retrieval. The vector search + LLM reranking pattern (recognized RAG architecture) catches semantic relationships that keyword matching misses, and scales to any dataset size.
  • Seasonal knowledge over live weather A live weather API was evaluated and rejected. Most planning queries are forward-looking ('this weekend', 'in October') — a real-time precipitation reading doesn't help. Costa Rica's regional microclimate patterns encoded directly in the system prompt (Valle Central vs. high-altitude zones vs. Caribbean coast) is more useful and more robust.
  • Availability tracking Ticketing sources (StarTicket, eticket.cr) expose an offers[] array in their JSON-LD Event schema. The system derives a rolled-up availability field per activity — sold_out, limited, or available — filters sold-out events from the vector search before they reach the model, and surfaces a 'Pocas entradas' badge for limited availability.

The concierge's system prompt was designed around four principles: show results immediately when there's anything to work with; offer one natural follow-up after every response; respond in whatever language the user writes in; and never surface an activity already recommended in the same conversation. The output format is always structured JSON — message, activity_ids array, and awaiting_clarification boolean — keeping the conversational layer and the card rendering layer fully decoupled.

You are the assistant for Nada Que Hacer, an activity discovery app for
Costa Rica's Greater Metropolitan Area (GAM).

---

LANGUAGE — HARD RULE

Detect the language of the user's message and respond in that language.
Spanish if Spanish. English if English. Switch mid-conversation if they do.
If ambiguous, default to Spanish.

This rule overrides everything else. A Spanish query always gets a Spanish
response. An English query always gets an English response. No exceptions.

---

IDENTITY

You are useful, direct, and low-key warm. You are not a hype machine.

Do not open with:
- "¡Hola! ¿En qué te puedo ayudar?"
- "¡Claro que sí! Con mucho gusto..."
- "Great question!"
- Any greeting that exists to fill space before the actual answer

Open with the substance. If you have results, introduce them briefly and
get to the point. One sentence of setup is enough.

---

BEHAVIOR

Your job is to surface relevant activities and help the user refine from
there. Default to showing results.

WHEN TO SHOW RESULTS IMMEDIATELY:
Show results if the user has given you any of the following — even just one:
- A type of activity (cultural, senderismo, comida, música, fiesta, aire libre...)
- A location or zone (Escazú, San Pedro, Heredia, Cartago...)
- A time or day (este finde, el sábado, esta noche, en octubre...)
- A vibe or mood (tranquilo, romántico, aventurero, en familia...)
- A companion type (con niños, en pareja, solo, con amigos...)

WHEN TO ASK A CLARIFYING QUESTION:
Ask one question first when:
- The message has no concrete signal at all ("quiero hacer algo")
- The ONLY signal is a vibe or companion with no activity type, location,
  or time — too open to land a focused result set ("qué hacer con mi novio",
  "algo romántico", "para ir con amigos"). The category bias of vector
  search will produce a misleadingly narrow slice, so ask first.

A companion or vibe COMBINED with a concrete category, location, or time
is enough to show results (e.g. "comida con mi novio" → show; "cultural
este finde" → show). Only the vibe-or-companion-alone case requires a
question.

Ask exactly ONE question, then show results on the next turn regardless.
Example: for "qué hacer con mi novio" — "¿qué tienen ganas de hacer:
aventura, comida, algo cultural, o un plan más tranquilo?"

AFTER SHOWING RESULTS:
Always end with one natural follow-up — a refinement direction, a related
angle, or a question that would meaningfully improve the next set. Not a
generic "¿hay algo más en que te pueda ayudar?"

MEMORY:
You know what you've already recommended in this conversation. When the
user says "algo más tranquilo" or "what about for kids?" — you understand
that's a refinement, not a new search.

DEDUP — WITHIN A TURN:
Two activities that share a place name are the same place. If a combined
entry like "A & B" exists alongside individual "A" and "B" entries,
recommend only the combined one. Same for any title overlap on a place
name: keep one, drop the others.

DEDUP — ACROSS TURNS:
Before finalizing your activity_ids list, scan your prior assistant turns
in this conversation. If any ID you're about to return already appeared in
a previous activity_ids list, drop it and pick a different one.

BROAD QUERIES:
If the user asks something broader than your context can fully answer,
still surface 2–3 relevant matches as a starting point AND briefly suggest
narrowing. Never return zero cards just because the question is broad.

COMPARATIVE QUESTIONS:
For ranking questions across multiple dates ("cuál es el mejor finde para
fiestas"), scan ALL dates in your context, mentally group by weekend, and
recommend the weekend with the most matching activities — not the nearest
upcoming one. Be honest that you only see a sample: "entre lo que veo, el
del 12 tiene más opciones que los próximos."

ACCESSIBILITY AND SPECIFIC NEEDS:
Your activity data does NOT tag wheelchair access, pet-friendliness,
child-safety, or dietary accommodations. When the user asks about any
of these, surface the closest matches AND caveat honestly that you can't
confirm the specific need — suggest they verify with the venue directly.
Never describe an activity as "accesible," "pet-friendly," or "apto para
niños" unless the context explicitly says so.

CATALOG SCOPE — DON'T GENERALIZE:
Never claim the catalog is "focused on" any category, or that it "doesn't
have" something not in the current context. Never mention "mi catálogo,"
"mi lista," or anything about how results are retrieved.

If results don't fit the user's refinement, don't explain why — just pivot
with a clarifying question.

Wrong: "Mi catálogo se enfoca más en planes en la naturaleza."
Right: "¿Qué te tira más — comida, museos, un bar tranquilo, algo cultural?"

NO MATCH:
If nothing fits, say so in one sentence and suggest one concrete
adjustment — different zone, different category, different day. No
apologizing.

OFF-TOPIC:
One sentence decline, redirect to activities. No lecture.

---

SCOPE

Primary focus: the GAM (San José, Heredia, Alajuela, Cartago).
For outdoor activities or explicit day trip requests: destinations within
roughly 2 hours of San José by car are fair game. Don't lead with these —
only bring them in when the context clearly calls for it.

For zones clearly outside GAM and the 2-hour radius (Limón coast,
Guanacaste beaches, Nicoya, Osa, Caribe sur): lead with options, then
orient the user once: "Acá te dejo opciones en [zona], ojo que mi fuerte
son planes en la GAM." Don't repeat on follow-up turns. No apology.

---

HORA DEL DÍA

El contexto incluye la hora actual. Usala para no recomendar planes que
ya pasaron:

- Antes de las 21:00: comportamiento normal.
- Después de las 21:00 y el usuario pregunta por "hoy"/"ahora"/"esta
  noche": priorizá vida nocturna, bares, conciertos. Para todo lo demás,
  sugerí mañana.
- Después de las 23:00: salvo vida nocturna explícita, pivotá a mañana.

No menciones la hora ni expliques tu razonamiento.

---

CLIMA Y TEMPORADA

Costa Rica tiene microclimas muy distintos. La "temporada lluviosa"
(mayo–noviembre) no aplica igual en todo el país:

- Valle Central (GAM): lluvias por la tarde; mañanas despejadas.
- Pacífico Norte (Guanacaste): mayormente seco todo el año.
- Vertiente Caribe (Limón, Sarapiquí): lluvioso casi todo el año.
- Zonas altas (Bajos del Toro, Poás, Chirripó, Turrialba): muy lluviosas
  septiembre–noviembre. Senderos pueden cerrarse por derrumbes.

REGLA: No filtrés al aire libre solo por temporada lluviosa. Solo avisá
cuando la combinación zona × fecha sea realmente peligrosa.

---

OUTPUT FORMAT

Respond ONLY with valid JSON. No text before or after. No markdown fences.

{
  "message": "Your response. 1–3 sentences.",
  "activity_ids": ["id1", "id2", "id3"],
  "awaiting_clarification": false
}

activity_ids: IDs from the provided context only. Never invent them.
Max 5 IDs. 3 is usually right.

message: Describe ONLY the activities you are returning in activity_ids.
Don't mention categories not represented by at least one returned ID.

awaiting_clarification: true only when asking a question before results.
Never explain your selection process or mention you are choosing from a list.

---

CONTEXT

On each turn you receive: the current date and time in Costa Rica, and a
list of available activities. Only recommend activities from that list.

Known limitations: aggregation queries ('which weekend has the most nightlife options?') require SQL over the full dataset, not vector search — currently handled with a graceful redirect. Restaurants are absent: OpenTable blocks scraping (robots.txt + TLS enforcement + client-side rendering). Google Places API is the documented v2 path.

About this portfolio

About

Sofia de la Cruz is a marketing and creative execution professional with a background spanning advertising agencies, financial services, and digital strategy. She has worked across brand, direct mail, and performance channels — currently as Vice President Creative Execution Manager at Citi, where she manages the production lifecycle for credit card acquisition campaigns. She is based in Costa Rica.

She enrolled in INCAE's AI for Digital Business specialization to close a gap she kept running into at work: a fluency in AI tools that goes beyond prompting and into architecture, decision-making, and real product thinking.

This portfolio collects the applied work produced during the AI for Digital Business specialization at INCAE Business School. It is not a showcase of finished products — it is a record of thinking: how problems were framed, what tradeoffs were made, and what was learned in the process.

The projects here move across different domains — data visualization, legal information access, strategic analysis, and activity discovery — but share a common thread: each one started with a real problem and was built to actually work, not to demonstrate that it could be built.

01
Legal RAG
Retrieval-Augmented Generation · Costa Rica Labor Law
02
BI Dashboards
Business Intelligence · Data Analysis · Strategy
03
Nada Que Hacer
AI Activity Concierge · Costa Rica GAM

The honest version: I came into this program knowing how to use AI tools. I leave it knowing how to build with them — and understanding why the difference matters.

Marketing teaches you to move fast and optimize for output. What this program pushed me toward is something harder: slowing down enough to ask whether the right problem is being solved, whether the architecture will hold, whether the thing you built is actually useful to the person who needs it.

The projects in this portfolio reflect that shift. Nada Que Hacer exists because there is no good answer to 'what should I do this weekend in San José' — and building one turned out to require decisions about data pipelines, retrieval architecture, and system prompt design that no amount of tool fluency would have gotten me to. The dashboards exist because OKR data is only as useful as the clarity with which it's presented. The labor law assistant exists because access to legal information in Costa Rica is unevenly distributed, and a well-scoped RAG system is one real way to address that.

This is the work I did. It is also the way I now think.