Spaces:
Runtime error
Runtime error
import gradio as gr | |
from openai import OpenAI | |
import requests | |
import os | |
import asana | |
from asana.rest import ApiException | |
from pprint import pprint | |
import openai | |
import json | |
import datetime | |
import dateparser | |
import multiprocessing.pool | |
# Monkey patch ApiClient.__del__ | |
def noop_del(self): | |
print("Monkey patched ApiClient.__del__ called. Doing nothing.") | |
pass | |
asana.api_client.ApiClient.__del__ = noop_del | |
# Monkey patch multiprocessing.pool.Pool.__del__ | |
def noop_pool_del(self): | |
print("Monkey patched multiprocessing.pool.Pool.__del__ called. Doing nothing.") | |
pass | |
multiprocessing.pool.Pool.__del__ = noop_pool_del | |
settings={ | |
"PREFER_DATES_FROM": "future", | |
"RELATIVE_BASE": datetime.datetime.now() | |
} | |
# Get the OpenAI API key from the environment variable | |
open_ai_key = os.environ.get("OPENAI_API_KEY") | |
# Initialize the OpenAI client with the API key | |
client = OpenAI(api_key=open_ai_key) | |
# Get Asana API key from the environment variable | |
access_token = os.environ.get("ASANA_API_KEY") | |
# Set up the Asana API client with the retrieved access token | |
configuration = asana.Configuration() | |
configuration.access_token = access_token | |
api_client = asana.ApiClient(configuration) | |
ASANA_BASE_URL = "https://app.asana.com/api/1.0" | |
ASANA_HEADERS = { | |
"Authorization": f"Bearer {access_token}", | |
"Content-Type": "application/json" | |
} | |
DEFAULT_PROJECT_GID = "1209104858113361" | |
def create_asana_task(name, due_date=None): | |
"""Create a task in Asana.""" | |
url = f"{ASANA_BASE_URL}/tasks" | |
data = { | |
"data": { | |
"name": name, | |
"projects": [DEFAULT_PROJECT_GID] | |
} | |
} | |
if due_date: | |
data["data"]["due_on"] = due_date # Asana uses 'due_on' in YYYY-MM-DD format | |
resp = requests.post(url, json=data, headers=ASANA_HEADERS) | |
if resp.status_code == 201: | |
return resp.json()["data"] # returns the newly created task object | |
else: | |
return {"error": resp.text} | |
def list_asana_tasks(only_open=True): | |
"""List tasks in the default project, optionally filtering for only open tasks.""" | |
url = f"{ASANA_BASE_URL}/projects/{DEFAULT_PROJECT_GID}/tasks" | |
params = { | |
"opt_fields": "name,completed" # Include the "completed" field to verify task status | |
} | |
if only_open: | |
params["completed_since"] = "now" # Fetch only incomplete or recently updated tasks | |
resp = requests.get(url, headers=ASANA_HEADERS, params=params) | |
if resp.status_code == 200: | |
tasks = resp.json()["data"] | |
if only_open: | |
# Filter out completed tasks if only_open is True | |
tasks = [task for task in tasks if not task.get("completed", False)] | |
return tasks | |
else: | |
return {"error": resp.text} | |
def complete_asana_task(task_gid): | |
"""Mark a task as complete.""" | |
url = f"{ASANA_BASE_URL}/tasks/{task_gid}" | |
data = { | |
"data": { | |
"completed": True | |
} | |
} | |
resp = requests.put(url, json=data, headers=ASANA_HEADERS) | |
if resp.status_code == 200: | |
return resp.json()["data"] | |
else: | |
return {"error": resp.text} | |
def call_llm(user_message, conversation_history=None): | |
today_date = datetime.date.today().strftime("%Y-%m-%d") | |
messages = [{"role": "system", "content": system_prompt}] | |
messages.append({"role": "user", "content": user_message}) | |
response = client.chat.completions.create( | |
model="gpt-4o", | |
messages=messages, | |
temperature=0.2, | |
max_tokens=200 | |
) | |
# Access 'content' using dot notation instead of indexing | |
llm_content = response.choices[0].message.content | |
print(f"Debug: Raw LLM Content: {llm_content}") | |
return llm_content | |
# Global variables | |
last_task_list = [] | |
def execute_turn(user_message, test_feed_iter=None): | |
global last_task_list # Declare global variable at the top of the function | |
llm_output = call_llm(user_message) | |
parsed = parse_llm_response(llm_output) | |
action = parsed.get("action") | |
if action == "CREATE_TASK": | |
task_name = parsed.get("name", "").strip() | |
due_date = parsed.get("due") # Could be a natural language string like "tomorrow" | |
# --- 1) OVERRIDE "NEW TASK" NAME IF POSSIBLE --- | |
if (not task_name or task_name.lower() == "new task"): | |
if test_feed_iter is not None: | |
# If we have more lines in TEST_FEED, use them | |
try: | |
override_name = next(test_feed_iter) # get the next line from the feed | |
task_name = override_name.strip() | |
print(f"Debug: Overriding 'New Task' with '{task_name}' from TEST_FEED") | |
except StopIteration: | |
# If there's nothing left in the feed, fallback to user prompt | |
if not task_name: | |
print("Bot: Task name cannot be empty. Please try again.") | |
return | |
else: | |
# If we don't have a test_feed_iter, just prompt the user | |
print("Bot: What would you like the name of the task to be?") | |
task_name = input(USER_PROMPT).strip() | |
if not task_name: | |
print("Bot: Task name cannot be empty. Please try again.") | |
return | |
# --- 2) OVERRIDE DUE DATE IF POSSIBLE --- | |
if not due_date: | |
if test_feed_iter is not None: | |
# Try to parse the next line as a date | |
try: | |
maybe_date = next(test_feed_iter) | |
parsed_date = dateparser.parse(maybe_date) | |
if parsed_date: | |
due_date = parsed_date.strftime('%Y-%m-%d') | |
print(f"Debug: Overriding due date with '{due_date}' from TEST_FEED") | |
else: | |
# Not recognized as a date; do nothing | |
print(f"Debug: '{maybe_date}' did not parse as a date; skipping due.") | |
except StopIteration: | |
pass | |
# If we still have no due_date after that, fallback to user prompt | |
if not due_date: | |
user_due_date = input(USER_PROMPT).strip() | |
if user_due_date: | |
parsed_date = dateparser.parse( | |
user_due_date, | |
settings={ | |
"PREFER_DATES_FROM": "future", | |
# Optionally: "RELATIVE_BASE": datetime.datetime.now() | |
} | |
) | |
if parsed_date: | |
due_date = parsed_date.strftime('%Y-%m-%d') | |
else: | |
print("Bot: I couldn’t understand the due date. Skipping it.") | |
due_date = None | |
# --- 3) ATTEMPT TO CREATE THE TASK --- | |
print(f"Debug: Attempting to create task with name '{task_name}' and due date '{due_date}'") | |
result = create_asana_task(task_name, due_date) | |
# Provide a confirmation message if successful | |
if "error" in result: | |
print("Bot: Sorry, I had trouble creating the task:", result["error"]) | |
else: | |
message = f"Bot: I've created your task '{result['name']}' (ID: {result['gid']})." | |
if due_date: | |
message += f" It's due on {due_date}." | |
print(message) | |
elif action == "LIST_TASKS": | |
# (Unmodified code for listing tasks) | |
filter_type = parsed.get("filter", "open") # Default to "open" if no filter is provided | |
only_open = filter_type == "open" | |
tasks = list_asana_tasks(only_open=only_open) | |
if "error" in tasks: | |
print("Bot: Sorry, I had trouble listing tasks:", tasks["error"]) | |
elif not tasks: | |
if only_open: | |
print("Bot: You have no open tasks!") | |
else: | |
print("Bot: You have no tasks!") | |
else: | |
task_type = "open" if only_open else "all" | |
print(f"Here are your {task_type} tasks:") | |
last_task_list.clear() # Clear previous tasks | |
for t in tasks: | |
task_info = {'name': t['name'], 'gid': t['gid']} | |
last_task_list.append(task_info) # Store task info | |
print(f"- {t['name']} (ID: {t['gid']})") | |
elif action == "COMPLETE_TASK": | |
# (Unmodified code for completing tasks) | |
task_gid = parsed.get("task_gid") | |
task_name = parsed.get("name") # Capture task name for fuzzy matching | |
if task_gid: | |
result = complete_asana_task(task_gid) | |
if "error" in result: | |
print("Bot: Sorry, I couldn’t complete the task:", result["error"]) | |
else: | |
print(f"Task '{result['name']}' marked as complete.") | |
elif task_name: | |
tasks = list_asana_tasks() | |
if "error" in tasks: | |
print("Bot: Sorry, I had trouble fetching tasks to find a match.") | |
return | |
matches = [t for t in tasks if task_name.lower() in t['name'].lower()] | |
if len(matches) == 1: | |
task_to_close = matches[0] | |
result = complete_asana_task(task_to_close["gid"]) | |
if "error" in result: | |
print(f"Bot: Sorry, I couldn’t complete the task: {result['error']}") | |
else: | |
print(f"Task '{task_to_close['name']}' marked as complete.") | |
elif len(matches) > 1: | |
print("Bot: I found multiple tasks matching that name. " | |
"Please provide the ID of the task you'd like to close:") | |
for task in matches: | |
print(f"- {task['name']} (ID: {task['gid']})") | |
else: | |
print(f"Bot: I couldn’t find any tasks matching '{task_name}'.") | |
else: | |
# Attempt to extract ordinal-based task position. | |
ordinal_map = { | |
'first': 1, | |
'second': 2, | |
'third': 3, | |
'fourth': 4, | |
'fifth': 5, | |
'sixth': 6, | |
'seventh': 7, | |
'eighth': 8, | |
'ninth': 9, | |
'tenth': 10 | |
} | |
words = user_message.lower().split() | |
ordinal_position = None | |
for word in words: | |
if word in ordinal_map: | |
ordinal_position = ordinal_map[word] - 1 # zero-based index | |
break | |
elif word.isdigit(): | |
ordinal_position = int(word) - 1 | |
break | |
if ordinal_position is not None and last_task_list: | |
if 0 <= ordinal_position < len(last_task_list): | |
task_to_close = last_task_list[ordinal_position] | |
result = complete_asana_task(task_to_close["gid"]) | |
if "error" in result: | |
print(f"Bot: Sorry, I couldn’t complete the task: {result['error']}") | |
else: | |
print(f"Task '{task_to_close['name']}' marked as complete.") | |
else: | |
print("Bot: The task number you specified is out of range.") | |
else: | |
print("Bot: Please provide a valid task name, ID, or position to close.") | |
else: | |
# No recognized action, or just normal text | |
print(llm_output) | |
def extract_task_id_from_message(message): | |
""" | |
Extract task ID (task_gid) from the user message. | |
Example input: "Can we close task 1234567890?" | |
Example output: "1234567890" | |
""" | |
import re | |
# Use a regular expression to find a numeric sequence in the message | |
match = re.search(r'\b\d{10,}\b', message) # Look for 10+ digit numbers | |
if match: | |
return match.group(0) # Return the first match | |
return None # If no match found, return None | |
def parse_llm_response(llm_output): | |
try: | |
print(f"Debug: Raw LLM Content: {llm_output}") # Debug raw output | |
# Strip the backticks and "json" tag | |
if llm_output.startswith("```json") and llm_output.endswith("```"): | |
llm_output = llm_output.strip("```").strip("json").strip() | |
print(f"Debug: Cleaned LLM Output: {llm_output}") # Debug cleaned output | |
# Parse the cleaned JSON | |
parsed_response = json.loads(llm_output) | |
print(f"Debug: Parsed Response: {parsed_response}") # Debug parsed JSON | |
return parsed_response | |
except json.JSONDecodeError as e: | |
print(f"Error: Failed to parse LLM response: {e}") | |
return {"action": "NONE"} # Fallback | |
except Exception as e: | |
print(f"Error: Unexpected issue in parse_llm_response: {e}") | |
return {"action": "NONE"} # Fallback | |
def run_manual_chat(): | |
print("Hello! I'm your Asana Copilot!") | |
print("I can help you create new tasks, list your tasks, and mark tasks as completed.") | |
print("Let me know how I can help.") | |
print("Want to end our chat? Just type 'quit' to exit.\n") | |
USER_PROMPT = "[USER]\n>>> " | |
TURN_BREAK = "-------------------\n" | |
while True: | |
user_input = input(USER_PROMPT).strip() | |
if user_input.lower() == "quit": | |
print("\nExiting the chat. Goodbye!") | |
break | |
print(TURN_BREAK + "[COPILOT]") | |
execute_turn(user_input) | |
print(TURN_BREAK) | |
system_prompt = """ | |
You are a friendly AI Copilot that helps users interface with Asana -- namely creating new tasks, listing tasks, and marking tasks as complete. | |
You will interpret the user's request and respond with structured JSON. | |
Today's date is {today_date}. | |
Rules: | |
1. If the user asks to create a task, respond with: | |
{ "action": "CREATE_TASK", "name": "<TASK NAME>", "due": "<YYYY-MM-DD>" } | |
If they gave a date in any format. For words like 'tomorrow', interpret it as {today_date} + 1 day, etc. | |
If no date is given or you cannot parse it, omit the 'due' field. | |
2. If the user asks to list tasks, respond with: | |
{"action": "LIST_TASKS", "filter": "open"} # For "list my open tasks" or similar | |
{"action": "LIST_TASKS", "filter": "all"} # For "list all my tasks" or similar | |
If the user specifies "open tasks" or similar, return only incomplete tasks. If the user specifies "all tasks," return all tasks (completed and incomplete). | |
If the intent is unclear, default to showing only open tasks. | |
3. If the user asks to complete a task, respond with: | |
{ "action": "COMPLETE_TASK", "task_gid": "<ID>" } | |
OR | |
{ "action": "COMPLETE_TASK", "name": "<TASK NAME>" } | |
OR | |
{ "action": "COMPLETE_TASK", "position": <NUMBER> } | |
Use 'position' if the user refers to a task by its position in the list (e.g., "third one"). | |
4. If no action is needed, respond with: | |
{ "action": "NONE" } | |
Examples: | |
- User: "Close task 1209105096577103" | |
Response: { "action": "COMPLETE_TASK", "task_gid": "1209105096577103" } | |
- User: "Can you close rub jason's feet?" | |
Response: { "action": "COMPLETE_TASK", "name": "rub jason's feet" } | |
- User: "List all my tasks" | |
Response: { "action": "LIST_TASKS" } | |
- User: "Create a task called 'Finish report' due tomorrow" | |
Response: { "action": "CREATE_TASK", "name": "Finish report", "due": "2025-01-08" } | |
- User: "Close the third one" | |
Response: { "action": "COMPLETE_TASK", "position": 3 } | |
- User: "Complete task number 5" | |
Response: { "action": "COMPLETE_TASK", "position": 5 } | |
- {"action": "LIST_TASKS", "filter": "open"} # For "list my open tasks" | |
- {"action": "LIST_TASKS", "filter": "all"} # For "list all my tasks" | |
- {"action": "CREATE_TASK", "name": "Task Name", "due": "2025-01-15"} | |
- {"action": "COMPLETE_TASK", "task_gid": "1209105096577103"} | |
Again, always respond in JSON format. Example: | |
{ | |
"action": "CREATE_TASK", | |
"name": "Submit Assignment", | |
"due": "2023-12-31" | |
} | |
If no action is required, respond with: | |
{ | |
"action": "NONE" | |
} | |
""" | |
def process_input(user_input): | |
""" | |
This function takes the user's input, processes it using the Asana Copilot logic, | |
and returns the formatted output for display. | |
""" | |
global last_task_list # Make sure to handle global variables | |
# Simulate the conversation turn | |
response = "" # Initialize an empty string to accumulate the response | |
# Process the user input using the execute_turn function | |
# Capture the output from execute_turn (e.g., print statements) and append it to 'response' | |
import io | |
from contextlib import redirect_stdout | |
with io.StringIO() as buf, redirect_stdout(buf): | |
execute_turn(user_input) | |
output = buf.getvalue() | |
response += output # Append the captured output to the response | |
return response |