Spaces:
Runtime error
Runtime error
| # app.py | |
| import os | |
| import re | |
| import json | |
| import requests | |
| import gradio as gr | |
| from google.oauth2.credentials import Credentials | |
| from googleapiclient.discovery import build | |
| from agentpro import create_model, ReactAgent | |
| from agentpro.tools import AresInternetTool | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 1) READ ENVIRONMENT VARIABLES (set in HF Space Secrets) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") | |
| GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") | |
| GOOGLE_CLIENT_SECRET= os.getenv("GOOGLE_CLIENT_SECRET") | |
| ARES_API_KEY = os.getenv("ARES_API_KEY") # if you are using AresInternetTool | |
| # Your HF Space URL (must match what you registered as redirect URI in Google Cloud Console) | |
| HF_SPACE_URL = "https://huggingface.co/spaces/case-llm-traversaal/calendar-chatbot" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 2) GLOBAL IN-MEMORY STORAGE FOR TOKENS (basic demo) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # In a production Βgrade multiuser app, key by user ID or IP. Here, we keep one βdefaultβ slot. | |
| user_tokens = { | |
| "default": None # Will hold a dict { "access_token": ..., "refresh_token": ..., "expiry": ... } | |
| } | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 3) HELPERS: Build Google Auth URL / Extract & Exchange Code / Build Service | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_auth_url(): | |
| """ | |
| Construct the Google OAuth2 authorization URL. When the user clicks 'Connect', | |
| we open this in a new tab. Google will redirect to HF_SPACE_URL?code=XXXX. | |
| """ | |
| base = "https://accounts.google.com/o/oauth2/v2/auth" | |
| scope = "https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events" | |
| params = { | |
| "client_id": GOOGLE_CLIENT_ID, | |
| "redirect_uri": HF_SPACE_URL, | |
| "response_type": "code", | |
| "scope": scope, | |
| "access_type": "offline", # get a refresh token | |
| "prompt": "consent" # always ask consent to receive a refresh token | |
| } | |
| # Build query string manually | |
| q = "&".join([f"{k}={requests.utils.quote(v)}" for k, v in params.items()]) | |
| return f"{base}?{q}" | |
| def extract_auth_code(full_redirect_url: str) -> str: | |
| """ | |
| The user pastes the entire redirect URL (which contains '?code=XYZ&scope=...' etc.). | |
| We pull out the 'XYZ' (authorization code) to exchange for tokens. | |
| """ | |
| match = re.search(r"[?&]code=([^&]+)", full_redirect_url) | |
| if match: | |
| return match.group(1) | |
| return None | |
| def exchange_code_for_tokens(auth_code: str): | |
| """ | |
| Given the singleβuse auth_code from Google, exchange it at Google's token endpoint | |
| for access_token + refresh_token + expiry. Then store it in user_tokens["default"]. | |
| """ | |
| token_url = "https://oauth2.googleapis.com/token" | |
| data = { | |
| "code": auth_code, | |
| "client_id": GOOGLE_CLIENT_ID, | |
| "client_secret": GOOGLE_CLIENT_SECRET, | |
| "redirect_uri": HF_SPACE_URL, | |
| "grant_type": "authorization_code", | |
| } | |
| resp = requests.post(token_url, data=data) | |
| if resp.status_code != 200: | |
| return None, resp.text | |
| token_data = resp.json() | |
| # Example token_data: { "access_token": "...", "expires_in": 3599, "refresh_token": "...", "scope": "...", "token_type": "Bearer" } | |
| user_tokens["default"] = { | |
| "access_token": token_data.get("access_token"), | |
| "refresh_token": token_data.get("refresh_token"), | |
| "token_uri": token_url, | |
| "client_id": GOOGLE_CLIENT_ID, | |
| "client_secret": GOOGLE_CLIENT_SECRET, | |
| "scopes": [ | |
| "https://www.googleapis.com/auth/calendar.readonly", | |
| "https://www.googleapis.com/auth/calendar.events" | |
| ] | |
| # Note: You could also store expiry/time; google.oauth2.credentials will handle refreshing automatically. | |
| } | |
| return token_data, None | |
| def get_calendar_service(): | |
| """ | |
| Build a googleapiclient βserviceβ object using the stored tokens. | |
| If tokens exist, we can construct google.oauth2.credentials.Credentials, | |
| which will auto-refresh if expired. | |
| """ | |
| token_info = user_tokens.get("default") | |
| if not token_info: | |
| return None # Not authenticated yet | |
| creds = Credentials( | |
| token=token_info["access_token"], | |
| refresh_token=token_info["refresh_token"], | |
| token_uri=token_info["token_uri"], | |
| client_id=token_info["client_id"], | |
| client_secret=token_info["client_secret"], | |
| scopes=token_info["scopes"], | |
| ) | |
| try: | |
| service = build("calendar", "v3", credentials=creds) | |
| except Exception as e: | |
| return None | |
| return service | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 4) IMPORT YOUR EXISTING TOOLS | |
| # (Assumes you placed them in a subfolder named βtools/β) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| from tools.current_datetime_tool import CurrentDateTimeTool | |
| from tools.daily_event_summary_tool import DailyEventSummaryTool | |
| from tools.weekly_event_summary_tool import WeeklyEventSummaryTool | |
| from tools.modify_event_tool import ModifyEventTool | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 5) FUNCTION TO BUILD A FRESH AGENT FOR EACH USER | |
| # (Because the βserviceβ object is user-specific once authenticated) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_agent_for_user(): | |
| """ | |
| 1. Build the OpenAI model via AgentPro. | |
| 2. Build a user-specific Google Calendar βserviceβ via stored tokens. | |
| 3. Instantiate each tool with that service (where required). | |
| 4. Return a ReactAgent ready to run queries. | |
| """ | |
| # 5.1) Create the LLM model | |
| model = create_model( | |
| provider = "openai", | |
| model_name = "gpt-3.5-turbo", | |
| api_key = OPENAI_API_KEY, | |
| temperature = 0.3 | |
| ) | |
| # 5.2) Get the userβs Google Calendar service | |
| service = get_calendar_service() | |
| if service is None: | |
| return None # Not authenticated yet | |
| # 5.3) Instantiate each tool, passing βserviceβ where needed | |
| daily_planner_tool = DailyEventSummaryTool(service=service) | |
| weekly_planner_tool = WeeklyEventSummaryTool(service=service) | |
| modify_event_tool = ModifyEventTool(service=service) | |
| current_dt_tool = CurrentDateTimeTool() | |
| ares_tool = AresInternetTool(ARES_API_KEY) | |
| tools_list = [ | |
| daily_planner_tool, | |
| weekly_planner_tool, | |
| current_dt_tool, | |
| modify_event_tool, | |
| ares_tool, | |
| ] | |
| # 5.4) Create and return the ReactAgent | |
| agent = ReactAgent(model=model, tools=tools_list) | |
| return agent | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 6) GRADIO CALLBACKS FOR AUTHENTICATION & EXCHANGE | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def open_google_oauth(): | |
| """ | |
| Called when user clicks βConnect Google Calendar.β Returns a message telling | |
| them to copy the URL that opens. | |
| """ | |
| url = build_auth_url() | |
| # Attempt to open in a new tab. (Browsers may block this; user can copy manually.) | |
| try: | |
| import webbrowser | |
| webbrowser.open_new_tab(url) | |
| except: | |
| pass | |
| return ( | |
| "β A new tab (or popup) should have opened for you to log in.\n\n" | |
| "If it did not, click this link manually:\n\n" | |
| f"{url}\n\n" | |
| "After granting access, youβll end up on an error page. Copy the \ | |
| entire URL from your browserβs address bar (it contains β?code=β¦β) \ | |
| and paste it below." | |
| ) | |
| def handle_auth_code(full_redirect_url: str): | |
| """ | |
| Called when user pastes the entire redirect URL back into our textbox. | |
| We parse out βcode=β¦β, exchange it, and report success or failure. | |
| """ | |
| code = extract_auth_code(full_redirect_url) | |
| if code is None: | |
| return "β Could not find βcodeβ in the URL you pasted. Please paste the exact URL you were redirected to." | |
| token_data, error = exchange_code_for_tokens(code) | |
| if error: | |
| return f"β Token exchange failed: {error}" | |
| return "β Successfully connected your Google Calendar! You can now ask about your events." | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 7) GRADIO CHAT FUNCTION (uses user-specific agent) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def chat_with_agent(user_input): | |
| """ | |
| This is called whenever the user sends a chat query. | |
| If not authenticated, prompt them to connect first. Otherwise, | |
| build a fresh agent for this user and get a response. | |
| """ | |
| # 7.1) Do we have a Calendar βserviceβ for this user yet? | |
| service = get_calendar_service() | |
| if service is None: | |
| return "β Please connect your Google Calendar first (use the button above)." | |
| # 7.2) Build a new Agent with the userβs Calendar service | |
| agent = build_agent_for_user() | |
| if agent is None: | |
| return "β Something went wrong building the agent. Ensure you completed Calendar authentication." | |
| # 7.3) Run the agent on the userβs input | |
| try: | |
| response = agent.run(user_input) | |
| return response.final_answer | |
| except Exception as e: | |
| return "β Exception in agent.run():\n" + repr(e) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # 8) ASSEMBLE GRADIO INTERFACE | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks() as demo: | |
| gr.Markdown("## ποΈ Calendar Chatbot (Gradio + HF Spaces)") | |
| # ββββββββββββββββ | |
| # A) CONNECT GOOGLE CALENDAR SECTION | |
| # ββββββββββββββββ | |
| with gr.Row(): | |
| connect_btn = gr.Button("π Connect Google Calendar") | |
| auth_message = gr.Textbox(label="Auth Instructions / Status", interactive=False) | |
| connect_btn.click(fn=open_google_oauth, outputs=auth_message) | |
| auth_code_input = gr.Textbox( | |
| lines=1, | |
| placeholder="Paste full Google redirect URL here (contains βcode=β¦β)", | |
| label="Step 2: Paste full redirect URL" | |
| ) | |
| auth_status = gr.Textbox(label="Authentication Result", interactive=False) | |
| auth_code_input.submit(fn=handle_auth_code, inputs=auth_code_input, outputs=auth_status) | |
| gr.Markdown("---") | |
| # ββββββββββββββββ | |
| # B) CHAT SECTION | |
| # ββββββββββββββββ | |
| chat_input = gr.Textbox(lines=2, placeholder="E.g., Whatβs on my calendar today?") | |
| chat_output = gr.Textbox(label="Response", interactive=False) | |
| chat_input.submit(fn=chat_with_agent, inputs=chat_input, outputs=chat_output) | |
| # You can also add a βSendβ button if you like: | |
| # send_btn = gr.Button("βοΈ Send") | |
| # send_btn.click(fn=chat_with_agent, inputs=chat_input, outputs=chat_output) | |
| demo.launch() | |