diff --git a/netbot/cog_tickets.py b/netbot/cog_tickets.py index bc23583..2f58a17 100644 --- a/netbot/cog_tickets.py +++ b/netbot/cog_tickets.py @@ -4,6 +4,7 @@ import logging import datetime as dt +import re import discord from discord import ScheduledEvent, OptionChoice @@ -12,6 +13,10 @@ from discord.enums import InputTextStyle from discord.ui.item import Item, V from discord.utils import basic_autocomplete +from datetime import datetime +from netbot.llm_redactor import RedmineProcessor +import logging +import json import dateparser @@ -254,38 +259,48 @@ async def callback(self, interaction: discord.Interaction): class EditDescriptionModal(discord.ui.Modal): - """modal dialog to edit the ticket subject and description""" - def __init__(self, redmine: Client, ticket: Ticket, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - # Note: redmine must be available in callback, as the bot is not - # available thru the Interaction. - self.redmine = redmine - self.ticket_id = ticket.id - self.add_item(discord.ui.InputText(label="Description", - value=ticket.description, - style=InputTextStyle.paragraph)) + def __init__(self, processor, ticket_id: int, initial_text: str, title: str = "Edit Description"): + super().__init__(title=title, timeout=300) + self.processor = processor + self.ticket_id = ticket_id + + self.input = discord.ui.InputText( + label="Description (raw, will be redacted on submit)", + style=discord.InputTextStyle.long, + value=initial_text[:4000] if initial_text else "", + required=False + ) + self.add_item(self.input) async def callback(self, interaction: discord.Interaction): - description = self.children[0].value - log.debug(f"callback: {description}") + await interaction.response.defer() - user = self.redmine.user_mgr.find_discord_user(interaction.user.name) + new_text = (self.input.value or "").strip() + # print(f"new_text: {new_text}") - fields = { - "description": description, - } - ticket = self.redmine.ticket_mgr.update(self.ticket_id, fields, user.login) - - embed = discord.Embed(title=f"Updated ticket {ticket.id} description") - embed.add_field(name="Description", value=ticket.description) + result = self.processor.redact_pii(new_text, {}) + # print(f"result: {result}") + cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", (result or "").strip(), flags=re.DOTALL) + # print(f"cleaned: {cleaned}") + final_output = cleaned + # print(f"final_output: {new_text}") + try: + payload = json.loads(cleaned) + final_output = payload.get("redacted_text", cleaned) + except json.JSONDecodeError: + pass - await interaction.response.send_message(embeds=[embed]) + self.processor.update_ticket(self.ticket_id, { + "description": new_text, + "red_description": final_output + }) + embed = discord.Embed(title=f"Updated ticket #{self.ticket_id}") + embed.add_field(name="Redacted description", value=final_output or "*empty*", inline=False) + await interaction.followup.send(embed=embed, ephemeral=False) -# distinct from above. takes app-context def default_term(ctx: discord.ApplicationContext) -> str: - # examine the thread ch_name = ctx.channel.name ticket_id = NetBot.parse_thread_title(ch_name) if ticket_id: @@ -301,6 +316,11 @@ class TicketsCog(commands.Cog): def __init__(self, bot:NetBot): self.bot:NetBot = bot self.redmine: Client = bot.redmine + self.bot = bot + self.redmine_processor = RedmineProcessor( + api_key="8e9f6efeac50a11afc26d0c7bead709f0dfce25b", + redmine_url="http://localhost:80" + ) # see https://github.com/Pycord-Development/pycord/blob/master/examples/app_commands/slash_cog_groups.py @@ -349,51 +369,80 @@ def resolve_query_term(self, term) -> list[Ticket]: @option(name="term", description="Query can include ticket ID, owner, team or any term used for a text match.", default="") - async def query(self, ctx: discord.ApplicationContext, term:str = ""): - """List tickets for you, or filtered by parameter""" - # different options: none, me (default), [group-name], intake, tracker name - # buid index for trackers, groups - # add groups to users. - - # lookup the user + async def query(self, ctx: discord.ApplicationContext, term: str = ""): + """List tickets for you, or filtered by parameter. If a single ticket ID is requested, + show the redacted description (if present) instead of raw.""" user = self.redmine.user_mgr.find(ctx.user.name) if not user: - log.info(f"Unknown user name: {ctx.user.name}") - # TODO make this a standard error. - await ctx.respond(f"Discord member **{ctx.user.name}** is not provisioned in redmine. Use `/scn add [redmine-user]` to provision.") + await ctx.respond( + f"Discord member **{ctx.user.name}** is not provisioned in redmine. " + "Use `/scn add [redmine-user]` to provision." + ) return - log.debug(f"found user mapping for {ctx.user.name}: {user}") - if term == "": - # empty param, try to derive from channel name - term = default_term(ctx) - if term is None: - # still none, default to... - term = "me" + term = default_term(ctx) or "me" if term == "me": results = self.redmine.ticket_mgr.my_tickets(user.login) else: results = self.resolve_query_term(term) - if results and len(results) > 0: - await self.bot.formatter.print_tickets(f"{term}", results, ctx) - else: + if not results or len(results) == 0: await ctx.respond(f"Zero results for: `{term}`") + return + + is_single_numeric = term.isdigit() and len(results) == 1 + if is_single_numeric: + ticket = results[0] + + redacted = await self.get_redacted_description(ticket.id) + if not redacted: + try: + processed = self.redmine_processor.process_ticket(ticket.id) + if processed: + redacted = processed.get("redacted_description") + except Exception as e: + log.warning(f"On-the-fly redaction failed for #{ticket.id}: {e}") + + await self.bot.formatter.print_ticket(ticket, ctx, description_override=redacted) + return + for t in results: + issue = self.redmine_processor.fetch_ticket_by_id(t.id) + desc = issue.get("red_description") or issue.get("description") or "" + await self.bot.formatter.print_ticket(t, ctx, description_override=desc) + + + + + async def get_redacted_description(self, ticket_id: int): + issue = self.redmine_processor.fetch_ticket_by_id(ticket_id) + if not issue: + return None + return issue.get("red_description") @ticket.command(description="Get ticket details") @option("ticket_id", description="ticket ID", autocomplete=basic_autocomplete(default_ticket)) - async def details(self, ctx: discord.ApplicationContext, ticket_id:int): - """Update status on a ticket, using: unassign, resolve, progress""" - #log.debug(f"found user mapping for {ctx.user.name}: {user}") - ticket = self.redmine.ticket_mgr.get(ticket_id, include="children,watchers") - if ticket: - await self.bot.formatter.print_ticket(ticket, ctx) - else: - await ctx.respond(f"Ticket {ticket_id} not found.") # print error + async def details(self, ctx: discord.ApplicationContext, ticket_id: int): + await ctx.defer() + try: + ticket = self.redmine.ticket_mgr.get(ticket_id, include="children,watchers") + if not ticket: + return await ctx.respond(f"Ticket #{ticket_id} not found.") + + redacted = await self.get_redacted_description(ticket_id) + await self.bot.formatter.print_ticket( + ticket, + ctx, + description_override=redacted or ticket.description + ) + except Exception as e: + log.error(f"Error in /ticket details for #{ticket_id}: {e}") + await ctx.respond("An error occurred while processing your ticket.") + + @ticket.command(description="Collaborate on a ticket") @@ -509,17 +558,6 @@ async def assign(self, ctx: discord.ApplicationContext, ticket_id:int, member:di else: await ctx.respond(f"Ticket {ticket_id} not found.") # print error - - # command disabled - #@ticket.command(name="edit", description="Edit a ticket") - #@option("ticket_id", description="ticket ID") - # async def edit(self, ctx:discord.ApplicationContext, ticket_id: int): - # """Edit the fields of a ticket""" - # # check team? admin?, provide reasonable error msg. - # ticket = self.redmine.ticket_mgr.get(ticket_id) - # await ctx.respond(f"EDIT #{ticket.id}", view=EditView(self.bot)) - - async def create_thread(self, ticket:Ticket, ctx:discord.ApplicationContext): log.info(f"creating a new thread for ticket #{ticket.id} in channel: {ctx.channel.name}") thread_name = f"Ticket #{ticket.id}: {ticket.subject}" @@ -533,7 +571,6 @@ async def create_thread(self, ticket:Ticket, ctx:discord.ApplicationContext): await thread.send(self.bot.formatter.format_ticket_details(ticket)) return thread - @ticket.command(name="new", description="Create a new ticket") @option("title", description="Title of the new SCN ticket") async def create_new_ticket(self, ctx: discord.ApplicationContext, title:str): @@ -550,7 +587,6 @@ async def create_new_ticket(self, ctx: discord.ApplicationContext, title:str): ticket_id = NetBot.parse_thread_title(channel_name) log.debug(f">>> {channel_name} --> ticket: {ticket_id}") if ticket_id: - # check if it's an epic epic = self.redmine.ticket_mgr.get(ticket_id) if epic and epic.priority.name == "EPIC": log.debug(f">>> {ticket_id} is an EPIC!") @@ -558,7 +594,6 @@ async def create_new_ticket(self, ctx: discord.ApplicationContext, title:str): await self.thread(ctx, ticket.id) return - # not in ticket thread, try tracker tracker = self.bot.tracker_for_channel(channel_name) team = self.bot.team_for_tracker(tracker) role = self.bot.get_role_by_name(team.name) @@ -576,8 +611,9 @@ async def create_new_ticket(self, ctx: discord.ApplicationContext, title:str): log.warning(f"unable to load role by team name: {team.name}") await ctx.respond(alert_msg, embed=self.bot.formatter.ticket_embed(ctx, ticket)) else: - log.error(f"no tracker for {channel_name}") - await ctx.respond(f"ERROR: No tracker for {channel_name}.") + log.debug(f"no parent ot tracker for {channel_name}") + ticket = self.redmine.ticket_mgr.create(user, message) + await self.thread(ctx, ticket.id) @ticket.command(name="notify", description="Notify collaborators on a ticket") @@ -808,41 +844,40 @@ async def due(self, ctx: discord.ApplicationContext, date:str): @ticket.command(name="description", description="Edit the description of a ticket") async def edit_description(self, ctx: discord.ApplicationContext): - # pop the the edit description embed + """Open a modal to edit the ticket's description using a fresh fetch from Redmine.""" ticket_id = NetBot.parse_thread_title(ctx.channel.name) - ticket = self.redmine.ticket_mgr.get(ticket_id) - if ticket: - modal = EditDescriptionModal(self.redmine, ticket, title=f"Editing ticket #{ticket.id}") - await ctx.send_modal(modal) - else: - await ctx.respond(f"Cannot find ticket for {ctx.channel}") + issue = self.redmine_processor.fetch_ticket_by_id(ticket_id) + if not issue: + await ctx.followup.send(f"Couldn't find ticket for thread: {ctx.channel.mention}", ephemeral=True) + return + initial_text = issue.get("description") or "" + modal = EditDescriptionModal( + processor=self.redmine_processor, + ticket_id=ticket_id, + initial_text=initial_text, + title=f"Editing ticket #{ticket_id} description" + ) + await ctx.send_modal(modal) @ticket.command(name="parent", description="Set a parent ticket for ") @option("parent_ticket", description="The ID of the parent ticket") async def parent(self, ctx: discord.ApplicationContext, parent_ticket:int): - # /ticket parent 234 <- Get *this* ticket and set the parent to 234. - - # get ticket Id from thread ticket_id = NetBot.parse_thread_title(ctx.channel.name) if not ticket_id: - # error - no ticket ID await ctx.respond("Command only valid in ticket thread. No ticket info found in this thread.") return - # validate user user = self.redmine.user_mgr.find_discord_user(ctx.user.name) if not user: await ctx.respond(f"ERROR: Discord user without redmine config: {ctx.user.name}. Create with `/scn add`") return - # check that parent_ticket is valid parent = self.redmine.ticket_mgr.get(parent_ticket) if not parent: await ctx.respond(f"ERROR: Unknow ticket #: {parent_ticket}") return - # update the ticket params = { "parent_issue_id": parent_ticket, } diff --git a/netbot/formatting.py b/netbot/formatting.py index 4f8dc17..cd3c650 100644 --- a/netbot/formatting.py +++ b/netbot/formatting.py @@ -76,8 +76,8 @@ async def print_tickets(self, title:str, tickets:list[Ticket], ctx:discord.Appli await ctx.respond(msg) - async def print_ticket(self, ticket, ctx:discord.ApplicationContext): - await ctx.respond(embed=self.ticket_embed(ctx, ticket)) + async def print_ticket(self, ticket, ctx: discord.ApplicationContext, *, description_override: str | None = None): + await ctx.respond(embed=self.ticket_embed(ctx, ticket, description_override=description_override)) def format_registered_users(self, users: list[User]) -> str: @@ -376,21 +376,27 @@ def format_collaborators(self, ctx: discord.ApplicationContext, ticket:Ticket) - return self.format_discord_member(ctx, ticket.watchers[0].id) - def ticket_embed(self, ctx: discord.ApplicationContext, ticket:Ticket) -> discord.Embed: + def ticket_embed(self, ctx: discord.ApplicationContext, ticket:Ticket, description_override: str | None = None) -> discord.Embed: """Build an embed panel with full ticket details""" subject = f"{get_emoji(ticket.priority.name)} {ticket.subject[:EMBED_TITLE_LEN-8]} (#{ticket.id})" + + desc = ( + description_override or getattr(ticket, "red_description", None) + or getattr(ticket, "description", None) + or "" + ) + if len(desc) > EMBED_DESC_LEN: + desc = desc[:EMBED_DESC_LEN] embed = discord.Embed( title=subject, - description=ticket.description[:EMBED_DESC_LEN], + description=desc, colour=self.ticket_color(ticket) ) - - # noting, assuming all these values are less than - # status = self.format_icon(ticket.status) - #priority = self.format_icon(ticket.priority) embed.add_field(name="Status", value=self.format_icon(ticket.status)) embed.add_field(name="Priority", value=self.format_icon(ticket.priority)) embed.add_field(name="Tracker", value=self.format_icon(ticket.tracker)) + + if ticket.category: embed.add_field(name="Category", value=ticket.category) diff --git a/netbot/llm_redactor.py b/netbot/llm_redactor.py new file mode 100644 index 0000000..f8ed730 --- /dev/null +++ b/netbot/llm_redactor.py @@ -0,0 +1,327 @@ +import google.generativeai as genai +import dspy +import requests +import re +from datetime import datetime +import json +import re + +import logging +from requests.exceptions import RequestException, ConnectionError, Timeout, HTTPError + +# check signal for details +# genai.configure(api_key="TBDTBDTBD") + +class PIIRedactionModule(dspy.Module): + def __init__(self): + super().__init__() + + def forward(self, text, pii_entities): + pii_list = "\n".join([f"- {entity}: {label}" for entity, label in pii_entities.items()]) + + prompt = f""" + You are a **privacy compliance officer** responsible for redacting **personally identifiable information (PII)** from **Redmine support tickets** before they are shared on public platforms like Discord. + + Your primary goal is to **remove PII while preserving ticket clarity and structure** so that it remains **useful for troubleshooting and public discussion.** + ALWAYS REMEMBER: never summarize a ticket. redact per instructions but never summarize or shorten excessively + + --- + + ## **Strict Redaction Rules:** + Apply these redaction rules carefully: + + ### **1. Names** + Replace all personal names with placeholders **original firstname [LastNameN]**, where **N** is a unique number per name within the ticket. + + - Example: + - **Original:** *Chris Caputo reported an issue with network latency.* + - **Redacted:** *Chris [LastName1] reported an issue with network latency.* + + - Example (Multiple people): + - **Original:** *Esther Jang contacted Alice about the issue.* + - **Redacted:** *Esther [LastName1] contacted Alice about the issue.* + + ### **2. Emails** + Replace all email addresses with **[EmailN]**, ensuring correct numbering within the ticket. + + - Example: + - **Original:** *Please contact john.doe@example.com for assistance.* + - **Redacted:** *Please contact [Email1] for assistance.* + + - **Original:** *infrared@cs.washington.edu requested access to the document.* + - **Redacted:** *[Email2] requested access to the document.* + + ### **3. Phone Numbers** + Replace all phone numbers with **[PhoneN]**. + + - Example: + - **Original:** *Call us at +1 (555) 123-4567 for support.* + - **Redacted:** *Call us at [Phone1] for support.* + + ### **4. Physical Addresses** + Replace all physical addresses (including partial ones) with **[AddressN]**. + + - Example: + - **Original:** *The router is located at 1234 Elm St, Seattle, WA 98101.* + - **Redacted:** *The router is located at [Address1].* + + ### **5. IP & MAC Addresses** + Replace all IP addresses (both IPv4 and IPv6) and MAC addresses with **[IPN]** and **[MACN]**, respectively. + + - Example: + - **Original:** *Router IP: 192.168.1.100, IPv6: 2001:db8::ff00:42:8329.* + - **Redacted:** *Router IP: [IP1], IPv6: [IP2].* + + - **Original:** *Device MAC: AA:BB:CC:DD:EE:FF.* + - **Redacted:** *Device MAC: [MAC1].* + + ### **6. Public Keys & Login Credentials** + Replace cryptographic keys (such as SSH, PGP, and API keys) with **[PublicKeyN]**. + + - Example: + - **Original:** *PGP Key: 1280 AFA4 DD14 589B.* + - **Redacted:** *PGP Key: [PublicKey1].* + + Replace **any login credentials** (usernames and passwords) with **[UsernameN]** and **[PasswordN]**. + + - Example: + - **Original:** *Username: admin, Password: pass123!* + - **Redacted:** *Username: [Username1], Password: [Password1].* + + ### **7. Links & URLs** + - Replace **personal document-sharing links** with **[Document Link]**. + - Keep **institutional URLs** **(such as Seattle Community Network pages and PeeringDB links)** intact. + + - Example: + - **Original:** *Google Doc: https://docs.google.com/document/d/xyz123.* + - **Redacted:** *Google Doc: [Document Link].* + + - **Original:** *Seattle IX route servers: https://www.seattleix.net/route-servers.* + - **Redacted:** *Seattle IX route servers: https://www.seattleix.net/route-servers.* (Kept because it is public knowledge) + + ### **8. Message Context Preservation** + - **DO NOT** modify technical details, ticket metadata, or non-PII network-related content. + - **DO NOT** over-redact information that is **public knowledge or operationally relevant**. + + --- + + ## **Handling Multi-User Interactions** + When a ticket has **multiple users**, assign **unique placeholders per person** to maintain readability: + + - **Original:** + ``` + Chris Caputo: Please fix this issue. + Esther Jang: I have escalated this to IT. + Chris Caputo: Thank you! + ``` + - **Redacted:** + ``` + [FirstName1] [LastName1]: Please fix this issue. + [FirstName2] [LastName2]: I have escalated this to IT. + [FirstName1] [LastName1]: Thank you! + ``` + + --- + + ## **Example Redacted Ticket:** + **Original Ticket (ID: 1591)** + **Subject:** *MacBook Air 2023 Laptop donation* + **Description:** + ``` + Hi Shannon, + + I saw your message on Meetup! Are you still down to donate your laptop? + + Thanks, + -Esther + 1280 AFA4 DD14 589B + Seattle Community Network + Join SCN on Discord + Note: I have flexible working hours, so my emails may come at unusual times. Please do not feel obligated to respond outside of your usual hours. Thank you!! + + shannonhoffman007@gmail.com + ``` + + **Redacted Version:** + **Subject:** *[FirstName1] [LastName1] donating a MacBook Air 2023 Laptop* + **Description:** + ``` + Hi [FirstName2], + + I saw your message on [MeetupN]! Are you still willing to donate your laptop? + + Thanks, + -[FirstName1] + [PublicKey1] <[Keybase Link]> + Seattle Community Network + Join SCN on Discord <[Discord Link]> + Note: I have flexible working hours, so my emails may come at unusual times. Please do not feel obligated to respond outside of your usual hours. Thank you!! + + [Email1] + + ALWAYS REMEMBER: never summarize a ticket. redact per instructions but never summarize or shorten excessively + ``` + + Detected PII: + {pii_list} + + Original text: + {text} + + Return JSON output: + {{ + "redacted_text": "Formatted, cleaned, and redacted output." + }} + """ + + model = genai.GenerativeModel("gemini-2.0-flash-lite") + response = model.generate_content(prompt) + + if response and response.text: + return dspy.Prediction(redacted_text=response.text) + else: + return dspy.Prediction(redacted_text=text) + + +class RedmineProcessor: + def __init__(self, api_key: str, redmine_url: str): + self.api_key = api_key + self.redmine_url = redmine_url.rstrip("/") + # self.base = redmine_url.rstrip("/") + self.session = requests.Session() + self.session.headers.update({ + "User-Agent": "netbot/0.0.1", + "Content-Type": "application/json", + "X-Redmine-API-Key": api_key, + }) + + def fetch_ticket_by_id(self, ticket_id: int): + """Fetch a Redmine ticket by ID and flatten the JSON result.""" + url = f"{self.redmine_url}/issues/{ticket_id}.json?include=children,watchers" + print(f"[DEBUG] Fetching ticket {ticket_id} from {url}") + resp = self.session.get(url, timeout=10) + print(f"[DEBUG] Response status: {resp.status_code}") + if not resp.ok: + logging.warning(f"Failed to fetch ticket #{ticket_id}: {resp.status_code}") + return {} + + data = resp.json() + issue = data.get("issue", {}) + print(f"[DEBUG] Keys in issue: {list(issue.keys())}") + + if "red_description" in issue: + print(f"[DEBUG] red_description directly in issue: {issue['red_description'][:80]!r}") + else: + print("[DEBUG] red_description not found directly; checking custom_fields") + + flat_issue = {**issue} + + if "custom_fields" in issue: + for cf in issue["custom_fields"]: + if cf.get("name") == "red_description": + flat_issue["red_description"] = cf.get("value") + print(f"[DEBUG] Found red_description in custom_fields: {cf.get('value')[:80]!r}") + return flat_issue + + def update_ticket(self, ticket_id: int, fields: dict) -> dict: + """Update a ticket via Redmine REST API.""" + payload = {"issue": fields} + r = self.session.put( + f"{self.redmine_url}/issues/{ticket_id}.json", + data=json.dumps(payload), + timeout=10, + ) + if r.status_code not in (200, 201, 204): + r.raise_for_status() + try: + return (r.json() or {}).get("issue", {}) + except ValueError: + return {} + + + + def clean_text(self, text): + return re.sub(r"\s+", " ", text).strip() + + def redact_pii(self, text, pii_entities): + redactor = PIIRedactionModule() + result = redactor.forward(text, pii_entities) + return self.clean_text(result.redacted_text) + + + def process_ticket(self, ticket_id): + try: + ticket = self.fetch_ticket_by_id(ticket_id) + if not ticket: + log.error(f"Ticket #{ticket_id} not found.") + self.logger.warning(f"Could not retrieve ticket {ticket_id}") + return None + + title = self.clean_text(ticket.get("subject", "")) + description = self.clean_text(ticket.get("description", "")) + print(f"[DEBUG] Ticket fetched: id={ticket.get('id')} subject={ticket.get('subject')}") + print(f"[DEBUG] red_description={ticket.get('red_description')}") + if not ticket.get("red_description"): + redacted_title = self.redact_pii(title, {}) + redacted_description = self.redact_pii(description, {}) + + cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", redacted_description.strip(), flags=re.DOTALL) + try: + payload = json.loads(cleaned) + redacted_description = payload.get("redacted_text", cleaned) + except json.JSONDecodeError: + redacted_description = cleaned + + self.update_ticket(ticket_id, {"red_description": redacted_description}) + self.logger.info(f"Updating red_description for ticket #{ticket_id}: {redacted_description}") + + return { + "id": ticket["id"], + "redacted_subject": redacted_title, + "redacted_description": redacted_description, + "original_subject": title, + "original_description": description + } + else: + return { + "id": ticket["id"], + "redacted_subject": ticket.get("redacted_subject", ""), + "redacted_description": ticket["red_description"], + "original_subject": title, + "original_description": description + } + + except Exception as e: + self.logger.error(f"Unexpected error processing ticket {ticket_id}: {e}") + return None + +if __name__ == "__main__": + try: + API_KEY = "8e9f6efeac50a11afc26d0c7bead709f0dfce25b" + REDMINE_URL = "http://localhost:80" # Updated IP + + processor = RedmineProcessor(API_KEY, REDMINE_URL) + TICKET_ID = 11735 + final_ticket = processor.process_ticket(TICKET_ID) + + if final_ticket: + print("\nFinal Processed Ticket:\n") + print(f"ID: {final_ticket['id']}\n") + + print("\nRedacted Description:") + json_str = final_ticket["redacted_description"] + json_match = re.search(r'```json\s*({.*})\s*```', json_str, re.DOTALL) + + if json_match: + try: + json_data = json.loads(json_match.group(1)) + print(json_data["redacted_text"]) + except json.JSONDecodeError as e: + print(f"JSON Decode Error: {e}") + else: + print("No JSON found in the string") + else: + print(f"Failed to process ticket {TICKET_ID}") + + except Exception as e: + print(f"Critical error in main execution: {e}") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f83b913 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,26 @@ +aiohappyeyeballs==2.4.6 +aiohttp==3.11.12 +aiosignal==1.3.2 +attrs==25.1.0 +audioop-lts==0.2.1 +certifi==2025.1.31 +charset-normalizer==3.4.1 +dateparser==1.2.1 +frozenlist==1.5.0 +humanize==4.12.0 +idna==3.10 +imapclient==3.0.1 +multidict==6.1.0 +propcache==0.2.1 +py-cord==2.6.1 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +pytz==2025.1 +regex==2024.11.6 +requests==2.32.3 +six==1.17.0 +tzlocal==5.3 +urllib3==2.3.0 +yarl==1.18.3 +google-generativeai +