From ce91ff46e0c89417589f0f4585fedd3c2850f594 Mon Sep 17 00:00:00 2001 From: Cailean Finn Date: Sun, 26 Jul 2026 14:17:11 +0100 Subject: [PATCH] add: worksop entry & end date for events/workshops & summary change --- AGENTS.md | 49 +++++++++++++++++++ README.md | 2 +- __pycache__/agent.cpython-312.pyc | Bin 2152 -> 2302 bytes __pycache__/database.cpython-312.pyc | Bin 4289 -> 4869 bytes __pycache__/prompts.cpython-312.pyc | Bin 2372 -> 3897 bytes __pycache__/schemas.cpython-312.pyc | Bin 1659 -> 2267 bytes agent.py | 19 ++++++-- bot.py | 70 ++++++++++++++++++++++----- database.py | 25 +++++++--- prompts.py | 42 +++++++++++++--- schemas.py | 11 ++++- 11 files changed, 189 insertions(+), 29 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7cfd551 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,49 @@ +# AGENTS.md + +## Run +``` +python main.py +``` +Uses `runpy` to exec `bot.py`. No tests, no lint, no CI. + +## Architecture +- `bot.py` — Telegram bot (python-telegram-bot), single-worker `asyncio.Queue`, all crawl/parse serialized +- `agent.py` — pydantic-ai with local Ollama (`granite4.1:8b`), three agents (opportunity/event/workshop), `output_type=str` +- `database.py` — PocketBase client, collections `events` / `opportunities` / `workshops` +- `scraper.py` — crawl4ai `AsyncWebCrawler` URL → markdown +- `prompts.py` — system prompts for both agent types +- `schemas.py` — **unused at runtime**, per file comment: "File doesn't do anything, its just an outline" + +## Windows event-loop quirk +DO NOT remove the dual-event-loop pattern in `bot.py`: + +1. PTB runs on `WindowsSelectorEventLoopPolicy` (line 357) +2. Scraper runs in a **separate thread** with `WindowsProactorEventLoopPolicy` (line 25) — required for Playwright subprocesses on Windows + +The `get_clean_content()` wrapper in `bot.py` runs `_run_scraper_in_thread` via `ThreadPoolExecutor`. Scraper functions must not be called directly. + +## Agent output format +Agents use `output_type=str` (not structured output). JSON is parsed manually from `agent.run()` result: +- LLM returns JSON wrapped in `````json ... ````` markdown backticks +- `agent.py:56` strips the backtick wrappers before `json.loads()` +- The markdown wrapper is specified in `prompts.py` — don't change prompt format without updating the strip logic + +## Date formats +- Prompts instruct LLM: `DD-MM-YYYY` (or `DD-MM-YYYY (HH:MM)` for events) +- PocketBase expects: `YYYY-MM-DD HH:MM:SS` +- `database.py` field mapping: event `date_time` → PocketBase `datetime`, `end_date` → `end_datetime`; workshop same; opportunity `deadline` stays `deadline` + +## PocketBase collections +- `events` — fields: `title`, `org`, `datetime`, `end_datetime`, `summary`, `location`, `url` +- `opportunities` — fields: `title`, `org`, `type`, `deadline`, `summary`, `location`, `url` +- `workshops` — fields: `title`, `org`, `datetime`, `end_datetime`, `summary`, `location`, `url` +- Auth uses admin credentials from `.env` + +## Env vars +`.env` required: `TG_TOKEN`, `OLLAMA_BASE_URL`, `ALLOWED_USERS`, `POCKETBASE_URL`, `POCKETBASE_ADMIN_EMAIL`, `POCKETBASE_ADMIN_PASSWORD` + +## Auth +`ALLOWED_USERS` — comma-separated Telegram user IDs (no brackets). Checked on every command/message handler. + +## Save flow +When source is pasted text (not URL), the bot asks for a source URL before saving. `context.user_data` keys: `last_extracted`, `last_source_value`, `last_source_kind`, `last_entry_type`, `awaiting_save_url`, `pending_save_url`, `pending_url_to_process`. diff --git a/README.md b/README.md index ed0d550..85ee7c1 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ POCKETBASE_ADMIN_PASSWORD=secret 4. Set `OLLAMA_BASE_URL` in your `.env` to point to the running API, for example: ```text - OLLAMA_BASE_URL=http://localhost:11434/v1 + OLLAMA_BASE_URL=http://localhost:11434 ``` 5. Verify the API is reachable (example curl): diff --git a/__pycache__/agent.cpython-312.pyc b/__pycache__/agent.cpython-312.pyc index 59a6e04037bd166fe79b5fafcdfd26af38b3a634..16780678629f6d7730801a3597ee4edbb32cef24 100644 GIT binary patch delta 976 zcmZuwO=uHA6rS1H$?j&eo83*5+8-;eB8{!}pr95J`v*dimRb>`rHHfErav1tY13k2 zi-_1;%77J$ccsyzNDo5WqZcg*A=HE5p~tE{co1slMqbvB2s&JD9a@;uA{qn&uq7g8Ll55b-4{9RUO>A*z)0icbNS zLDPb~fFB|No(n7JDaIg;QYc!umr5#+(&C{eo7GQ)6yd;36o$B-2zGY1|?zGN7vB@DUa=0 zdpbeq#Q8QgZ#uD<<0fXN#}o6TcgL8JR||R(=t;+&h(B;*)Q9^$0oK*6p(D5#AMNeD z6Qk?+J*`_afM{fNW((wxyOGqj?GElt3oAtu*Zz4NLgD(ew;ch1uWZCu#w2=s)|7BqTHv}Ix*hY;h#&<(Nba4r4g z8LMkyVP1;4&QvTBclk-UGB*lMN~g}%^RR&%{!CoH+t=V1v%qa*CNE!_a>&f2ecA0~ zkUy;WTlz_A^!0rPecynZ8U6z5=}&3&i~+3LoT%iCK+bB;`LulnTf(CLLq86U7x(r6 ObFbq2+_@LoGXDTdG`!IO delta 794 zcmZuvO=uHA6n?X_yR*qAo8A1_Dm5iqwNXKY)I$kkn+Sp+Dimo6h;&wQ`;)L6NlUX@ zV)5KEV3pc?CGl)8_KB|Z+AVjVkVy^)eSI^AJnR2SQNw4;Z(p6V82RHtFnpqgpi7inZgnu;$wH?iL{f~sHEgiV!f83&_`nz{CB~Yo`ec|gDWVlvZ%PjzKV+|$Zkm~ zR+I9w!FRT!?4y)FdNDUzm~c$nDNNscIBOR2rZZD6mI^mnLOy+xKQNr))A_{1KKx#W zRuoR0-*P{vvR^{S-=AB)%I4+mu?#QQMsgwa7L5;z+hY+9o63>#6f~29hf}d!05>yw zPQ<&S#PMzbb3KjhIJc$~%pwosNe=f&$u7>7%8pGqvlaH8oW0QJ!G14z4ZVh+Jc50O zhdm`bSEP?-&CB*BPQ6d+tg>s$fSmaT>PncEl)HUe66r6dTfozn^UF*22w(UYG;5FO diff --git a/__pycache__/database.cpython-312.pyc b/__pycache__/database.cpython-312.pyc index f9c3d3c94d62e17e5229e8e64f8688c3cdf72109..f242cd6cbe248aea86a5566afda31982299ef6a0 100644 GIT binary patch delta 1060 zcmb7DO-vI(7@aBo*>?Zh(zT@`wm>OO2vyXQK!HF6HStnKkVYiX*~BWPb-Gy5`g6d< zW1BhH#3*roK>REWi!;zCk^``t8}z{v6Tc#397W49=+WA0C@ZO>!KIF$uA?325@bc!1zpQO zbnQ?hXvslC3O*zYhPy$;r@<9~ZHh`Lk2FBMDp9F|Vj)Pey59wq-?agNMAF6hp_#;( zL(oDVF*a)&BI~7Q05C>|ZCSEvX!if-86;p#lm#VLDyy{7qb2g3scx74C`?idQ}pG! zX~6DXvQnZknLK4@Q8c1pmukzRVohSAWQxQmC*_C|jZc;I5iHY_utUvI z`+}V`_!vqYSH?>wc}l@EW6DfI#tmeGZ#hy|NlY?yRsJ|=!22mEanp&(_&6FXlW>4; zwbCaU)sPqbiMGQ!p~y-(`v&VOiq0G=i1o#~`uWa6oj=2FHT14Eu3azmo!)F1%xE*= zg4tCh-9nr9gIJRrEQszcZ~q#4)lnE2+Vl?7jPnI^W6|cwIrFy0kDP@}3!S%$ygk>k z!FwwDe^sTNy?LAGA9eG#Q2x|VJ~W)So_)iOY)STequJ5<&P~bxP6`yHz>=~l1@8`Q zah5!p6W;rpsO2Yp^~b8gi<;g5onD&zG;~_8HXhejt=Bdj>#17r(H+;4IWYuV$yc$( zuU^geC8gee(-C C9~Gkj delta 632 zcmY*WO=uHA6rQ))O*Xrk-AyDWtu<|;HU?_b)P_p`&}t6qL5uWY2ukSWP)(b#n;I)y zy+rXKVtgJ7LGUDmoTOmSDmfLwdMdrxgO>_|pk6#V+n9q7=FRs$zQ_0G&BMWWQEiJ- zh446KlUq%ru7yFY$Il5ZQeQp+?)r;)@YKIW6LJe26(KF5b7y_bo25*tk*vmiOszpy zUxft4F__tS0LR6XVJ@^Jo(7!wGj@I=H*Xb8E4PqeEEaMX3fApPshqoto6;ht+! z#j!q!F4ygF>gUQ4)o)7;!W5^(K_#jLx&pw<-ZJR;UP%Rmw)=lKc9?}wR^$HAn)|-H z3RQB~b)8fp5#BhfW`VageB;8s3Czfb^D*4lhSEp7tR-`Fusoj32rqVhE9GJ-&u$i$E9R=1;?yud)+XR3{bD38X*+6g zJ@i=}*wcI0*|r|riNsz_Zby>O)UWtPKLmT&)<>mZZbAbXF(z@}_!x60dONFRMDhV< z1%9h|8ax;q)S|YrKPVKT-R@4CvAa9V%xp^z zQcr?-vCP4%S8tV`JP01V`3DG|`X3nSO%Qx*n$(kZVV8MtzWMfj-+S|U;KNw|m%ctd zfzROAm8YLB?Dr3k+ zEC>|luG9)ANGiYv7LX!hfb5V!0^<;{m=e!P0*DYHap)!`uBlI}k6Kwp*}PUce35;w zjdUw9s$i7MMXORQTjdIrZm3`9j6vDtq{A&*e#iukd}ZWbrR+o|R+-ynd|NbGxc~O0 zQkj8XtZG_C9cA)8=|iVmDCFNh3{7-t&+y~!8GdwW{=<@sJ#6gg28*Pr#}18Zd$WP$ z%#9pLSpbqjoo-QSd$8z4UMM3@j42(!V0%$v}_k)#gAL6ZV@spo+cS5A0RPDzF$Ab>(wWrI?i35&S9wGD<<&MPHCFf&W&XBb}FF_wr>r0R(W__6Eku zH+i`yzvAFyPD!ks%+1-&?5D{zc^;pV;!1|kAbWoqJ6pws7N-^!$0R3a=A_2Bq!wqF za#&%6c=npV@7qj_w+F`d>2qP?5$QCMsov0*iu_6lKOIVUES7bX`NeU=|RuL9| zMpz>9z8IHR6z-LoH%XMfSLI%XdDQ`LihEPcn;!6{i9l46BI%m=E^AuJ1etvHPiUVP zBl)Rgx=y65nLeS06{+hDpE(RGpNLW`hDRQ|m{^hgz$6x~#%qePyFhgMoY}7sK@|a` zO5!Qo1`)Avkk}XE#0uLENn&wN>nx*Q2zq+b(We@SN54@0*4dN2tQq zOtTCk(er6Etm}s3xITZcr|WbG;#U~#p;LzFuiM4XHXGF4CRN|tbefhm``q<6y=tA< zhBwy;XbLI-lI-Vw%-|(D8GYw)aycAbX_s~i$I=S7KMhCkwF_Y14`GM|m)(hrP{NGALy`9XuUZWaxnXoiC|-#q7>XHbw}l)@}Q zhhVuiSz>u$pBI=l&2C!0Ig5=z<7In^NWr9LFfYwO2bDNjhpN`~HMh!%>djWX64mOu z_p)@DRnQTDs{l6v;9sQI066*M@H7f=3jmG-dK+L2fD@5}ryD(hRd}lmUZUS8rrX7i zva1}8Xvfm@NpAe`PBF}t+7CP8u~a(Ire5R2rCO+MwZH7ByVrghI+nJ0))!%JiTT7( zT8iHR?Jto1-^(_$ey+{dBB&5Jg+F&dwA!8Z26424VOcZUgQkN`XKP0Lns_E=!VG3K z;#cSLoNWGgx8^oLpG=*geoPE(nGVrRhubxm+APXmf$^+u&jvxH_HtTR2l-{AMgvX^ zllEm^Igy-PP9!IH0{HqTBHxDq0E|RWpupT#n5#278FO{c(YIml8E|wgJ>&NMFt-qM zwh(g$O)hZu{}QOW;nMHCy4#4<7u~0rl8Gn2LH$e8>z>d3Opq%Dag^wX} ra6ik6!eS>6p+$%kg`EH*Md5804Uct7yZ2wKhhy_cS3deF!?*P}T)8Ez delta 717 zcmZuuzi-n(7`^k^=d;teDTL6ZfYeH*n5|-?ElEWqK#C$Nn#GEQqWAzq>_lfXcnU}i zjEOwZe*u++e}R>yNM&({&TOs71Pk{a0-=f@xsqJ=X0@WBY8dz(er&W zbr#2oWbcrVd^|uQ_Hd|q8V0TnNT_>yXm|!j8oGyk@&@@j*AEDEK**y?HKa;8{i~Wv zHKoe_Q5nbF6T>Bd|DY;s1bfRDw=vTt}PrXi+O+&0Bj(1fC~W1YYn0W zfF*#70BZgvh%Wzuf(+vIY)@aUR+bKL)Q>8SSNA96NN@bkhQ#UFI9oGX6wHWfmKwWl z=-GCy^G4KUaOl4r@H=8froxlQI}%O_#`pv+pP>5cj1k str: result = await loop.run_in_executor(pool, _run_scraper_in_thread, url) return result -load_dotenv() +load_dotenv(override=True) logging.getLogger("httpx").setLevel(logging.WARNING) # Configuration @@ -103,20 +103,46 @@ def build_url_choice_keyboard(url: str): return InlineKeyboardMarkup([ [InlineKeyboardButton("📅 Process as Event", callback_data='choose_type:event')], [InlineKeyboardButton("📋 Process as Opportunity", callback_data='choose_type:opportunity')], + [InlineKeyboardButton("🛠 Process as Workshop", callback_data='choose_type:workshop')], ]) def build_entry_summary(data, entry_type, saved=False): if entry_type == "event": event_datetime = data.get('date_time') or data.get('datetime') - return ( - f"✅ **{data.get('title', 'Unknown')}**\n" - f"🦆 Org/s: {data.get('org')}\n" - f"📅 Date & Time: {event_datetime}\n" - f"📍 Location: {data.get('location')}\n" - f"🐊 Summary: {data.get('summary')}" - + ("\n\n💾 **Saved to PocketBase!**" if saved else "") - ) + end_date = data.get('end_date') or data.get('end_datetime') + lines = [ + f"✅ **{data.get('title', 'Unknown')}**", + f"🦆 Org/s: {data.get('org')}", + f"📅 Date & Time: {event_datetime}", + ] + if end_date and end_date != 'N/A': + lines.append(f"📅 End Date: {end_date}") + lines.extend([ + f"📍 Location: {data.get('location')}", + f"🐊 Summary: {data.get('summary')}", + ]) + if saved: + lines.append("\n💾 **Saved to PocketBase!**") + return "\n".join(lines) + + if entry_type == "workshop": + event_datetime = data.get('date_time') or data.get('datetime') + end_date = data.get('end_date') or data.get('end_datetime') + lines = [ + f"✅ **{data.get('title', 'Unknown')}**", + f"🦆 Org/s: {data.get('org')}", + f"📅 Date & Time: {event_datetime}", + ] + if end_date and end_date != 'N/A': + lines.append(f"📅 End Date: {end_date}") + lines.extend([ + f"📍 Location: {data.get('location')}", + f"🐊 Summary: {data.get('summary')}", + ]) + if saved: + lines.append("\n💾 **Saved to PocketBase!**") + return "\n".join(lines) return ( f"✅ **{data.get('title', 'Unknown')}**\n" @@ -194,8 +220,9 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): "Welcome! I can extract arts opportunities and events.\n\n" "📋 **Commands:**\n" "/op - Extract an opportunity\n" - "/ev - Extract an event\n\n" - "You can also send a URL directly and I will ask whether to process it as an event or opportunity." + "/ev - Extract an event\n" + "/ws - Extract a workshop\n\n" + "You can also send a URL directly and I will ask whether to process it as an event, workshop, or opportunity." ) async def handle_opportunity(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -238,6 +265,26 @@ async def handle_event(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("📥 Link queued for processing...") await task_queue.put((update, context, input_text, "event", source_kind)) +async def handle_workshop(update: Update, context: ContextTypes.DEFAULT_TYPE): + user_id = update.effective_user.id + if user_id not in ALLOWED_IDS: + await update.message.reply_text("Unauthorized. User ID needs to be added!") + return + + if not context.args: + await update.message.reply_text("Please provide a URL or paste text. Usage: /ws ") + return + + input_text = " ".join(context.args).strip() + if not input_text: + await update.message.reply_text("Please provide a URL or paste text. Usage: /ws ") + return + + source_kind = "url" if input_text.startswith("http") else "text" + + await update.message.reply_text("📥 Link queued for processing...") + await task_queue.put((update, context, input_text, "workshop", source_kind)) + async def handle_followup_text(update: Update, context: ContextTypes.DEFAULT_TYPE): if update.effective_user.id not in ALLOWED_IDS: return @@ -327,6 +374,7 @@ async def _main(): application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("op", handle_opportunity)) application.add_handler(CommandHandler("ev", handle_event)) + application.add_handler(CommandHandler("ws", handle_workshop)) application.add_handler(MessageHandler(filters.TEXT & (~filters.COMMAND), handle_followup_text)) application.add_handler(CallbackQueryHandler(button_handler)) diff --git a/database.py b/database.py index 558d56c..a50fdff 100644 --- a/database.py +++ b/database.py @@ -4,7 +4,7 @@ from pocketbase import PocketBase from schemas import EntrySchema from datetime import datetime -load_dotenv() +load_dotenv(override=True) pb = PocketBase(os.getenv('POCKETBASE_URL')) admin_data = pb.admins.auth_with_password(os.getenv('POCKETBASE_ADMIN_EMAIL'), os.getenv('POCKETBASE_ADMIN_PASSWORD')) @@ -62,23 +62,36 @@ def upload_entry(data, entry_type='opportunity', url=None): try: if entry_type == 'event': - # Map 'date_time' from agent to 'datetime' for PocketBase if 'date_time' in data: original_dt = data['date_time'] - # Convert and map to PocketBase field name data['datetime'] = convert_datetime_to_pocketbase(data['date_time']) - # Remove the original field since PocketBase expects 'datetime' del data['date_time'] if show_debug_msg: print(f"[DEBUG] Event datetime: '{original_dt}' -> '{data['datetime']}'") else: print(f"[WARNING] No 'date_time' field found in event data") - - # Upload to events collection + + if 'end_date' in data: + data['end_datetime'] = convert_datetime_to_pocketbase(data['end_date']) + del data['end_date'] + print(f"[DEBUG] Creating record in 'events' collection with data: {data}") result = pb.collection('events').create(data) print(f"[DEBUG] Successfully created record: {result}") return result + elif entry_type == 'workshop': + if 'date_time' in data: + data['datetime'] = convert_datetime_to_pocketbase(data['date_time']) + del data['date_time'] + + if 'end_date' in data: + data['end_datetime'] = convert_datetime_to_pocketbase(data['end_date']) + del data['end_date'] + + print(f"[DEBUG] Creating record in 'workshops' collection with data: {data}") + result = pb.collection('workshops').create(data) + print(f"[DEBUG] Successfully created record: {result}") + return result else: # Opportunities - convert deadline to datetime format if 'deadline' in data: diff --git a/prompts.py b/prompts.py index 32bc682..4693e29 100644 --- a/prompts.py +++ b/prompts.py @@ -8,7 +8,7 @@ OPPORTUNITY_PROMPT = ( "1. 'title': The title of the opportunity\n" "2. 'org': The name of the organizing body/bodies\n" "3. 'type': The category (e.g., Residency, Funding, Open Call, Workshop).\n" - "4. 'summary': A 3-sentence description of what the opportunity involves.\n" + "4. 'summary': A 3-sentence description of what the opportunity involves, including who it is targeted towards (e.g. emerging artists, students, professionals).\n" "5. 'deadline': The deadline of the opportunity. Format: DD-MM-YYYY. Assume year 2026 if missing.\n" "6. 'location': The physical city/country or 'Online'.\n\n" "# CONSTRAINTS\n" @@ -22,7 +22,7 @@ OPPORTUNITY_PROMPT = ( " \"title\": \"Digital Horizons 2026\",\n" " \"org\": \"Digital Horizons\",\n" " \"type\": \"Residency\",\n" - " \"summary\": \"A residency for digital artists to explore VR. Includes a stipend.\",\n" + " \"summary\": \"A residency for emerging digital artists to explore VR. Open to EU-based practitioners with less than 5 years of professional experience. Includes a stipend.\",\n" " \"deadline\": \"15-11-2026\",\n" " \"location\": \"Berlin, Germany\"\n" "}\n" @@ -36,9 +36,10 @@ EVENT_PROMPT = ( "Analyze the provided text and extract information into these JSON keys:\n" "1. 'title': The name/title of the event\n" "2. 'org': The name of the organizing body/bodies\n" - "3. 'date_time': The date and time of the event. Format: DD-MM-YYYY (HH:MM) or 'N/A' if not specified.\n" - "4. 'summary': A 3-sentence description of what the event is about.\n" - "5. 'location': The physical venue/city/country or 'Online'.\n\n" + "3. 'date_time': The start date and time of the event. Format: DD-MM-YYYY (HH:MM). Assume year 2026 if missing.\n" + "4. 'end_date': The end date and time of the event, if it spans multiple days. Format: DD-MM-YYYY (HH:MM). Use 'N/A' if it is a one-day event or no end date is specified.\n" + "5. 'summary': A 3-sentence description of what the event is about, including the intended audience (e.g. general public, industry professionals, students).\n" + "6. 'location': The physical venue/city/country or 'Online'.\n\n" "# CONSTRAINTS\n" "- Return ONLY the JSON object inside markdown backticks (```json ... ```).\n" "- Do NOT include any introductory or conversational text.\n" @@ -49,8 +50,37 @@ EVENT_PROMPT = ( " \"title\": \"Digital Arts Symposium 2026\",\n" " \"org\": \"Digital Arts Society\",\n" " \"date_time\": \"20-06-2026 14:00\",\n" - " \"summary\": \"Join us for a day of talks and workshops exploring digital art. Meet artists and curators. Includes lunch and networking.\",\n" + " \"end_date\": \"22-06-2026 18:00\",\n" + " \"summary\": \"Join us for a three-day symposium of talks and workshops exploring digital art, aimed at artists, curators, and cultural professionals. Meet leading practitioners in the field. Includes lunch and networking.\",\n" " \"location\": \"London, UK\"\n" "}\n" "```" ) + +WORKSHOP_PROMPT = ( + "You are a precise Data Extraction Specialist. Your goal is to convert " + "unstructured workshop text into a strictly valid JSON object.\n\n" + "# TASK\n" + "Analyze the provided text and extract information into these JSON keys:\n" + "1. 'title': The name/title of the workshop\n" + "2. 'org': The name of the organizing body/bodies\n" + "3. 'date_time': The start date and time of the workshop. Format: DD-MM-YYYY (HH:MM). Assume year 2026 if missing.\n" + "4. 'end_date': The end date and time of the workshop, if it spans multiple days. Format: DD-MM-YYYY (HH:MM). Use 'N/A' if it is a one-day workshop or no end date is specified.\n" + "5. 'summary': A 3-sentence description of what the workshop is about, including the target audience (e.g. beginners, experienced practitioners, students).\n" + "6. 'location': The physical venue/city/country or 'Online'.\n\n" + "# CONSTRAINTS\n" + "- Return ONLY the JSON object inside markdown backticks (```json ... ```).\n" + "- Do NOT include any introductory or conversational text.\n" + "- If a field is missing, use 'N/A'.\n\n" + "# EXAMPLE OUTPUT\n" + "```json\n" + "{\n" + " \"title\": \"Introduction to Digital Sculpture\",\n" + " \"org\": \"Creative Tech Lab\",\n" + " \"date_time\": \"10-09-2026 10:00\",\n" + " \"end_date\": \"12-09-2026 16:00\",\n" + " \"summary\": \"A three-day hands-on workshop covering Blender and 3D printing, tailored for beginners and intermediate digital artists. Participants will create their own digital sculpture. All skill levels welcome.\",\n" + " \"location\": \"Dublin, Ireland\"\n" + "}\n" + "```" +) diff --git a/schemas.py b/schemas.py index 568256b..4a45b6f 100644 --- a/schemas.py +++ b/schemas.py @@ -1,4 +1,4 @@ -from typing import Union, Literal +from typing import Union, Literal, Optional from pydantic import BaseModel, Field ## File doesn't do anything, its just an outline for the schemas @@ -11,6 +11,7 @@ class BaseEntry(BaseModel): class Event(BaseEntry): type: Literal["event"] = "event" date_time: str = Field(description="Date and time of the event") + end_date: Optional[str] = Field(default=None, description="End date and time if multi-day event") location: str = Field(description="Location of the event") class Opportunity(BaseEntry): @@ -18,4 +19,10 @@ class Opportunity(BaseEntry): deadline: str = Field(description="What is the deadline in the format of dd-mm-yy") location: str = Field(description="Location of entry") -EntrySchema = Union[Event, Opportunity] \ No newline at end of file +class Workshop(BaseEntry): + type: Literal["workshop"] = "workshop" + date_time: str = Field(description="Start date and time of the workshop") + end_date: Optional[str] = Field(default=None, description="End date and time if multi-day workshop") + location: str = Field(description="Location of the workshop") + +EntrySchema = Union[Event, Opportunity, Workshop] \ No newline at end of file