dhorvath commited on
Commit
ed0ad4e
·
verified ·
1 Parent(s): 7ecbc6a

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +403 -52
app.py CHANGED
@@ -1,64 +1,415 @@
1
  import gradio as gr
2
- from huggingface_hub import InferenceClient
 
 
 
 
 
 
 
 
 
 
3
 
4
- """
5
- For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
6
- """
7
- client = InferenceClient("HuggingFaceH4/zephyr-7b-beta")
 
 
8
 
 
 
9
 
10
- def respond(
11
- message,
12
- history: list[tuple[str, str]],
13
- system_message,
14
- max_tokens,
15
- temperature,
16
- top_p,
17
- ):
18
- messages = [{"role": "system", "content": system_message}]
19
 
20
- for val in history:
21
- if val[0]:
22
- messages.append({"role": "user", "content": val[0]})
23
- if val[1]:
24
- messages.append({"role": "assistant", "content": val[1]})
25
 
26
- messages.append({"role": "user", "content": message})
 
 
 
 
27
 
28
- response = ""
29
 
30
- for message in client.chat_completion(
31
- messages,
32
- max_tokens=max_tokens,
33
- stream=True,
34
- temperature=temperature,
35
- top_p=top_p,
36
- ):
37
- token = message.choices[0].delta.content
 
 
 
 
 
 
 
 
38
 
39
- response += token
40
- yield response
 
 
 
 
 
 
41
 
 
 
 
 
 
 
 
 
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  """
44
- For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
45
- """
46
- demo = gr.ChatInterface(
47
- respond,
48
- additional_inputs=[
49
- gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
50
- gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
51
- gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
52
- gr.Slider(
53
- minimum=0.1,
54
- maximum=1.0,
55
- value=0.95,
56
- step=0.05,
57
- label="Top-p (nucleus sampling)",
58
- ),
59
- ],
60
- )
61
-
62
-
63
- if __name__ == "__main__":
64
- demo.launch()
 
 
 
1
  import gradio as gr
2
+ from openai import OpenAI
3
+ from google.colab import userdata
4
+ import requests
5
+ import os
6
+ import asana
7
+ from asana.rest import ApiException
8
+ from pprint import pprint
9
+ import openai
10
+ import json
11
+ import datetime
12
+ import dateparser
13
 
14
+ settings={
15
+ "PREFER_DATES_FROM": "future",
16
+ "RELATIVE_BASE": datetime.datetime.now()
17
+ }
18
+ # Get the OpenAI API key from the environment variable
19
+ open_ai_key = os.environ.get("OPENAI_API_KEY")
20
 
21
+ # Initialize the OpenAI client with the API key
22
+ client = OpenAI(api_key=open_ai_key)
23
 
24
+ # Get Asana API key from the environment variable
25
+ access_token = os.environ.get("ASANA_API_KEY")
 
 
 
 
 
 
 
26
 
27
+ # Set up the Asana API client with the retrieved access token
28
+ configuration = asana.Configuration()
29
+ configuration.access_token = access_token
30
+ api_client = asana.ApiClient(configuration)
 
31
 
32
+ ASANA_BASE_URL = "https://app.asana.com/api/1.0"
33
+ ASANA_HEADERS = {
34
+ "Authorization": f"Bearer {access_token}",
35
+ "Content-Type": "application/json"
36
+ }
37
 
38
+ DEFAULT_PROJECT_GID = "1209104858113361"
39
 
40
+ def create_asana_task(name, due_date=None):
41
+ """Create a task in Asana."""
42
+ url = f"{ASANA_BASE_URL}/tasks"
43
+ data = {
44
+ "data": {
45
+ "name": name,
46
+ "projects": [DEFAULT_PROJECT_GID]
47
+ }
48
+ }
49
+ if due_date:
50
+ data["data"]["due_on"] = due_date # Asana uses 'due_on' in YYYY-MM-DD format
51
+ resp = requests.post(url, json=data, headers=ASANA_HEADERS)
52
+ if resp.status_code == 201:
53
+ return resp.json()["data"] # returns the newly created task object
54
+ else:
55
+ return {"error": resp.text}
56
 
57
+ def list_asana_tasks(only_open=True):
58
+ """List tasks in the default project, optionally filtering for only open tasks."""
59
+ url = f"{ASANA_BASE_URL}/projects/{DEFAULT_PROJECT_GID}/tasks"
60
+ params = {
61
+ "opt_fields": "name,completed" # Include the "completed" field to verify task status
62
+ }
63
+ if only_open:
64
+ params["completed_since"] = "now" # Fetch only incomplete or recently updated tasks
65
 
66
+ resp = requests.get(url, headers=ASANA_HEADERS, params=params)
67
+ if resp.status_code == 200:
68
+ tasks = resp.json()["data"]
69
+ if only_open:
70
+ # Filter out completed tasks if only_open is True
71
+ tasks = [task for task in tasks if not task.get("completed", False)]
72
+ return tasks
73
+ else:
74
+ return {"error": resp.text}
75
 
76
+ def complete_asana_task(task_gid):
77
+ """Mark a task as complete."""
78
+ url = f"{ASANA_BASE_URL}/tasks/{task_gid}"
79
+ data = {
80
+ "data": {
81
+ "completed": True
82
+ }
83
+ }
84
+ resp = requests.put(url, json=data, headers=ASANA_HEADERS)
85
+ if resp.status_code == 200:
86
+ return resp.json()["data"]
87
+ else:
88
+ return {"error": resp.text}
89
+
90
+ def call_llm(user_message, conversation_history=None):
91
+ today_date = datetime.date.today().strftime("%Y-%m-%d")
92
+ messages = [{"role": "system", "content": system_prompt}]
93
+ messages.append({"role": "user", "content": user_message})
94
+
95
+ response = client.chat.completions.create(
96
+ model="gpt-4o",
97
+ messages=messages,
98
+ temperature=0.2,
99
+ max_tokens=200
100
+ )
101
+ # Access 'content' using dot notation instead of indexing
102
+ llm_content = response.choices[0].message.content
103
+ print(f"Debug: Raw LLM Content: {llm_content}")
104
+ return llm_content
105
+
106
+ # Global variables
107
+ last_task_list = []
108
+
109
+ def execute_turn(user_message, test_feed_iter=None):
110
+ global last_task_list # Declare global variable at the top of the function
111
+ llm_output = call_llm(user_message)
112
+ parsed = parse_llm_response(llm_output)
113
+ action = parsed.get("action")
114
+
115
+ if action == "CREATE_TASK":
116
+ task_name = parsed.get("name", "").strip()
117
+ due_date = parsed.get("due") # Could be a natural language string like "tomorrow"
118
+
119
+ # --- 1) OVERRIDE "NEW TASK" NAME IF POSSIBLE ---
120
+ if (not task_name or task_name.lower() == "new task"):
121
+ if test_feed_iter is not None:
122
+ # If we have more lines in TEST_FEED, use them
123
+ try:
124
+ override_name = next(test_feed_iter) # get the next line from the feed
125
+ task_name = override_name.strip()
126
+ print(f"Debug: Overriding 'New Task' with '{task_name}' from TEST_FEED")
127
+ except StopIteration:
128
+ # If there's nothing left in the feed, fallback to user prompt
129
+ if not task_name:
130
+ print("Bot: Task name cannot be empty. Please try again.")
131
+ return
132
+ else:
133
+ # If we don't have a test_feed_iter, just prompt the user
134
+ print("Bot: What would you like the name of the task to be?")
135
+ task_name = input(USER_PROMPT).strip()
136
+ if not task_name:
137
+ print("Bot: Task name cannot be empty. Please try again.")
138
+ return
139
+
140
+ # --- 2) OVERRIDE DUE DATE IF POSSIBLE ---
141
+ if not due_date:
142
+ if test_feed_iter is not None:
143
+ # Try to parse the next line as a date
144
+ try:
145
+ maybe_date = next(test_feed_iter)
146
+ parsed_date = dateparser.parse(maybe_date)
147
+ if parsed_date:
148
+ due_date = parsed_date.strftime('%Y-%m-%d')
149
+ print(f"Debug: Overriding due date with '{due_date}' from TEST_FEED")
150
+ else:
151
+ # Not recognized as a date; do nothing
152
+ print(f"Debug: '{maybe_date}' did not parse as a date; skipping due.")
153
+ except StopIteration:
154
+ pass
155
+
156
+ # If we still have no due_date after that, fallback to user prompt
157
+ if not due_date:
158
+ user_due_date = input(USER_PROMPT).strip()
159
+ if user_due_date:
160
+ parsed_date = dateparser.parse(
161
+ user_due_date,
162
+ settings={
163
+ "PREFER_DATES_FROM": "future",
164
+ # Optionally: "RELATIVE_BASE": datetime.datetime.now()
165
+ }
166
+ )
167
+ if parsed_date:
168
+ due_date = parsed_date.strftime('%Y-%m-%d')
169
+ else:
170
+ print("Bot: I couldn’t understand the due date. Skipping it.")
171
+ due_date = None
172
+
173
+ # --- 3) ATTEMPT TO CREATE THE TASK ---
174
+ print(f"Debug: Attempting to create task with name '{task_name}' and due date '{due_date}'")
175
+ result = create_asana_task(task_name, due_date)
176
+ # Provide a confirmation message if successful
177
+ if "error" in result:
178
+ print("Bot: Sorry, I had trouble creating the task:", result["error"])
179
+ else:
180
+ message = f"Bot: I've created your task '{result['name']}' (ID: {result['gid']})."
181
+ if due_date:
182
+ message += f" It's due on {due_date}."
183
+ print(message)
184
+
185
+ elif action == "LIST_TASKS":
186
+ # (Unmodified code for listing tasks)
187
+ filter_type = parsed.get("filter", "open") # Default to "open" if no filter is provided
188
+ only_open = filter_type == "open"
189
+ tasks = list_asana_tasks(only_open=only_open)
190
+ if "error" in tasks:
191
+ print("Bot: Sorry, I had trouble listing tasks:", tasks["error"])
192
+ elif not tasks:
193
+ if only_open:
194
+ print("Bot: You have no open tasks!")
195
+ else:
196
+ print("Bot: You have no tasks!")
197
+ else:
198
+ task_type = "open" if only_open else "all"
199
+ print(f"Here are your {task_type} tasks:")
200
+ last_task_list.clear() # Clear previous tasks
201
+ for t in tasks:
202
+ task_info = {'name': t['name'], 'gid': t['gid']}
203
+ last_task_list.append(task_info) # Store task info
204
+ print(f"- {t['name']} (ID: {t['gid']})")
205
+
206
+ elif action == "COMPLETE_TASK":
207
+ # (Unmodified code for completing tasks)
208
+ task_gid = parsed.get("task_gid")
209
+ task_name = parsed.get("name") # Capture task name for fuzzy matching
210
+
211
+ if task_gid:
212
+ result = complete_asana_task(task_gid)
213
+ if "error" in result:
214
+ print("Bot: Sorry, I couldn’t complete the task:", result["error"])
215
+ else:
216
+ print(f"Task '{result['name']}' marked as complete.")
217
+ elif task_name:
218
+ tasks = list_asana_tasks()
219
+ if "error" in tasks:
220
+ print("Bot: Sorry, I had trouble fetching tasks to find a match.")
221
+ return
222
+
223
+ matches = [t for t in tasks if task_name.lower() in t['name'].lower()]
224
+
225
+ if len(matches) == 1:
226
+ task_to_close = matches[0]
227
+ result = complete_asana_task(task_to_close["gid"])
228
+ if "error" in result:
229
+ print(f"Bot: Sorry, I couldn’t complete the task: {result['error']}")
230
+ else:
231
+ print(f"Task '{task_to_close['name']}' marked as complete.")
232
+ elif len(matches) > 1:
233
+ print("Bot: I found multiple tasks matching that name. "
234
+ "Please provide the ID of the task you'd like to close:")
235
+ for task in matches:
236
+ print(f"- {task['name']} (ID: {task['gid']})")
237
+ else:
238
+ print(f"Bot: I couldn’t find any tasks matching '{task_name}'.")
239
+ else:
240
+ # Attempt to extract ordinal-based task position.
241
+ ordinal_map = {
242
+ 'first': 1,
243
+ 'second': 2,
244
+ 'third': 3,
245
+ 'fourth': 4,
246
+ 'fifth': 5,
247
+ 'sixth': 6,
248
+ 'seventh': 7,
249
+ 'eighth': 8,
250
+ 'ninth': 9,
251
+ 'tenth': 10
252
+ }
253
+ words = user_message.lower().split()
254
+ ordinal_position = None
255
+ for word in words:
256
+ if word in ordinal_map:
257
+ ordinal_position = ordinal_map[word] - 1 # zero-based index
258
+ break
259
+ elif word.isdigit():
260
+ ordinal_position = int(word) - 1
261
+ break
262
+
263
+ if ordinal_position is not None and last_task_list:
264
+ if 0 <= ordinal_position < len(last_task_list):
265
+ task_to_close = last_task_list[ordinal_position]
266
+ result = complete_asana_task(task_to_close["gid"])
267
+ if "error" in result:
268
+ print(f"Bot: Sorry, I couldn’t complete the task: {result['error']}")
269
+ else:
270
+ print(f"Task '{task_to_close['name']}' marked as complete.")
271
+ else:
272
+ print("Bot: The task number you specified is out of range.")
273
+ else:
274
+ print("Bot: Please provide a valid task name, ID, or position to close.")
275
+ else:
276
+ # No recognized action, or just normal text
277
+ print(llm_output)
278
+
279
+ def extract_task_id_from_message(message):
280
+ """
281
+ Extract task ID (task_gid) from the user message.
282
+ Example input: "Can we close task 1234567890?"
283
+ Example output: "1234567890"
284
+ """
285
+ import re
286
+ # Use a regular expression to find a numeric sequence in the message
287
+ match = re.search(r'\b\d{10,}\b', message) # Look for 10+ digit numbers
288
+ if match:
289
+ return match.group(0) # Return the first match
290
+ return None # If no match found, return None
291
+
292
+ def parse_llm_response(llm_output):
293
+ try:
294
+ print(f"Debug: Raw LLM Content: {llm_output}") # Debug raw output
295
+
296
+ # Strip the backticks and "json" tag
297
+ if llm_output.startswith("```json") and llm_output.endswith("```"):
298
+ llm_output = llm_output.strip("```").strip("json").strip()
299
+
300
+ print(f"Debug: Cleaned LLM Output: {llm_output}") # Debug cleaned output
301
+
302
+ # Parse the cleaned JSON
303
+ parsed_response = json.loads(llm_output)
304
+ print(f"Debug: Parsed Response: {parsed_response}") # Debug parsed JSON
305
+ return parsed_response
306
+ except json.JSONDecodeError as e:
307
+ print(f"Error: Failed to parse LLM response: {e}")
308
+ return {"action": "NONE"} # Fallback
309
+ except Exception as e:
310
+ print(f"Error: Unexpected issue in parse_llm_response: {e}")
311
+ return {"action": "NONE"} # Fallback
312
+
313
+ def run_manual_chat():
314
+ print("Hello! I'm your Asana Copilot!")
315
+ print("I can help you create new tasks, list your tasks, and mark tasks as completed.")
316
+ print("Let me know how I can help.")
317
+ print("Want to end our chat? Just type 'quit' to exit.\n")
318
+
319
+ USER_PROMPT = "[USER]\n>>> "
320
+ TURN_BREAK = "-------------------\n"
321
+
322
+ while True:
323
+ user_input = input(USER_PROMPT).strip()
324
+ if user_input.lower() == "quit":
325
+ print("\nExiting the chat. Goodbye!")
326
+ break
327
+
328
+ print(TURN_BREAK + "[COPILOT]")
329
+ execute_turn(user_input)
330
+ print(TURN_BREAK)
331
+
332
+ system_prompt = """
333
+ You are a friendly AI Copilot that helps users interface with Asana -- namely creating new tasks, listing tasks, and marking tasks as complete.
334
+ You will interpret the user's request and respond with structured JSON.
335
+ Today's date is {today_date}.
336
+
337
+ Rules:
338
+ 1. If the user asks to create a task, respond with:
339
+ { "action": "CREATE_TASK", "name": "<TASK NAME>", "due": "<YYYY-MM-DD>" }
340
+ If they gave a date in any format. For words like 'tomorrow', interpret it as {today_date} + 1 day, etc.
341
+ If no date is given or you cannot parse it, omit the 'due' field.
342
+ 2. If the user asks to list tasks, respond with:
343
+ {"action": "LIST_TASKS", "filter": "open"} # For "list my open tasks" or similar
344
+ {"action": "LIST_TASKS", "filter": "all"} # For "list all my tasks" or similar
345
+ If the user specifies "open tasks" or similar, return only incomplete tasks. If the user specifies "all tasks," return all tasks (completed and incomplete).
346
+ If the intent is unclear, default to showing only open tasks.
347
+ 3. If the user asks to complete a task, respond with:
348
+ { "action": "COMPLETE_TASK", "task_gid": "<ID>" }
349
+ OR
350
+ { "action": "COMPLETE_TASK", "name": "<TASK NAME>" }
351
+ OR
352
+ { "action": "COMPLETE_TASK", "position": <NUMBER> }
353
+ Use 'position' if the user refers to a task by its position in the list (e.g., "third one").
354
+ 4. If no action is needed, respond with:
355
+ { "action": "NONE" }
356
+
357
+ Examples:
358
+ - User: "Close task 1209105096577103"
359
+ Response: { "action": "COMPLETE_TASK", "task_gid": "1209105096577103" }
360
+
361
+ - User: "Can you close rub jason's feet?"
362
+ Response: { "action": "COMPLETE_TASK", "name": "rub jason's feet" }
363
+
364
+ - User: "List all my tasks"
365
+ Response: { "action": "LIST_TASKS" }
366
+
367
+ - User: "Create a task called 'Finish report' due tomorrow"
368
+ Response: { "action": "CREATE_TASK", "name": "Finish report", "due": "2025-01-08" }
369
+
370
+ - User: "Close the third one"
371
+ Response: { "action": "COMPLETE_TASK", "position": 3 }
372
+
373
+ - User: "Complete task number 5"
374
+ Response: { "action": "COMPLETE_TASK", "position": 5 }
375
+
376
+ - {"action": "LIST_TASKS", "filter": "open"} # For "list my open tasks"
377
+ - {"action": "LIST_TASKS", "filter": "all"} # For "list all my tasks"
378
+ - {"action": "CREATE_TASK", "name": "Task Name", "due": "2025-01-15"}
379
+ - {"action": "COMPLETE_TASK", "task_gid": "1209105096577103"}
380
+
381
+ Again, always respond in JSON format. Example:
382
+ {
383
+ "action": "CREATE_TASK",
384
+ "name": "Submit Assignment",
385
+ "due": "2023-12-31"
386
+ }
387
+
388
+ If no action is required, respond with:
389
+ {
390
+ "action": "NONE"
391
+ }
392
  """
393
+
394
+ def process_input(user_input):
395
+ """
396
+ This function takes the user's input, processes it using the Asana Copilot logic,
397
+ and returns the formatted output for display.
398
+ """
399
+ global last_task_list # Make sure to handle global variables
400
+
401
+ # Simulate the conversation turn
402
+ response = "" # Initialize an empty string to accumulate the response
403
+
404
+ # Process the user input using the execute_turn function
405
+ # Capture the output from execute_turn (e.g., print statements) and append it to 'response'
406
+ import io
407
+ from contextlib import redirect_stdout
408
+
409
+ with io.StringIO() as buf, redirect_stdout(buf):
410
+ execute_turn(user_input)
411
+ output = buf.getvalue()
412
+
413
+ response += output # Append the captured output to the response
414
+
415
+ return response