mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-12 00:28:31 -05:00
Compare commits
9 Commits
ci/test-op
...
swiftyos/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a86d750cf5 | ||
|
|
13bd648731 | ||
|
|
3d7ee7cc29 | ||
|
|
1ea52934cd | ||
|
|
78c2245269 | ||
|
|
f1c6c94636 | ||
|
|
113df689dc | ||
|
|
2c9563353e | ||
|
|
fb2a70e2d8 |
4
.github/workflows/platform-frontend-ci.yml
vendored
4
.github/workflows/platform-frontend-ci.yml
vendored
@@ -13,8 +13,8 @@ on:
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.pull_request.number || '' }}
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'merge_group' && format('merge-queue-{0}', github.ref) || format('{0}-{1}', github.ref, github.event.pull_request.number || github.sha) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
|
||||
4
.github/workflows/platform-fullstack-ci.yml
vendored
4
.github/workflows/platform-fullstack-ci.yml
vendored
@@ -13,8 +13,8 @@ on:
|
||||
merge_group:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event.pull_request.number || '' }}
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'merge_group' && format('merge-queue-{0}', github.ref) || github.head_ref && format('pr-{0}', github.event.pull_request.number) || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
defaults:
|
||||
run:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: start-core stop-core logs-core format lint migrate run-backend run-frontend
|
||||
.PHONY: start-core stop-core logs-core format lint migrate run-backend run-frontend load-store-agents
|
||||
|
||||
# Run just Supabase + Redis + RabbitMQ
|
||||
start-core:
|
||||
@@ -42,7 +42,10 @@ run-frontend:
|
||||
|
||||
test-data:
|
||||
cd backend && poetry run python test/test_data_creator.py
|
||||
|
||||
|
||||
load-store-agents:
|
||||
cd backend && poetry run python test/load_store_agents.py
|
||||
|
||||
help:
|
||||
@echo "Usage: make <target>"
|
||||
@echo "Targets:"
|
||||
@@ -54,4 +57,5 @@ help:
|
||||
@echo " migrate - Run backend database migrations"
|
||||
@echo " run-backend - Run the backend FastAPI server"
|
||||
@echo " run-frontend - Run the frontend Next.js development server"
|
||||
@echo " test-data - Run the test data creator"
|
||||
@echo " test-data - Run the test data creator"
|
||||
@echo " load-store-agents - Load store agents from agents/ folder into test database"
|
||||
242
autogpt_platform/backend/agents/StoreAgent_rows.csv
Normal file
242
autogpt_platform/backend/agents/StoreAgent_rows.csv
Normal file
@@ -0,0 +1,242 @@
|
||||
listing_id,storeListingVersionId,slug,agent_name,agent_video,agent_image,featured,sub_heading,description,categories,useForOnboarding,is_available
|
||||
6e60a900-9d7d-490e-9af2-a194827ed632,d85882b8-633f-44ce-a315-c20a8c123d19,flux-ai-image-generator,Flux AI Image Generator,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/ca154dd1-140e-454c-91bd-2d8a00de3f08.jpg"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/577d995d-bc38-40a9-a23f-1f30f5774bdb.jpg"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/415db1b7-115c-43ab-bd6c-4e9f7ef95be1.jpg""]",false,Transform ideas into breathtaking images,"Transform ideas into breathtaking images with this AI-powered Image Generator. Using cutting-edge Flux AI technology, the tool crafts highly detailed, photorealistic visuals from simple text prompts. Perfect for artists, marketers, and content creators, this generator produces unique images tailored to user specifications. From fantastical scenes to lifelike portraits, users can unleash creativity with professional-quality results in seconds. Easy to use and endlessly versatile, bring imagination to life with the AI Image Generator today!","[""creative""]",false,true
|
||||
f11fc6e9-6166-4676-ac5d-f07127b270c1,c775f60d-b99f-418b-8fe0-53172258c3ce,youtube-transcription-scraper,YouTube Transcription Scraper,https://youtu.be/H8S3pU68lGE,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/65bce54b-0124-4b0d-9e3e-f9b89d0dc99e.jpg""]",false,Fetch the transcriptions from the most popular YouTube videos in your chosen topic,"Effortlessly gather transcriptions from multiple YouTube videos with this agent. It scrapes and compiles video transcripts into a clean, organized list, making it easy to extract insights, quotes, or content from various sources in one go. Ideal for researchers, content creators, and marketers looking to quickly analyze or repurpose video content.","[""writing""]",false,true
|
||||
17908889-b599-4010-8e4f-bed19b8f3446,6e16e65a-ad34-4108-b4fd-4a23fced5ea2,business-ownerceo-finder,Decision Maker Lead Finder,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/1020d94e-b6a2-4fa7-bbdf-2c218b0de563.jpg""]",false,Contact CEOs today,"Find the key decision-makers you need, fast.
|
||||
|
||||
This agent identifies business owners or CEOs of local companies in any area you choose. Simply enter what kind of businesses you’re looking for and where, and it will:
|
||||
|
||||
* Search the area and gather public information
|
||||
* Return names, roles, and contact details when available
|
||||
* Provide smart Google search suggestions if details aren’t found
|
||||
|
||||
Perfect for:
|
||||
|
||||
* B2B sales teams seeking verified leads
|
||||
* Recruiters sourcing local talent
|
||||
* Researchers looking to connect with business leaders
|
||||
|
||||
Save hours of manual searching and get straight to the people who matter most.","[""business""]",true,true
|
||||
72beca1d-45ea-4403-a7ce-e2af168ee428,415b7352-0dc6-4214-9d87-0ad3751b711d,smart-meeting-brief,Smart Meeting Prep,https://youtu.be/9ydZR2hkxaY,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/2f116ce1-63ae-4d39-a5cd-f514defc2b97.png"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/0a71a60a-2263-4f12-9836-9c76ab49f155.png"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/95327695-9184-403c-907a-a9d3bdafa6a5.png"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/2bc77788-790b-47d4-8a61-ce97b695e9f5.png""]",true,Business meeting briefings delivered daily,"Never walk into a meeting unprepared again. Every day at 4 pm, the Smart Meeting Prep Agent scans your calendar for tomorrow's external meetings. It reviews your past email exchanges, researches each participant's background and role, and compiles the insights into a concise briefing, so you can close your workday ready for tomorrow's calls.
|
||||
|
||||
How It Works
|
||||
1. At 4 pm, the agent scans your calendar and identifies external meetings scheduled for the next day.
|
||||
2. It reviews recent email threads with each participant to surface key relationship history and communication context.
|
||||
3. It conducts online research to gather publicly available information on roles, company backgrounds, and relevant professional data.
|
||||
4. It produces a unified briefing for each participant, including past exchange highlights, profile notes, and strategic conversation points.","[""personal""]",true,true
|
||||
9fa5697a-617b-4fae-aea0-7dbbed279976,b8ceb480-a7a2-4c90-8513-181a49f7071f,automated-support-ai,Automated Support Agent,https://youtu.be/nBMfu_5sgDA,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/ed56febc-2205-4179-9e7e-505d8500b66c.png""]",true,Automate up to 80 percent of inbound support emails,"Overview:
|
||||
Support teams spend countless hours on basic tickets. This agent automates repetitive customer support tasks. It reads incoming requests, researches your knowledge base, and responds automatically when confident. When unsure, it escalates to a human for final resolution.
|
||||
|
||||
How it Works:
|
||||
New support emails are routed to the agent.
|
||||
The agent checks internal documentation for answers.
|
||||
It measures confidence in the answer found and either replies directly or escalates to a human.
|
||||
|
||||
Business Value:
|
||||
Automating the easy 80 percent of support tickets allows your team to focus on high-value, complex customer issues, improving efficiency and response times.","[""business""]",false,true
|
||||
2bdac92b-a12c-4131-bb46-0e3b89f61413,31daf49d-31d3-476b-aa4c-099abc59b458,unspirational-poster-maker,Unspirational Poster Maker,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/6a490dac-27e5-405f-a4c4-8d1c55b85060.jpg"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/d343fbb5-478c-4e38-94df-4337293b61f1.jpg""]",false,Because adulting is hard,"This witty AI agent generates hilariously relatable ""motivational"" posters that tackle the everyday struggles of procrastination, overthinking, and workplace chaos with a blend of absurdity and sarcasm. From goldfish facing impossible tasks to cats in existential crises, The Unspirational Poster Maker designs tongue-in-cheek graphics and captions that mock productivity clichés and embrace our collective struggles to ""get it together."" Perfect for adding a touch of humour to the workday, these posters remind us that sometimes, all we can do is laugh at the chaos.","[""creative""]",false,true
|
||||
9adf005e-2854-4cc7-98cf-f7103b92a7b7,a03b0d8c-4751-43d6-a54e-c3b7856ba4e3,ai-shortform-video-generator-create-viral-ready-content,AI Video Generator,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/8d2670b9-fea5-4966-a597-0a4511bffdc3.png"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/aabe8aec-0110-4ce7-a259-4f86fe8fe07d.png""]",false,Create Viral-Ready Shorts Content in Seconds,"OVERVIEW
|
||||
Transform any trending headline or broad topic into a polished, vertical short-form video in a single run.
|
||||
The agent automates research, scriptwriting, metadata creation, and Revid.ai rendering, returning one ready-to-publish MP4 plus its title, script and hashtags.
|
||||
|
||||
HOW IT WORKS
|
||||
1. Input a topic or an exact news headline.
|
||||
2. The agent fetches live search results and selects the most engaging related story.
|
||||
3. Key facts are summarised into concise research notes.
|
||||
4. Claude writes a 30–35 second script with visual cues, a three-second hook, tension loops, and a call-to-action.
|
||||
5. GPT-4o generates an eye-catching title and one or two discoverability hashtags.
|
||||
6. The script is sent to a state-of-the-art AI video generator to render a single 9:16 MP4 (default: 720 p, 30 fps, voice “Brian”, style “movingImage”, music “Bladerunner 2049”).
|
||||
– All voice, style and resolution settings can be adjusted in the Builder before you press ""Run"".
|
||||
7. Output delivered: Title, Script, Hashtags, Video URL.
|
||||
|
||||
KEY USE CASES
|
||||
- Broad-topic explainers (e.g. “Artificial Intelligence” or “Climate Tech”).
|
||||
- Real-time newsjacking with a specific breaking headline.
|
||||
- Product-launch spotlights and quick event recaps while interest is high.
|
||||
|
||||
BUSINESS VALUE
|
||||
- One-click speed: from idea to finished video in minutes.
|
||||
- Consistent brand look: Revid presets keep voice, style and aspect ratio on spec.
|
||||
- No-code workflow: marketers create social video without design or development queues.
|
||||
- Cloud convenience: Auto-GPT Cloud users are pre-configured with all required keys.
|
||||
Self-hosted users simply add OpenAI, Anthropic, Perplexity (OpenRouter/Jina) and Revid keys once.
|
||||
|
||||
IMPORTANT NOTES
|
||||
- The agent outputs exactly one video per execution. Run it again for additional shorts.
|
||||
- Video rendering time varies; AI-generated footage may take several minutes.","[""writing""]",false,true
|
||||
864e48ef-fee5-42c1-b6a4-2ae139db9fc1,55d40473-0f31-4ada-9e40-d3a7139fcbd4,automated-blog-writer,Automated SEO Blog Writer,https://youtu.be/nKcDCbDVobs,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/2dd5f95b-5b30-4bf8-a11b-bac776c5141a.jpg""]",true,"Automate research, writing, and publishing for high-ranking blog posts","Scale your blog with a fully automated content engine. The Automated SEO Blog Writer learns your brand voice, finds high-demand keywords, and creates SEO-optimized articles that attract organic traffic and boost visibility.
|
||||
|
||||
How it works:
|
||||
|
||||
1. Share your pitch, website, and values.
|
||||
2. The agent studies your site and uncovers proven SEO opportunities.
|
||||
3. It spends two hours researching and drafting each post.
|
||||
4. You set the cadence—publishing runs on autopilot.
|
||||
|
||||
Business value: Consistently publish research-backed, optimized posts that build domain authority, rankings, and thought leadership while you focus on what matters most.
|
||||
|
||||
Use cases:
|
||||
• Founders: Keep your blog active with no time drain.
|
||||
• Agencies: Deliver scalable SEO content for clients.
|
||||
• Strategists: Automate execution, focus on strategy.
|
||||
• Marketers: Drive steady organic growth.
|
||||
• Local businesses: Capture nearby search traffic.","[""writing""]",false,true
|
||||
6046f42e-eb84-406f-bae0-8e052064a4fa,a548e507-09a7-4b30-909c-f63fcda10fff,lead-finder-local-businesses,Lead Finder,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/abd6605f-d5f8-426b-af36-052e8ba5044f.webp""]",false,Auto-Prospect Like a Pro,"Turbo-charge your local lead generation with the AutoGPT Marketplace’s top Google Maps prospecting agent. “Lead Finder: Local Businesses” delivers verified, ready-to-contact prospects in any niche and city—so you can focus on closing, not searching.
|
||||
|
||||
**WHAT IT DOES**
|
||||
• Searches Google Maps via the official API (no scraping)
|
||||
• Prompts like “dentists in Chicago” or “coffee shops near me”
|
||||
• Returns: Name, Website, Rating, Reviews, **Phone & Address**
|
||||
• Exports instantly to your CRM, sheet, or outreach workflow
|
||||
|
||||
**WHY YOU’LL LOVE IT**
|
||||
✓ Hyper-targeted leads in minutes
|
||||
✓ Unlimited searches & locations
|
||||
✓ Zero CAPTCHAs or IP blocks
|
||||
✓ Works on AutoGPT Cloud or self-hosted (with your API key)
|
||||
✓ Cut prospecting time by 90%
|
||||
|
||||
**PERFECT FOR**
|
||||
— Marketers & PPC agencies
|
||||
— SEO consultants & designers
|
||||
— SaaS founders & sales teams
|
||||
|
||||
Stop scrolling directories—start filling your pipeline. Start now and let AI prospect while you profit.
|
||||
|
||||
→ Click *Add to Library* and own your market today.","[""business""]",true,true
|
||||
f623c862-24e9-44fc-8ce8-d8282bb51ad2,eafa21d3-bf14-4f63-a97f-a5ee41df83b3,linkedin-post-generator,LinkedIn Post Generator,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/297f6a8e-81a8-43e2-b106-c7ad4a5662df.png"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/fceebdc1-aef6-4000-97fc-4ef587f56bda.png""]",false,Auto‑craft LinkedIn gold,"Create research‑driven, high‑impact LinkedIn posts in minutes. This agent searches YouTube for the best videos on your chosen topic, pulls their transcripts, and distils the most valuable insights into a polished post ready for your company page or personal feed.
|
||||
|
||||
FEATURES
|
||||
• Automated YouTube research – discovers and analyses top‑ranked videos so you don’t have to
|
||||
• AI‑curated synthesis – combines multiple transcripts into one authoritative narrative
|
||||
• Full creative control – adjust style, tone, objective, opinion, clarity, target word count and number of videos
|
||||
• LinkedIn‑optimised output – hook, 2‑3 key points, CTA, strategic line breaks, 3‑5 hashtags, no markdown
|
||||
• One‑click publish – returns a ready‑to‑post text block (≤1 300 characters)
|
||||
|
||||
HOW IT WORKS
|
||||
1. Enter a topic and your preferred writing parameters.
|
||||
2. The agent builds a YouTube search, fetches the page, and extracts the top N video URLs.
|
||||
3. It pulls each transcript, then feeds them—plus your settings—into Claude 3.5 Sonnet.
|
||||
4. The model writes a concise, engaging post designed for maximum LinkedIn engagement.
|
||||
|
||||
USE CASES
|
||||
• Thought‑leadership updates backed by fresh video research
|
||||
• Rapid industry summaries after major events, webinars, or conferences
|
||||
• Consistent LinkedIn content for busy founders, marketers, and creators
|
||||
|
||||
WHY YOU’LL LOVE IT
|
||||
Save hours of manual research, avoid surface‑level hot‑takes, and publish posts that showcase real expertise—without the heavy lift.","[""writing""]",true,true
|
||||
7d4120ad-b6b3-4419-8bdb-7dd7d350ef32,e7bb29a1-23c7-4fee-aa3b-5426174b8c52,youtube-to-linkedin-post-converter,YouTube to LinkedIn Post Converter,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/f084b326-a708-4396-be51-7ba59ad2ef32.png""]",false,Transform Your YouTube Videos into Engaging LinkedIn Posts with AI,"WHAT IT DOES:
|
||||
This agent converts YouTube video content into a LinkedIn post by analyzing the video's transcript. It provides you with a tailored post that reflects the core ideas, key takeaways, and tone of the original video, optimizing it for engagement on LinkedIn.
|
||||
|
||||
HOW IT WORKS:
|
||||
- You provide the URL to the YouTube video (required)
|
||||
- You can choose the structure for the LinkedIn post (e.g., Personal Achievement Story, Lesson Learned, Thought Leadership, etc.)
|
||||
- You can also select the tone (e.g., Inspirational, Analytical, Conversational, etc.)
|
||||
- The transcript of the video is analyzed by the GPT-4 model and the Claude 3.5 Sonnet model
|
||||
- The models extract key insights, memorable quotes, and the main points from the video
|
||||
- You’ll receive a LinkedIn post, formatted according to your chosen structure and tone, optimized for professional engagement
|
||||
|
||||
INPUTS:
|
||||
- Source YouTube Video – Provide the URL to the YouTube video
|
||||
- Structure – Choose the post format (e.g., Personal Achievement Story, Thought Leadership, etc.)
|
||||
- Content – Specify the main message or idea of the post (e.g., Hot Take, Key Takeaways, etc.)
|
||||
- Tone – Select the tone for the post (e.g., Conversational, Inspirational, etc.)
|
||||
|
||||
OUTPUT:
|
||||
- LinkedIn Post – A well-crafted, AI-generated LinkedIn post with a professional tone, based on the video content and your specified preferences
|
||||
|
||||
Perfect for content creators, marketers, and professionals who want to repurpose YouTube videos for LinkedIn and boost their professional branding.","[""writing""]",false,true
|
||||
c61d6a83-ea48-4df8-b447-3da2d9fe5814,00fdd42c-a14c-4d19-a567-65374ea0e87f,personalized-morning-coffee-newsletter,Personal Newsletter,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/f4b38e4c-8166-4caf-9411-96c9c4c82d4c.png""]",false,Start your day with personalized AI newsletters that deliver credibility and context for every interest or mood.,"This Personal Newsletter Agent provides a bespoke daily digest on your favorite topics and tone. Whether you prefer industry insights, lighthearted reads, or breaking news, this agent crafts your own unique newsletter to keep you informed and entertained.
|
||||
|
||||
|
||||
How It Works
|
||||
1. Enter your favorite topics, industries, or areas of interest.
|
||||
2. Choose your tone—professional, casual, or humorous.
|
||||
3. Set your preferred delivery cadence: daily or weekly.
|
||||
4. The agent scans top sources and compiles 3–5 engaging stories, insights, and fun facts into a conversational newsletter.
|
||||
|
||||
Skip the morning scroll and enjoy a thoughtfully curated newsletter designed just for you. Stay ahead of trends, spark creative ideas, and enjoy an effortless, informed start to your day.
|
||||
|
||||
|
||||
Use Cases
|
||||
• Executives: Get a daily digest of market updates and leadership insights.
|
||||
• Marketers: Receive curated creative trends and campaign inspiration.
|
||||
• Entrepreneurs: Stay updated on your industry without information overload.","[""research""]",true,true
|
||||
e2e49cfc-4a39-4d62-a6b3-c095f6d025ff,fc2c9976-0962-4625-a27b-d316573a9e7f,email-address-finder,Email Scout - Contact Finder Assistant,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/da8a690a-7a8b-4c1d-b6f8-e2f840c0205d.jpg"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/6a2ac25c-1609-4881-8140-e6da2421afb3.jpg"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/26179263-fe06-45bd-b6a0-0754660a0a46.jpg""]",false,Find contact details from name and location using AI search,"Finding someone's professional email address can be time-consuming and frustrating. Manual searching across multiple websites, social profiles, and business directories often leads to dead ends or outdated information.
|
||||
|
||||
Email Scout automates this process by intelligently searching across publicly available sources when you provide a person's name and location. Simply input basic information like ""Tim Cook, USA"" or ""Sarah Smith, London"" and let the AI assistant do the work of finding potential contact details.
|
||||
|
||||
Key Features:
|
||||
- Quick search from just name and location
|
||||
- Scans multiple public sources
|
||||
- Automated AI-powered search process
|
||||
- Easy to use with simple inputs
|
||||
|
||||
Perfect for recruiters, business development professionals, researchers, and anyone needing to establish professional contact.
|
||||
|
||||
Note: This tool searches only publicly available information. Search results depend on what contact information people have made public. Some searches may not yield results if the information isn't publicly accessible.","[""""]",false,true
|
||||
81bcc372-0922-4a36-bc35-f7b1e51d6939,e437cc95-e671-489d-b915-76561fba8c7f,ai-youtube-to-blog-converter,YouTube Video to SEO Blog Writer,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/239e5a41-2515-4e1c-96ef-31d0d37ecbeb.webp"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/c7d96966-786f-4be6-ad7d-3a51c84efc0e.png"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/0275a74c-e2c2-4e29-a6e4-3a616c3c35dd.png""]",false,One link. One click. One powerful blog post.,"Effortlessly transform your YouTube videos into high-quality, SEO-optimized blog posts.
|
||||
|
||||
Your videos deserve a second life—in writing.
|
||||
Make your content work twice as hard by repurposing it into engaging, searchable articles.
|
||||
|
||||
Perfect for content creators, marketers, and bloggers, this tool analyzes video content and generates well-structured blog posts tailored to your tone, audience, and word count. Just paste a YouTube URL and let the AI handle the rest.
|
||||
|
||||
FEATURES
|
||||
|
||||
• CONTENT ANALYSIS
|
||||
Extracts key points from the video while preserving your message and intent.
|
||||
|
||||
• CUSTOMIZABLE OUTPUT
|
||||
Select a tone that fits your audience: casual, professional, educational, or formal.
|
||||
|
||||
• SEO OPTIMIZATION
|
||||
Automatically creates engaging titles and structured subheadings for better search visibility.
|
||||
|
||||
• USER-FRIENDLY
|
||||
Repurpose your videos into written content to expand your reach and improve accessibility.
|
||||
|
||||
Whether you're looking to grow your blog, boost SEO, or simply get more out of your content, the AI YouTube-to-Blog Converter makes it effortless.
|
||||
","[""writing""]",true,true
|
||||
5c3510d2-fc8b-4053-8e19-67f53c86eb1a,f2cc74bb-f43f-4395-9c35-ecb30b5b4fc9,ai-webpage-copy-improver,AI Webpage Copy Improver,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/d562d26f-5891-4b09-8859-fbb205972313.jpg""]",false,Boost Your Website's Search Engine Performance,"Elevate your web content with this powerful AI Webpage Copy Improver. Designed for marketers, SEO specialists, and web developers, this tool analyses and enhances website copy for maximum impact. Using advanced language models, it optimizes text for better clarity, SEO performance, and increased conversion rates. The AI examines your existing content, identifies areas for improvement, and generates refined copy that maintains your brand voice while boosting engagement. From homepage headlines to product descriptions, transform your web presence with AI-driven insights. Improve readability, incorporate targeted keywords, and craft compelling calls-to-action - all with the click of a button. Take your digital marketing to the next level with the AI Webpage Copy Improver.","[""marketing""]",true,true
|
||||
94d03bd3-7d44-4d47-b60c-edb2f89508d6,b6f6f0d3-49f4-4e3b-8155-ffe9141b32c0,domain-name-finder,Domain Name Finder,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/28545e09-b2b8-4916-b4c6-67f982510a78.jpeg""]",false,Instantly generate brand-ready domain names that are actually available,"Overview:
|
||||
Finding a domain name that fits your brand shouldn’t take hours of searching and failed checks. The Domain Name Finder Agent turns your pitch into hundreds of creative, brand-ready domain ideas—filtered by live availability so every result is actionable.
|
||||
|
||||
How It Works
|
||||
1. Input your product pitch, company name, or core keywords.
|
||||
2. The agent analyzes brand tone, audience, and industry context.
|
||||
3. It generates a list of unique, memorable domains that match your criteria.
|
||||
4. All names are pre-filtered for real-time availability, so you can register immediately.
|
||||
|
||||
|
||||
Business Value
|
||||
Save hours of guesswork and eliminate dead ends. Accelerate brand launches, startup naming, and campaign creation with ready-to-claim domains.
|
||||
|
||||
|
||||
Key Use Cases
|
||||
• Startup Founders: Quickly find brand-ready domains for MVP launches or rebrands.
|
||||
• Marketers: Test name options across campaigns with instant availability data.
|
||||
• Entrepreneurs: Validate ideas faster with instant domain options.","[""business""]",false,true
|
||||
7a831906-daab-426f-9d66-bcf98d869426,516d813b-d1bc-470f-add7-c63a4b2c2bad,ai-function,AI Function,,"[""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/620e8117-2ee1-4384-89e6-c2ef4ec3d9c9.webp"",""https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/476259e2-5a79-4a7b-8e70-deeebfca70d7.png""]",false,Never Code Again,"AI FUNCTION MAGIC
|
||||
Your AI‑powered assistant for turning plain‑English descriptions into working Python functions.
|
||||
|
||||
HOW IT WORKS
|
||||
1. Describe what the function should do.
|
||||
2. Specify the inputs it needs.
|
||||
3. Receive the generated Python code.
|
||||
|
||||
FEATURES
|
||||
- Effortless Function Generation: convert natural‑language specs into complete functions.
|
||||
- Customizable Inputs: define the parameters that matter to you.
|
||||
- Versatile Use Cases: simulate data, automate tasks, prototype ideas.
|
||||
- Seamless Integration: add the generated function directly to your codebase.
|
||||
|
||||
EXAMPLE
|
||||
Request: “Create a function that generates 20 examples of fake people, each with a name, date of birth, job title, and age.”
|
||||
Input parameter: number_of_people (default 20)
|
||||
Result: a list of dictionaries such as
|
||||
[
|
||||
{ ""name"": ""Emma Martinez"", ""date_of_birth"": ""1992‑11‑03"", ""job_title"": ""Data Analyst"", ""age"": 32 },
|
||||
{ ""name"": ""Liam O’Connor"", ""date_of_birth"": ""1985‑07‑19"", ""job_title"": ""Marketing Manager"", ""age"": 39 },
|
||||
…18 more entries…
|
||||
]","[""development""]",false,true
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,590 @@
|
||||
{
|
||||
"id": "7b2e2095-782a-4f8d-adda-e62b661bccf5",
|
||||
"version": 29,
|
||||
"is_active": false,
|
||||
"name": "Unspirational Poster Maker",
|
||||
"description": "This witty AI agent generates hilariously relatable \"motivational\" posters that tackle the everyday struggles of procrastination, overthinking, and workplace chaos with a blend of absurdity and sarcasm. From goldfish facing impossible tasks to cats in existential crises, The Unspirational Poster Maker designs tongue-in-cheek graphics and captions that mock productivity clich\u00e9s and embrace our collective struggles to \"get it together.\" Perfect for adding a touch of humour to the workday, these posters remind us that sometimes, all we can do is laugh at the chaos.",
|
||||
"instructions": null,
|
||||
"recommended_schedule_cron": null,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"block_id": "363ae599-353e-4804-937e-b2ee3cef3da4",
|
||||
"input_default": {
|
||||
"name": "Generated Image",
|
||||
"description": "The resulting generated image ready for you to review and post."
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 2329.937006807125,
|
||||
"y": 80.49068076698347
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "c6c511e8-e6a4-4969-9bc8-f67d60c1e229",
|
||||
"source_id": "86665e90-ffbf-48fb-ad3f-e5d31fd50c51",
|
||||
"sink_id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "20845dda-91de-4508-8077-0504b1a5ae03",
|
||||
"source_id": "28bda769-b88b-44c9-be5c-52c2667f137e",
|
||||
"sink_id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "6524c611-774b-45e9-899d-9a6aa80c549c",
|
||||
"source_id": "e7cdc1a2-4427-4a8a-a31b-63c8e74842f8",
|
||||
"sink_id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "714a0821-e5ba-4af7-9432-50491adda7b1",
|
||||
"source_id": "576c5677-9050-4d1c-aad4-36b820c04fef",
|
||||
"sink_id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [],
|
||||
"graph_id": "7b2e2095-782a-4f8d-adda-e62b661bccf5",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "7e026d19-f9a6-412f-8082-610f9ba0c410",
|
||||
"block_id": "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b",
|
||||
"input_default": {
|
||||
"name": "Theme",
|
||||
"value": "Cooking"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": -1219.5966324967521,
|
||||
"y": 80.50339731789956
|
||||
}
|
||||
},
|
||||
"input_links": [],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "8c2bd1f7-b17b-4835-81b6-bb336097aa7a",
|
||||
"source_id": "7e026d19-f9a6-412f-8082-610f9ba0c410",
|
||||
"sink_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_THEME",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"graph_id": "7b2e2095-782a-4f8d-adda-e62b661bccf5",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "28bda769-b88b-44c9-be5c-52c2667f137e",
|
||||
"block_id": "6ab085e2-20b3-4055-bc3e-08036e01eca6",
|
||||
"input_default": {
|
||||
"upscale": "No Upscale"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 1132.373897280427,
|
||||
"y": 88.44610377514573
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "54588c74-e090-4e49-89e4-844b9952a585",
|
||||
"source_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"sink_id": "28bda769-b88b-44c9-be5c-52c2667f137e",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "20845dda-91de-4508-8077-0504b1a5ae03",
|
||||
"source_id": "28bda769-b88b-44c9-be5c-52c2667f137e",
|
||||
"sink_id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "7b2e2095-782a-4f8d-adda-e62b661bccf5",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "e7cdc1a2-4427-4a8a-a31b-63c8e74842f8",
|
||||
"block_id": "6ab085e2-20b3-4055-bc3e-08036e01eca6",
|
||||
"input_default": {
|
||||
"upscale": "No Upscale"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 590.7543882245375,
|
||||
"y": 85.69546832466654
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "66646786-3006-4417-a6b7-0158f2603d1d",
|
||||
"source_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"sink_id": "e7cdc1a2-4427-4a8a-a31b-63c8e74842f8",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "6524c611-774b-45e9-899d-9a6aa80c549c",
|
||||
"source_id": "e7cdc1a2-4427-4a8a-a31b-63c8e74842f8",
|
||||
"sink_id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "7b2e2095-782a-4f8d-adda-e62b661bccf5",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "576c5677-9050-4d1c-aad4-36b820c04fef",
|
||||
"block_id": "6ab085e2-20b3-4055-bc3e-08036e01eca6",
|
||||
"input_default": {
|
||||
"upscale": "No Upscale"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 60.48904654237981,
|
||||
"y": 86.06183359510214
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "201d3e03-bc06-4cee-846d-4c3c804d8857",
|
||||
"source_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"sink_id": "576c5677-9050-4d1c-aad4-36b820c04fef",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "714a0821-e5ba-4af7-9432-50491adda7b1",
|
||||
"source_id": "576c5677-9050-4d1c-aad4-36b820c04fef",
|
||||
"sink_id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "7b2e2095-782a-4f8d-adda-e62b661bccf5",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "86665e90-ffbf-48fb-ad3f-e5d31fd50c51",
|
||||
"block_id": "6ab085e2-20b3-4055-bc3e-08036e01eca6",
|
||||
"input_default": {
|
||||
"prompt": "A cat sprawled dramatically across an important-looking document during a work-from-home meeting, making direct eye contact with the camera while knocking over a coffee mug in slow motion. Text Overlay: \"Chaos is a career path. Be the obstacle everyone has to work around.\"",
|
||||
"upscale": "No Upscale"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 1668.3572666956795,
|
||||
"y": 89.69665262457966
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "509b7587-1940-4a06-808d-edde9a74f400",
|
||||
"source_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"sink_id": "86665e90-ffbf-48fb-ad3f-e5d31fd50c51",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "c6c511e8-e6a4-4969-9bc8-f67d60c1e229",
|
||||
"source_id": "86665e90-ffbf-48fb-ad3f-e5d31fd50c51",
|
||||
"sink_id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "7b2e2095-782a-4f8d-adda-e62b661bccf5",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"block_id": "1f292d4a-41a4-4977-9684-7c8d560b9f91",
|
||||
"input_default": {
|
||||
"model": "gpt-4o",
|
||||
"prompt": "<example_output>\nA photo of a sloth lounging on a desk, with its head resting on a keyboard. The keyboard is on top of a laptop with a blank spreadsheet open. A to-do list is placed beside the laptop, with the top item written as \"Do literally anything\". There is a text overlay that says \"If you can't outwork them, outnap them.\".\n</example_output>\n\nCreate a relatable satirical, snarky, user-deprecating motivational style image based on the theme: \"{{THEME}}\".\n\nOutput only the image description and caption, without any additional commentary or formatting.",
|
||||
"prompt_values": {}
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": -561.1139207164056,
|
||||
"y": 78.60434452403524
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "8c2bd1f7-b17b-4835-81b6-bb336097aa7a",
|
||||
"source_id": "7e026d19-f9a6-412f-8082-610f9ba0c410",
|
||||
"sink_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_THEME",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "54588c74-e090-4e49-89e4-844b9952a585",
|
||||
"source_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"sink_id": "28bda769-b88b-44c9-be5c-52c2667f137e",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "201d3e03-bc06-4cee-846d-4c3c804d8857",
|
||||
"source_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"sink_id": "576c5677-9050-4d1c-aad4-36b820c04fef",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "509b7587-1940-4a06-808d-edde9a74f400",
|
||||
"source_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"sink_id": "86665e90-ffbf-48fb-ad3f-e5d31fd50c51",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "66646786-3006-4417-a6b7-0158f2603d1d",
|
||||
"source_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"sink_id": "e7cdc1a2-4427-4a8a-a31b-63c8e74842f8",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "7b2e2095-782a-4f8d-adda-e62b661bccf5",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "66646786-3006-4417-a6b7-0158f2603d1d",
|
||||
"source_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"sink_id": "e7cdc1a2-4427-4a8a-a31b-63c8e74842f8",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "c6c511e8-e6a4-4969-9bc8-f67d60c1e229",
|
||||
"source_id": "86665e90-ffbf-48fb-ad3f-e5d31fd50c51",
|
||||
"sink_id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "6524c611-774b-45e9-899d-9a6aa80c549c",
|
||||
"source_id": "e7cdc1a2-4427-4a8a-a31b-63c8e74842f8",
|
||||
"sink_id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "20845dda-91de-4508-8077-0504b1a5ae03",
|
||||
"source_id": "28bda769-b88b-44c9-be5c-52c2667f137e",
|
||||
"sink_id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "8c2bd1f7-b17b-4835-81b6-bb336097aa7a",
|
||||
"source_id": "7e026d19-f9a6-412f-8082-610f9ba0c410",
|
||||
"sink_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_THEME",
|
||||
"is_static": true
|
||||
},
|
||||
{
|
||||
"id": "201d3e03-bc06-4cee-846d-4c3c804d8857",
|
||||
"source_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"sink_id": "576c5677-9050-4d1c-aad4-36b820c04fef",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "714a0821-e5ba-4af7-9432-50491adda7b1",
|
||||
"source_id": "576c5677-9050-4d1c-aad4-36b820c04fef",
|
||||
"sink_id": "5ac3727a-1ea7-436b-a902-ef1bfd883a30",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "54588c74-e090-4e49-89e4-844b9952a585",
|
||||
"source_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"sink_id": "28bda769-b88b-44c9-be5c-52c2667f137e",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "509b7587-1940-4a06-808d-edde9a74f400",
|
||||
"source_id": "7543b9b0-0409-4cf8-bc4e-e0336273e2c4",
|
||||
"sink_id": "86665e90-ffbf-48fb-ad3f-e5d31fd50c51",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"forked_from_id": null,
|
||||
"forked_from_version": null,
|
||||
"sub_graphs": [],
|
||||
"user_id": "",
|
||||
"created_at": "2024-12-20T19:58:34.390Z",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Theme": {
|
||||
"advanced": false,
|
||||
"secret": false,
|
||||
"title": "Theme",
|
||||
"default": "Cooking"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Generated Image": {
|
||||
"advanced": false,
|
||||
"secret": false,
|
||||
"title": "Generated Image",
|
||||
"description": "The resulting generated image ready for you to review and post."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Generated Image"
|
||||
]
|
||||
},
|
||||
"has_external_trigger": false,
|
||||
"has_human_in_the_loop": false,
|
||||
"trigger_setup_info": null,
|
||||
"credentials_input_schema": {
|
||||
"properties": {
|
||||
"ideogram_api_key_credentials": {
|
||||
"credentials_provider": [
|
||||
"ideogram"
|
||||
],
|
||||
"credentials_types": [
|
||||
"api_key"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Title"
|
||||
},
|
||||
"provider": {
|
||||
"const": "ideogram",
|
||||
"title": "Provider",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "api_key",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"provider",
|
||||
"type"
|
||||
],
|
||||
"title": "CredentialsMetaInput[Literal[<ProviderName.IDEOGRAM: 'ideogram'>], Literal['api_key']]",
|
||||
"type": "object",
|
||||
"discriminator_values": []
|
||||
},
|
||||
"openai_api_key_credentials": {
|
||||
"credentials_provider": [
|
||||
"openai"
|
||||
],
|
||||
"credentials_types": [
|
||||
"api_key"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Title"
|
||||
},
|
||||
"provider": {
|
||||
"const": "openai",
|
||||
"title": "Provider",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "api_key",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"provider",
|
||||
"type"
|
||||
],
|
||||
"title": "CredentialsMetaInput[Literal[<ProviderName.OPENAI: 'openai'>], Literal['api_key']]",
|
||||
"type": "object",
|
||||
"discriminator": "model",
|
||||
"discriminator_mapping": {
|
||||
"Llama-3.3-70B-Instruct": "llama_api",
|
||||
"Llama-3.3-8B-Instruct": "llama_api",
|
||||
"Llama-4-Maverick-17B-128E-Instruct-FP8": "llama_api",
|
||||
"Llama-4-Scout-17B-16E-Instruct-FP8": "llama_api",
|
||||
"Qwen/Qwen2.5-72B-Instruct-Turbo": "aiml_api",
|
||||
"amazon/nova-lite-v1": "open_router",
|
||||
"amazon/nova-micro-v1": "open_router",
|
||||
"amazon/nova-pro-v1": "open_router",
|
||||
"claude-3-7-sonnet-20250219": "anthropic",
|
||||
"claude-3-haiku-20240307": "anthropic",
|
||||
"claude-haiku-4-5-20251001": "anthropic",
|
||||
"claude-opus-4-1-20250805": "anthropic",
|
||||
"claude-opus-4-20250514": "anthropic",
|
||||
"claude-opus-4-5-20251101": "anthropic",
|
||||
"claude-sonnet-4-20250514": "anthropic",
|
||||
"claude-sonnet-4-5-20250929": "anthropic",
|
||||
"cohere/command-r-08-2024": "open_router",
|
||||
"cohere/command-r-plus-08-2024": "open_router",
|
||||
"deepseek/deepseek-chat": "open_router",
|
||||
"deepseek/deepseek-r1-0528": "open_router",
|
||||
"dolphin-mistral:latest": "ollama",
|
||||
"google/gemini-2.0-flash-001": "open_router",
|
||||
"google/gemini-2.0-flash-lite-001": "open_router",
|
||||
"google/gemini-2.5-flash": "open_router",
|
||||
"google/gemini-2.5-flash-lite-preview-06-17": "open_router",
|
||||
"google/gemini-2.5-pro-preview-03-25": "open_router",
|
||||
"google/gemini-3-pro-preview": "open_router",
|
||||
"gpt-3.5-turbo": "openai",
|
||||
"gpt-4-turbo": "openai",
|
||||
"gpt-4.1-2025-04-14": "openai",
|
||||
"gpt-4.1-mini-2025-04-14": "openai",
|
||||
"gpt-4o": "openai",
|
||||
"gpt-4o-mini": "openai",
|
||||
"gpt-5-2025-08-07": "openai",
|
||||
"gpt-5-chat-latest": "openai",
|
||||
"gpt-5-mini-2025-08-07": "openai",
|
||||
"gpt-5-nano-2025-08-07": "openai",
|
||||
"gpt-5.1-2025-11-13": "openai",
|
||||
"gryphe/mythomax-l2-13b": "open_router",
|
||||
"llama-3.1-8b-instant": "groq",
|
||||
"llama-3.3-70b-versatile": "groq",
|
||||
"llama3": "ollama",
|
||||
"llama3.1:405b": "ollama",
|
||||
"llama3.2": "ollama",
|
||||
"llama3.3": "ollama",
|
||||
"meta-llama/Llama-3.2-3B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/Llama-3.3-70B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/llama-4-maverick": "open_router",
|
||||
"meta-llama/llama-4-scout": "open_router",
|
||||
"microsoft/wizardlm-2-8x22b": "open_router",
|
||||
"mistralai/mistral-nemo": "open_router",
|
||||
"moonshotai/kimi-k2": "open_router",
|
||||
"nousresearch/hermes-3-llama-3.1-405b": "open_router",
|
||||
"nousresearch/hermes-3-llama-3.1-70b": "open_router",
|
||||
"nvidia/llama-3.1-nemotron-70b-instruct": "aiml_api",
|
||||
"o1": "openai",
|
||||
"o1-mini": "openai",
|
||||
"o3-2025-04-16": "openai",
|
||||
"o3-mini": "openai",
|
||||
"openai/gpt-oss-120b": "open_router",
|
||||
"openai/gpt-oss-20b": "open_router",
|
||||
"perplexity/sonar": "open_router",
|
||||
"perplexity/sonar-deep-research": "open_router",
|
||||
"perplexity/sonar-pro": "open_router",
|
||||
"qwen/qwen3-235b-a22b-thinking-2507": "open_router",
|
||||
"qwen/qwen3-coder": "open_router",
|
||||
"v0-1.0-md": "v0",
|
||||
"v0-1.5-lg": "v0",
|
||||
"v0-1.5-md": "v0",
|
||||
"x-ai/grok-4": "open_router",
|
||||
"x-ai/grok-4-fast": "open_router",
|
||||
"x-ai/grok-4.1-fast": "open_router",
|
||||
"x-ai/grok-code-fast-1": "open_router"
|
||||
},
|
||||
"discriminator_values": [
|
||||
"gpt-4o"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ideogram_api_key_credentials",
|
||||
"openai_api_key_credentials"
|
||||
],
|
||||
"title": "UnspirationalPosterMakerCredentialsInputSchema",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,447 @@
|
||||
{
|
||||
"id": "622849a7-5848-4838-894d-01f8f07e3fad",
|
||||
"version": 18,
|
||||
"is_active": true,
|
||||
"name": "AI Function",
|
||||
"description": "## AI-Powered Function Magic: Never code again!\nProvide a description of a python function and your inputs and AI will provide the results.",
|
||||
"instructions": null,
|
||||
"recommended_schedule_cron": null,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "26ff2973-3f9a-451d-b902-d45e5da0a7fe",
|
||||
"block_id": "363ae599-353e-4804-937e-b2ee3cef3da4",
|
||||
"input_default": {
|
||||
"name": "return",
|
||||
"title": null,
|
||||
"value": null,
|
||||
"format": "",
|
||||
"secret": false,
|
||||
"advanced": false,
|
||||
"description": "The value returned by the function"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 1598.8622921127233,
|
||||
"y": 291.59140862204725
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "caecc1de-fdbc-4fd9-9570-074057bb15f9",
|
||||
"source_id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"sink_id": "26ff2973-3f9a-451d-b902-d45e5da0a7fe",
|
||||
"source_name": "response",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [],
|
||||
"graph_id": "622849a7-5848-4838-894d-01f8f07e3fad",
|
||||
"graph_version": 18,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"block_id": "1f292d4a-41a4-4977-9684-7c8d560b9f91",
|
||||
"input_default": {
|
||||
"model": "o3-mini",
|
||||
"retry": 3,
|
||||
"prompt": "{{ARGS}}",
|
||||
"sys_prompt": "You are now the following python function:\n\n```\n# {{DESCRIPTION}}\n{{FUNCTION}}\n```\n\nThe user will provide your input arguments.\nOnly respond with your `return` value.\nDo not include any commentary or additional text in your response. \nDo not include ``` backticks or any other decorators.",
|
||||
"ollama_host": "localhost:11434",
|
||||
"prompt_values": {}
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 995,
|
||||
"y": 290.50000000000006
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "dc7cb15f-76cc-4533-b96c-dd9e3f7f75ed",
|
||||
"source_id": "4eab3a55-20f2-4c1d-804c-7377ba8202d2",
|
||||
"sink_id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_FUNCTION",
|
||||
"is_static": true
|
||||
},
|
||||
{
|
||||
"id": "093bdca5-9f44-42f9-8e1c-276dd2971675",
|
||||
"source_id": "844530de-2354-46d8-b748-67306b7bbca1",
|
||||
"sink_id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_ARGS",
|
||||
"is_static": true
|
||||
},
|
||||
{
|
||||
"id": "6c63d8ee-b63d-4ff6-bae0-7db8f99bb7af",
|
||||
"source_id": "0fd6ef54-c1cd-478d-b764-17e40f882b99",
|
||||
"sink_id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_DESCRIPTION",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "caecc1de-fdbc-4fd9-9570-074057bb15f9",
|
||||
"source_id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"sink_id": "26ff2973-3f9a-451d-b902-d45e5da0a7fe",
|
||||
"source_name": "response",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "622849a7-5848-4838-894d-01f8f07e3fad",
|
||||
"graph_version": 18,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "4eab3a55-20f2-4c1d-804c-7377ba8202d2",
|
||||
"block_id": "7fcd3bcb-8e1b-4e69-903d-32d3d4a92158",
|
||||
"input_default": {
|
||||
"name": "Function Definition",
|
||||
"title": null,
|
||||
"value": "def fake_people(n: int) -> list[dict]:",
|
||||
"secret": false,
|
||||
"advanced": false,
|
||||
"description": "The function definition (text). This is what you would type on the first line of the function when programming.\n\ne.g \"def fake_people(n: int) -> list[dict]:\"",
|
||||
"placeholder_values": []
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": -672.6908629664215,
|
||||
"y": 302.42044359789116
|
||||
}
|
||||
},
|
||||
"input_links": [],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "dc7cb15f-76cc-4533-b96c-dd9e3f7f75ed",
|
||||
"source_id": "4eab3a55-20f2-4c1d-804c-7377ba8202d2",
|
||||
"sink_id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_FUNCTION",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"graph_id": "622849a7-5848-4838-894d-01f8f07e3fad",
|
||||
"graph_version": 18,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "844530de-2354-46d8-b748-67306b7bbca1",
|
||||
"block_id": "7fcd3bcb-8e1b-4e69-903d-32d3d4a92158",
|
||||
"input_default": {
|
||||
"name": "Arguments",
|
||||
"title": null,
|
||||
"value": "20",
|
||||
"secret": false,
|
||||
"advanced": false,
|
||||
"description": "The function's inputs\n\ne.g \"20\"",
|
||||
"placeholder_values": []
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": -158.1623599617334,
|
||||
"y": 295.410856928333
|
||||
}
|
||||
},
|
||||
"input_links": [],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "093bdca5-9f44-42f9-8e1c-276dd2971675",
|
||||
"source_id": "844530de-2354-46d8-b748-67306b7bbca1",
|
||||
"sink_id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_ARGS",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"graph_id": "622849a7-5848-4838-894d-01f8f07e3fad",
|
||||
"graph_version": 18,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "0fd6ef54-c1cd-478d-b764-17e40f882b99",
|
||||
"block_id": "90a56ffb-7024-4b2b-ab50-e26c5e5ab8ba",
|
||||
"input_default": {
|
||||
"name": "Description",
|
||||
"title": null,
|
||||
"value": "Generates n examples of fake data representing people, each with a name, DoB, Job title, and an age.",
|
||||
"secret": false,
|
||||
"advanced": false,
|
||||
"description": "Describe what the function does.\n\ne.g \"Generates n examples of fake data representing people, each with a name, DoB, Job title, and an age.\"",
|
||||
"placeholder_values": []
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 374.4548658057796,
|
||||
"y": 290.3779121974126
|
||||
}
|
||||
},
|
||||
"input_links": [],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "6c63d8ee-b63d-4ff6-bae0-7db8f99bb7af",
|
||||
"source_id": "0fd6ef54-c1cd-478d-b764-17e40f882b99",
|
||||
"sink_id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_DESCRIPTION",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"graph_id": "622849a7-5848-4838-894d-01f8f07e3fad",
|
||||
"graph_version": 18,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "caecc1de-fdbc-4fd9-9570-074057bb15f9",
|
||||
"source_id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"sink_id": "26ff2973-3f9a-451d-b902-d45e5da0a7fe",
|
||||
"source_name": "response",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "6c63d8ee-b63d-4ff6-bae0-7db8f99bb7af",
|
||||
"source_id": "0fd6ef54-c1cd-478d-b764-17e40f882b99",
|
||||
"sink_id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_DESCRIPTION",
|
||||
"is_static": true
|
||||
},
|
||||
{
|
||||
"id": "093bdca5-9f44-42f9-8e1c-276dd2971675",
|
||||
"source_id": "844530de-2354-46d8-b748-67306b7bbca1",
|
||||
"sink_id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_ARGS",
|
||||
"is_static": true
|
||||
},
|
||||
{
|
||||
"id": "dc7cb15f-76cc-4533-b96c-dd9e3f7f75ed",
|
||||
"source_id": "4eab3a55-20f2-4c1d-804c-7377ba8202d2",
|
||||
"sink_id": "c5d16ee4-de9e-4d93-bf32-ac2d15760d5b",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_FUNCTION",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"forked_from_id": null,
|
||||
"forked_from_version": null,
|
||||
"sub_graphs": [],
|
||||
"user_id": "",
|
||||
"created_at": "2025-04-19T17:10:48.857Z",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Function Definition": {
|
||||
"advanced": false,
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "short-text",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"secret": false,
|
||||
"title": "Function Definition",
|
||||
"description": "The function definition (text). This is what you would type on the first line of the function when programming.\n\ne.g \"def fake_people(n: int) -> list[dict]:\"",
|
||||
"default": "def fake_people(n: int) -> list[dict]:"
|
||||
},
|
||||
"Arguments": {
|
||||
"advanced": false,
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "short-text",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"secret": false,
|
||||
"title": "Arguments",
|
||||
"description": "The function's inputs\n\ne.g \"20\"",
|
||||
"default": "20"
|
||||
},
|
||||
"Description": {
|
||||
"advanced": false,
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "long-text",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"secret": false,
|
||||
"title": "Description",
|
||||
"description": "Describe what the function does.\n\ne.g \"Generates n examples of fake data representing people, each with a name, DoB, Job title, and an age.\"",
|
||||
"default": "Generates n examples of fake data representing people, each with a name, DoB, Job title, and an age."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"return": {
|
||||
"advanced": false,
|
||||
"secret": false,
|
||||
"title": "return",
|
||||
"description": "The value returned by the function"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"return"
|
||||
]
|
||||
},
|
||||
"has_external_trigger": false,
|
||||
"has_human_in_the_loop": false,
|
||||
"trigger_setup_info": null,
|
||||
"credentials_input_schema": {
|
||||
"properties": {
|
||||
"openai_api_key_credentials": {
|
||||
"credentials_provider": [
|
||||
"openai"
|
||||
],
|
||||
"credentials_types": [
|
||||
"api_key"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Title"
|
||||
},
|
||||
"provider": {
|
||||
"const": "openai",
|
||||
"title": "Provider",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "api_key",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"provider",
|
||||
"type"
|
||||
],
|
||||
"title": "CredentialsMetaInput[Literal[<ProviderName.OPENAI: 'openai'>], Literal['api_key']]",
|
||||
"type": "object",
|
||||
"discriminator": "model",
|
||||
"discriminator_mapping": {
|
||||
"Llama-3.3-70B-Instruct": "llama_api",
|
||||
"Llama-3.3-8B-Instruct": "llama_api",
|
||||
"Llama-4-Maverick-17B-128E-Instruct-FP8": "llama_api",
|
||||
"Llama-4-Scout-17B-16E-Instruct-FP8": "llama_api",
|
||||
"Qwen/Qwen2.5-72B-Instruct-Turbo": "aiml_api",
|
||||
"amazon/nova-lite-v1": "open_router",
|
||||
"amazon/nova-micro-v1": "open_router",
|
||||
"amazon/nova-pro-v1": "open_router",
|
||||
"claude-3-7-sonnet-20250219": "anthropic",
|
||||
"claude-3-haiku-20240307": "anthropic",
|
||||
"claude-haiku-4-5-20251001": "anthropic",
|
||||
"claude-opus-4-1-20250805": "anthropic",
|
||||
"claude-opus-4-20250514": "anthropic",
|
||||
"claude-opus-4-5-20251101": "anthropic",
|
||||
"claude-sonnet-4-20250514": "anthropic",
|
||||
"claude-sonnet-4-5-20250929": "anthropic",
|
||||
"cohere/command-r-08-2024": "open_router",
|
||||
"cohere/command-r-plus-08-2024": "open_router",
|
||||
"deepseek/deepseek-chat": "open_router",
|
||||
"deepseek/deepseek-r1-0528": "open_router",
|
||||
"dolphin-mistral:latest": "ollama",
|
||||
"google/gemini-2.0-flash-001": "open_router",
|
||||
"google/gemini-2.0-flash-lite-001": "open_router",
|
||||
"google/gemini-2.5-flash": "open_router",
|
||||
"google/gemini-2.5-flash-lite-preview-06-17": "open_router",
|
||||
"google/gemini-2.5-pro-preview-03-25": "open_router",
|
||||
"google/gemini-3-pro-preview": "open_router",
|
||||
"gpt-3.5-turbo": "openai",
|
||||
"gpt-4-turbo": "openai",
|
||||
"gpt-4.1-2025-04-14": "openai",
|
||||
"gpt-4.1-mini-2025-04-14": "openai",
|
||||
"gpt-4o": "openai",
|
||||
"gpt-4o-mini": "openai",
|
||||
"gpt-5-2025-08-07": "openai",
|
||||
"gpt-5-chat-latest": "openai",
|
||||
"gpt-5-mini-2025-08-07": "openai",
|
||||
"gpt-5-nano-2025-08-07": "openai",
|
||||
"gpt-5.1-2025-11-13": "openai",
|
||||
"gryphe/mythomax-l2-13b": "open_router",
|
||||
"llama-3.1-8b-instant": "groq",
|
||||
"llama-3.3-70b-versatile": "groq",
|
||||
"llama3": "ollama",
|
||||
"llama3.1:405b": "ollama",
|
||||
"llama3.2": "ollama",
|
||||
"llama3.3": "ollama",
|
||||
"meta-llama/Llama-3.2-3B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/Llama-3.3-70B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/llama-4-maverick": "open_router",
|
||||
"meta-llama/llama-4-scout": "open_router",
|
||||
"microsoft/wizardlm-2-8x22b": "open_router",
|
||||
"mistralai/mistral-nemo": "open_router",
|
||||
"moonshotai/kimi-k2": "open_router",
|
||||
"nousresearch/hermes-3-llama-3.1-405b": "open_router",
|
||||
"nousresearch/hermes-3-llama-3.1-70b": "open_router",
|
||||
"nvidia/llama-3.1-nemotron-70b-instruct": "aiml_api",
|
||||
"o1": "openai",
|
||||
"o1-mini": "openai",
|
||||
"o3-2025-04-16": "openai",
|
||||
"o3-mini": "openai",
|
||||
"openai/gpt-oss-120b": "open_router",
|
||||
"openai/gpt-oss-20b": "open_router",
|
||||
"perplexity/sonar": "open_router",
|
||||
"perplexity/sonar-deep-research": "open_router",
|
||||
"perplexity/sonar-pro": "open_router",
|
||||
"qwen/qwen3-235b-a22b-thinking-2507": "open_router",
|
||||
"qwen/qwen3-coder": "open_router",
|
||||
"v0-1.0-md": "v0",
|
||||
"v0-1.5-lg": "v0",
|
||||
"v0-1.5-md": "v0",
|
||||
"x-ai/grok-4": "open_router",
|
||||
"x-ai/grok-4-fast": "open_router",
|
||||
"x-ai/grok-4.1-fast": "open_router",
|
||||
"x-ai/grok-code-fast-1": "open_router"
|
||||
},
|
||||
"discriminator_values": [
|
||||
"o3-mini"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"openai_api_key_credentials"
|
||||
],
|
||||
"title": "AIFunctionCredentialsInputSchema",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,403 @@
|
||||
{
|
||||
"id": "ed2091cf-5b27-45a9-b3ea-42396f95b256",
|
||||
"version": 12,
|
||||
"is_active": true,
|
||||
"name": "Flux AI Image Generator",
|
||||
"description": "Transform ideas into breathtaking images with this AI-powered Image Generator. Using cutting-edge Flux AI technology, the tool crafts highly detailed, photorealistic visuals from simple text prompts. Perfect for artists, marketers, and content creators, this generator produces unique images tailored to user specifications. From fantastical scenes to lifelike portraits, users can unleash creativity with professional-quality results in seconds. Easy to use and endlessly versatile, bring imagination to life with the AI Image Generator today!",
|
||||
"instructions": null,
|
||||
"recommended_schedule_cron": null,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "7482c59d-725f-4686-82b9-0dfdc4e92316",
|
||||
"block_id": "cc10ff7b-7753-4ff2-9af6-9399b1a7eddc",
|
||||
"input_default": {
|
||||
"text": "Press the \"Advanced\" toggle and input your replicate API key.\n\nYou can get one here:\nhttps://replicate.com/account/api-tokens\n"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 872.8268131538296,
|
||||
"y": 614.9436919065381
|
||||
}
|
||||
},
|
||||
"input_links": [],
|
||||
"output_links": [],
|
||||
"graph_id": "ed2091cf-5b27-45a9-b3ea-42396f95b256",
|
||||
"graph_version": 12,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "0d1dec1a-e4ee-4349-9673-449a01bbf14e",
|
||||
"block_id": "363ae599-353e-4804-937e-b2ee3cef3da4",
|
||||
"input_default": {
|
||||
"name": "Generated Image"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 1453.6844137728922,
|
||||
"y": 963.2466395125115
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "06665d23-2f3d-4445-8f22-573446fcff5b",
|
||||
"source_id": "50bc23e9-f2b7-4959-8710-99679ed9eeea",
|
||||
"sink_id": "0d1dec1a-e4ee-4349-9673-449a01bbf14e",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [],
|
||||
"graph_id": "ed2091cf-5b27-45a9-b3ea-42396f95b256",
|
||||
"graph_version": 12,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "6f24c45f-1548-4eda-9784-da06ce0abef8",
|
||||
"block_id": "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b",
|
||||
"input_default": {
|
||||
"name": "Image Subject",
|
||||
"value": "Otto the friendly, purple \"Chief Automation Octopus\" helping people automate their tedious tasks.",
|
||||
"description": "The subject of the image"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": -314.43009631839783,
|
||||
"y": 962.935949165938
|
||||
}
|
||||
},
|
||||
"input_links": [],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "1077c61a-a32a-4ed7-becf-11bcf835b914",
|
||||
"source_id": "6f24c45f-1548-4eda-9784-da06ce0abef8",
|
||||
"sink_id": "0d1bca9a-d9b8-4bfd-a19c-fe50b54f4b12",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_TOPIC",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"graph_id": "ed2091cf-5b27-45a9-b3ea-42396f95b256",
|
||||
"graph_version": 12,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "50bc23e9-f2b7-4959-8710-99679ed9eeea",
|
||||
"block_id": "90f8c45e-e983-4644-aa0b-b4ebe2f531bc",
|
||||
"input_default": {
|
||||
"prompt": "dog",
|
||||
"output_format": "png",
|
||||
"replicate_model_name": "Flux Pro 1.1"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 873.0119949791526,
|
||||
"y": 966.1604399052493
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "a17ec505-9377-4700-8fe0-124ca81d43a9",
|
||||
"source_id": "0d1bca9a-d9b8-4bfd-a19c-fe50b54f4b12",
|
||||
"sink_id": "50bc23e9-f2b7-4959-8710-99679ed9eeea",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "06665d23-2f3d-4445-8f22-573446fcff5b",
|
||||
"source_id": "50bc23e9-f2b7-4959-8710-99679ed9eeea",
|
||||
"sink_id": "0d1dec1a-e4ee-4349-9673-449a01bbf14e",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "ed2091cf-5b27-45a9-b3ea-42396f95b256",
|
||||
"graph_version": 12,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "0d1bca9a-d9b8-4bfd-a19c-fe50b54f4b12",
|
||||
"block_id": "1f292d4a-41a4-4977-9684-7c8d560b9f91",
|
||||
"input_default": {
|
||||
"model": "gpt-4o-mini",
|
||||
"prompt": "Generate an incredibly detailed, photorealistic image prompt about {{TOPIC}}, describing the camera it's taken with and prompting the diffusion model to use all the best quality techniques.\n\nOutput only the prompt with no additional commentary.",
|
||||
"prompt_values": {}
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 277.3057034159709,
|
||||
"y": 962.8382498113764
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "1077c61a-a32a-4ed7-becf-11bcf835b914",
|
||||
"source_id": "6f24c45f-1548-4eda-9784-da06ce0abef8",
|
||||
"sink_id": "0d1bca9a-d9b8-4bfd-a19c-fe50b54f4b12",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_TOPIC",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "a17ec505-9377-4700-8fe0-124ca81d43a9",
|
||||
"source_id": "0d1bca9a-d9b8-4bfd-a19c-fe50b54f4b12",
|
||||
"sink_id": "50bc23e9-f2b7-4959-8710-99679ed9eeea",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "ed2091cf-5b27-45a9-b3ea-42396f95b256",
|
||||
"graph_version": 12,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "1077c61a-a32a-4ed7-becf-11bcf835b914",
|
||||
"source_id": "6f24c45f-1548-4eda-9784-da06ce0abef8",
|
||||
"sink_id": "0d1bca9a-d9b8-4bfd-a19c-fe50b54f4b12",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_TOPIC",
|
||||
"is_static": true
|
||||
},
|
||||
{
|
||||
"id": "06665d23-2f3d-4445-8f22-573446fcff5b",
|
||||
"source_id": "50bc23e9-f2b7-4959-8710-99679ed9eeea",
|
||||
"sink_id": "0d1dec1a-e4ee-4349-9673-449a01bbf14e",
|
||||
"source_name": "result",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "a17ec505-9377-4700-8fe0-124ca81d43a9",
|
||||
"source_id": "0d1bca9a-d9b8-4bfd-a19c-fe50b54f4b12",
|
||||
"sink_id": "50bc23e9-f2b7-4959-8710-99679ed9eeea",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"forked_from_id": null,
|
||||
"forked_from_version": null,
|
||||
"sub_graphs": [],
|
||||
"user_id": "",
|
||||
"created_at": "2024-12-20T18:46:11.492Z",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Image Subject": {
|
||||
"advanced": false,
|
||||
"secret": false,
|
||||
"title": "Image Subject",
|
||||
"description": "The subject of the image",
|
||||
"default": "Otto the friendly, purple \"Chief Automation Octopus\" helping people automate their tedious tasks."
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Generated Image": {
|
||||
"advanced": false,
|
||||
"secret": false,
|
||||
"title": "Generated Image"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Generated Image"
|
||||
]
|
||||
},
|
||||
"has_external_trigger": false,
|
||||
"has_human_in_the_loop": false,
|
||||
"trigger_setup_info": null,
|
||||
"credentials_input_schema": {
|
||||
"properties": {
|
||||
"replicate_api_key_credentials": {
|
||||
"credentials_provider": [
|
||||
"replicate"
|
||||
],
|
||||
"credentials_types": [
|
||||
"api_key"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Title"
|
||||
},
|
||||
"provider": {
|
||||
"const": "replicate",
|
||||
"title": "Provider",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "api_key",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"provider",
|
||||
"type"
|
||||
],
|
||||
"title": "CredentialsMetaInput[Literal[<ProviderName.REPLICATE: 'replicate'>], Literal['api_key']]",
|
||||
"type": "object",
|
||||
"discriminator_values": []
|
||||
},
|
||||
"openai_api_key_credentials": {
|
||||
"credentials_provider": [
|
||||
"openai"
|
||||
],
|
||||
"credentials_types": [
|
||||
"api_key"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Title"
|
||||
},
|
||||
"provider": {
|
||||
"const": "openai",
|
||||
"title": "Provider",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "api_key",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"provider",
|
||||
"type"
|
||||
],
|
||||
"title": "CredentialsMetaInput[Literal[<ProviderName.OPENAI: 'openai'>], Literal['api_key']]",
|
||||
"type": "object",
|
||||
"discriminator": "model",
|
||||
"discriminator_mapping": {
|
||||
"Llama-3.3-70B-Instruct": "llama_api",
|
||||
"Llama-3.3-8B-Instruct": "llama_api",
|
||||
"Llama-4-Maverick-17B-128E-Instruct-FP8": "llama_api",
|
||||
"Llama-4-Scout-17B-16E-Instruct-FP8": "llama_api",
|
||||
"Qwen/Qwen2.5-72B-Instruct-Turbo": "aiml_api",
|
||||
"amazon/nova-lite-v1": "open_router",
|
||||
"amazon/nova-micro-v1": "open_router",
|
||||
"amazon/nova-pro-v1": "open_router",
|
||||
"claude-3-7-sonnet-20250219": "anthropic",
|
||||
"claude-3-haiku-20240307": "anthropic",
|
||||
"claude-haiku-4-5-20251001": "anthropic",
|
||||
"claude-opus-4-1-20250805": "anthropic",
|
||||
"claude-opus-4-20250514": "anthropic",
|
||||
"claude-opus-4-5-20251101": "anthropic",
|
||||
"claude-sonnet-4-20250514": "anthropic",
|
||||
"claude-sonnet-4-5-20250929": "anthropic",
|
||||
"cohere/command-r-08-2024": "open_router",
|
||||
"cohere/command-r-plus-08-2024": "open_router",
|
||||
"deepseek/deepseek-chat": "open_router",
|
||||
"deepseek/deepseek-r1-0528": "open_router",
|
||||
"dolphin-mistral:latest": "ollama",
|
||||
"google/gemini-2.0-flash-001": "open_router",
|
||||
"google/gemini-2.0-flash-lite-001": "open_router",
|
||||
"google/gemini-2.5-flash": "open_router",
|
||||
"google/gemini-2.5-flash-lite-preview-06-17": "open_router",
|
||||
"google/gemini-2.5-pro-preview-03-25": "open_router",
|
||||
"google/gemini-3-pro-preview": "open_router",
|
||||
"gpt-3.5-turbo": "openai",
|
||||
"gpt-4-turbo": "openai",
|
||||
"gpt-4.1-2025-04-14": "openai",
|
||||
"gpt-4.1-mini-2025-04-14": "openai",
|
||||
"gpt-4o": "openai",
|
||||
"gpt-4o-mini": "openai",
|
||||
"gpt-5-2025-08-07": "openai",
|
||||
"gpt-5-chat-latest": "openai",
|
||||
"gpt-5-mini-2025-08-07": "openai",
|
||||
"gpt-5-nano-2025-08-07": "openai",
|
||||
"gpt-5.1-2025-11-13": "openai",
|
||||
"gryphe/mythomax-l2-13b": "open_router",
|
||||
"llama-3.1-8b-instant": "groq",
|
||||
"llama-3.3-70b-versatile": "groq",
|
||||
"llama3": "ollama",
|
||||
"llama3.1:405b": "ollama",
|
||||
"llama3.2": "ollama",
|
||||
"llama3.3": "ollama",
|
||||
"meta-llama/Llama-3.2-3B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/Llama-3.3-70B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/llama-4-maverick": "open_router",
|
||||
"meta-llama/llama-4-scout": "open_router",
|
||||
"microsoft/wizardlm-2-8x22b": "open_router",
|
||||
"mistralai/mistral-nemo": "open_router",
|
||||
"moonshotai/kimi-k2": "open_router",
|
||||
"nousresearch/hermes-3-llama-3.1-405b": "open_router",
|
||||
"nousresearch/hermes-3-llama-3.1-70b": "open_router",
|
||||
"nvidia/llama-3.1-nemotron-70b-instruct": "aiml_api",
|
||||
"o1": "openai",
|
||||
"o1-mini": "openai",
|
||||
"o3-2025-04-16": "openai",
|
||||
"o3-mini": "openai",
|
||||
"openai/gpt-oss-120b": "open_router",
|
||||
"openai/gpt-oss-20b": "open_router",
|
||||
"perplexity/sonar": "open_router",
|
||||
"perplexity/sonar-deep-research": "open_router",
|
||||
"perplexity/sonar-pro": "open_router",
|
||||
"qwen/qwen3-235b-a22b-thinking-2507": "open_router",
|
||||
"qwen/qwen3-coder": "open_router",
|
||||
"v0-1.0-md": "v0",
|
||||
"v0-1.5-lg": "v0",
|
||||
"v0-1.5-md": "v0",
|
||||
"x-ai/grok-4": "open_router",
|
||||
"x-ai/grok-4-fast": "open_router",
|
||||
"x-ai/grok-4.1-fast": "open_router",
|
||||
"x-ai/grok-code-fast-1": "open_router"
|
||||
},
|
||||
"discriminator_values": [
|
||||
"gpt-4o-mini"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"replicate_api_key_credentials",
|
||||
"openai_api_key_credentials"
|
||||
],
|
||||
"title": "FluxAIImageGeneratorCredentialsInputSchema",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,505 @@
|
||||
{
|
||||
"id": "0d440799-44ba-4d6c-85b3-b3739f1e1287",
|
||||
"version": 12,
|
||||
"is_active": true,
|
||||
"name": "AI Webpage Copy Improver",
|
||||
"description": "Elevate your web content with this powerful AI Webpage Copy Improver. Designed for marketers, SEO specialists, and web developers, this tool analyses and enhances website copy for maximum impact. Using advanced language models, it optimizes text for better clarity, SEO performance, and increased conversion rates. The AI examines your existing content, identifies areas for improvement, and generates refined copy that maintains your brand voice while boosting engagement. From homepage headlines to product descriptions, transform your web presence with AI-driven insights. Improve readability, incorporate targeted keywords, and craft compelling calls-to-action - all with the click of a button. Take your digital marketing to the next level with the AI Webpage Copy Improver.",
|
||||
"instructions": null,
|
||||
"recommended_schedule_cron": null,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "130ec496-f75d-4fe2-9cd6-8c00d08ea4a7",
|
||||
"block_id": "363ae599-353e-4804-937e-b2ee3cef3da4",
|
||||
"input_default": {
|
||||
"name": "Improved Webpage Copy"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 1039.5884372540172,
|
||||
"y": -0.8359099621230968
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "d4334477-3616-454f-a430-614ca27f5b36",
|
||||
"source_id": "c9924577-70d8-4ccb-9106-6f796df09ef9",
|
||||
"sink_id": "130ec496-f75d-4fe2-9cd6-8c00d08ea4a7",
|
||||
"source_name": "response",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [],
|
||||
"graph_id": "0d440799-44ba-4d6c-85b3-b3739f1e1287",
|
||||
"graph_version": 12,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "cefccd07-fe70-4feb-bf76-46b20aaa5d35",
|
||||
"block_id": "363ae599-353e-4804-937e-b2ee3cef3da4",
|
||||
"input_default": {
|
||||
"name": "Original Page Analysis",
|
||||
"description": "Analysis of the webpage as it currently stands."
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 1037.7724103954706,
|
||||
"y": -606.5934325506903
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "f979ab78-0903-4f19-a7c2-a419d5d81aef",
|
||||
"source_id": "08612ce2-625b-4c17-accd-3acace7b6477",
|
||||
"sink_id": "cefccd07-fe70-4feb-bf76-46b20aaa5d35",
|
||||
"source_name": "response",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [],
|
||||
"graph_id": "0d440799-44ba-4d6c-85b3-b3739f1e1287",
|
||||
"graph_version": 12,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "375f8bc3-afd9-4025-ad8e-9aeb329af7ce",
|
||||
"block_id": "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b",
|
||||
"input_default": {
|
||||
"name": "Homepage URL",
|
||||
"value": "https://agpt.co",
|
||||
"description": "Enter the URL of the homepage you want to improve"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": -1195.1455674454749,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
"input_links": [],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "cbb12335-fefd-4560-9fff-98675130fbad",
|
||||
"source_id": "375f8bc3-afd9-4025-ad8e-9aeb329af7ce",
|
||||
"sink_id": "b40595c6-dba3-4779-a129-cd4f01fff103",
|
||||
"source_name": "result",
|
||||
"sink_name": "url",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"graph_id": "0d440799-44ba-4d6c-85b3-b3739f1e1287",
|
||||
"graph_version": 12,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "b40595c6-dba3-4779-a129-cd4f01fff103",
|
||||
"block_id": "436c3984-57fd-4b85-8e9a-459b356883bd",
|
||||
"input_default": {
|
||||
"raw_content": false
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": -631.7330786555249,
|
||||
"y": 1.9638396496230826
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "cbb12335-fefd-4560-9fff-98675130fbad",
|
||||
"source_id": "375f8bc3-afd9-4025-ad8e-9aeb329af7ce",
|
||||
"sink_id": "b40595c6-dba3-4779-a129-cd4f01fff103",
|
||||
"source_name": "result",
|
||||
"sink_name": "url",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "adfa6113-77b3-4e32-b136-3e694b87553e",
|
||||
"source_id": "b40595c6-dba3-4779-a129-cd4f01fff103",
|
||||
"sink_id": "c9924577-70d8-4ccb-9106-6f796df09ef9",
|
||||
"source_name": "content",
|
||||
"sink_name": "prompt_values_#_CONTENT",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "5d5656fd-4208-4296-bc70-e39cc31caada",
|
||||
"source_id": "b40595c6-dba3-4779-a129-cd4f01fff103",
|
||||
"sink_id": "08612ce2-625b-4c17-accd-3acace7b6477",
|
||||
"source_name": "content",
|
||||
"sink_name": "prompt_values_#_CONTENT",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "0d440799-44ba-4d6c-85b3-b3739f1e1287",
|
||||
"graph_version": 12,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "c9924577-70d8-4ccb-9106-6f796df09ef9",
|
||||
"block_id": "1f292d4a-41a4-4977-9684-7c8d560b9f91",
|
||||
"input_default": {
|
||||
"model": "gpt-4o",
|
||||
"prompt": "Current Webpage Content:\n```\n{{CONTENT}}\n```\n\nBased on the following analysis of the webpage content:\n\n```\n{{ANALYSIS}}\n```\n\nRewrite and improve the content to address the identified issues. Focus on:\n1. Enhancing clarity and readability\n2. Optimizing for SEO (suggest and incorporate relevant keywords)\n3. Improving calls-to-action for better conversion rates\n4. Refining the structure and organization\n5. Maintaining brand consistency while improving the overall tone\n\nProvide the improved content in HTML format inside a code-block with \"```\" backticks, preserving the original structure where appropriate. Also, include a brief summary of the changes made and their potential impact.",
|
||||
"prompt_values": {}
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 488.37278423303917,
|
||||
"y": 0
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "adfa6113-77b3-4e32-b136-3e694b87553e",
|
||||
"source_id": "b40595c6-dba3-4779-a129-cd4f01fff103",
|
||||
"sink_id": "c9924577-70d8-4ccb-9106-6f796df09ef9",
|
||||
"source_name": "content",
|
||||
"sink_name": "prompt_values_#_CONTENT",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "6bcca45d-c9d5-439e-ac43-e4a1264d8f57",
|
||||
"source_id": "08612ce2-625b-4c17-accd-3acace7b6477",
|
||||
"sink_id": "c9924577-70d8-4ccb-9106-6f796df09ef9",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt_values_#_ANALYSIS",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "d4334477-3616-454f-a430-614ca27f5b36",
|
||||
"source_id": "c9924577-70d8-4ccb-9106-6f796df09ef9",
|
||||
"sink_id": "130ec496-f75d-4fe2-9cd6-8c00d08ea4a7",
|
||||
"source_name": "response",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "0d440799-44ba-4d6c-85b3-b3739f1e1287",
|
||||
"graph_version": 12,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "08612ce2-625b-4c17-accd-3acace7b6477",
|
||||
"block_id": "1f292d4a-41a4-4977-9684-7c8d560b9f91",
|
||||
"input_default": {
|
||||
"model": "gpt-4o",
|
||||
"prompt": "Analyze the following webpage content and provide a detailed report on its current state, including strengths and weaknesses in terms of clarity, SEO optimization, and potential for conversion:\n\n{{CONTENT}}\n\nInclude observations on:\n1. Overall readability and clarity\n2. Use of keywords and SEO-friendly language\n3. Effectiveness of calls-to-action\n4. Structure and organization of content\n5. Tone and brand consistency",
|
||||
"prompt_values": {}
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": -72.66206703605442,
|
||||
"y": -0.58403945075381
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "5d5656fd-4208-4296-bc70-e39cc31caada",
|
||||
"source_id": "b40595c6-dba3-4779-a129-cd4f01fff103",
|
||||
"sink_id": "08612ce2-625b-4c17-accd-3acace7b6477",
|
||||
"source_name": "content",
|
||||
"sink_name": "prompt_values_#_CONTENT",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "f979ab78-0903-4f19-a7c2-a419d5d81aef",
|
||||
"source_id": "08612ce2-625b-4c17-accd-3acace7b6477",
|
||||
"sink_id": "cefccd07-fe70-4feb-bf76-46b20aaa5d35",
|
||||
"source_name": "response",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "6bcca45d-c9d5-439e-ac43-e4a1264d8f57",
|
||||
"source_id": "08612ce2-625b-4c17-accd-3acace7b6477",
|
||||
"sink_id": "c9924577-70d8-4ccb-9106-6f796df09ef9",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt_values_#_ANALYSIS",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "0d440799-44ba-4d6c-85b3-b3739f1e1287",
|
||||
"graph_version": 12,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "adfa6113-77b3-4e32-b136-3e694b87553e",
|
||||
"source_id": "b40595c6-dba3-4779-a129-cd4f01fff103",
|
||||
"sink_id": "c9924577-70d8-4ccb-9106-6f796df09ef9",
|
||||
"source_name": "content",
|
||||
"sink_name": "prompt_values_#_CONTENT",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "d4334477-3616-454f-a430-614ca27f5b36",
|
||||
"source_id": "c9924577-70d8-4ccb-9106-6f796df09ef9",
|
||||
"sink_id": "130ec496-f75d-4fe2-9cd6-8c00d08ea4a7",
|
||||
"source_name": "response",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "5d5656fd-4208-4296-bc70-e39cc31caada",
|
||||
"source_id": "b40595c6-dba3-4779-a129-cd4f01fff103",
|
||||
"sink_id": "08612ce2-625b-4c17-accd-3acace7b6477",
|
||||
"source_name": "content",
|
||||
"sink_name": "prompt_values_#_CONTENT",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "f979ab78-0903-4f19-a7c2-a419d5d81aef",
|
||||
"source_id": "08612ce2-625b-4c17-accd-3acace7b6477",
|
||||
"sink_id": "cefccd07-fe70-4feb-bf76-46b20aaa5d35",
|
||||
"source_name": "response",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "6bcca45d-c9d5-439e-ac43-e4a1264d8f57",
|
||||
"source_id": "08612ce2-625b-4c17-accd-3acace7b6477",
|
||||
"sink_id": "c9924577-70d8-4ccb-9106-6f796df09ef9",
|
||||
"source_name": "response",
|
||||
"sink_name": "prompt_values_#_ANALYSIS",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "cbb12335-fefd-4560-9fff-98675130fbad",
|
||||
"source_id": "375f8bc3-afd9-4025-ad8e-9aeb329af7ce",
|
||||
"sink_id": "b40595c6-dba3-4779-a129-cd4f01fff103",
|
||||
"source_name": "result",
|
||||
"sink_name": "url",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"forked_from_id": null,
|
||||
"forked_from_version": null,
|
||||
"sub_graphs": [],
|
||||
"user_id": "",
|
||||
"created_at": "2024-12-20T19:47:22.036Z",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Homepage URL": {
|
||||
"advanced": false,
|
||||
"secret": false,
|
||||
"title": "Homepage URL",
|
||||
"description": "Enter the URL of the homepage you want to improve",
|
||||
"default": "https://agpt.co"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Improved Webpage Copy": {
|
||||
"advanced": false,
|
||||
"secret": false,
|
||||
"title": "Improved Webpage Copy"
|
||||
},
|
||||
"Original Page Analysis": {
|
||||
"advanced": false,
|
||||
"secret": false,
|
||||
"title": "Original Page Analysis",
|
||||
"description": "Analysis of the webpage as it currently stands."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Improved Webpage Copy",
|
||||
"Original Page Analysis"
|
||||
]
|
||||
},
|
||||
"has_external_trigger": false,
|
||||
"has_human_in_the_loop": false,
|
||||
"trigger_setup_info": null,
|
||||
"credentials_input_schema": {
|
||||
"properties": {
|
||||
"jina_api_key_credentials": {
|
||||
"credentials_provider": [
|
||||
"jina"
|
||||
],
|
||||
"credentials_types": [
|
||||
"api_key"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Title"
|
||||
},
|
||||
"provider": {
|
||||
"const": "jina",
|
||||
"title": "Provider",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "api_key",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"provider",
|
||||
"type"
|
||||
],
|
||||
"title": "CredentialsMetaInput[Literal[<ProviderName.JINA: 'jina'>], Literal['api_key']]",
|
||||
"type": "object",
|
||||
"discriminator_values": []
|
||||
},
|
||||
"openai_api_key_credentials": {
|
||||
"credentials_provider": [
|
||||
"openai"
|
||||
],
|
||||
"credentials_types": [
|
||||
"api_key"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Title"
|
||||
},
|
||||
"provider": {
|
||||
"const": "openai",
|
||||
"title": "Provider",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "api_key",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"provider",
|
||||
"type"
|
||||
],
|
||||
"title": "CredentialsMetaInput[Literal[<ProviderName.OPENAI: 'openai'>], Literal['api_key']]",
|
||||
"type": "object",
|
||||
"discriminator": "model",
|
||||
"discriminator_mapping": {
|
||||
"Llama-3.3-70B-Instruct": "llama_api",
|
||||
"Llama-3.3-8B-Instruct": "llama_api",
|
||||
"Llama-4-Maverick-17B-128E-Instruct-FP8": "llama_api",
|
||||
"Llama-4-Scout-17B-16E-Instruct-FP8": "llama_api",
|
||||
"Qwen/Qwen2.5-72B-Instruct-Turbo": "aiml_api",
|
||||
"amazon/nova-lite-v1": "open_router",
|
||||
"amazon/nova-micro-v1": "open_router",
|
||||
"amazon/nova-pro-v1": "open_router",
|
||||
"claude-3-7-sonnet-20250219": "anthropic",
|
||||
"claude-3-haiku-20240307": "anthropic",
|
||||
"claude-haiku-4-5-20251001": "anthropic",
|
||||
"claude-opus-4-1-20250805": "anthropic",
|
||||
"claude-opus-4-20250514": "anthropic",
|
||||
"claude-opus-4-5-20251101": "anthropic",
|
||||
"claude-sonnet-4-20250514": "anthropic",
|
||||
"claude-sonnet-4-5-20250929": "anthropic",
|
||||
"cohere/command-r-08-2024": "open_router",
|
||||
"cohere/command-r-plus-08-2024": "open_router",
|
||||
"deepseek/deepseek-chat": "open_router",
|
||||
"deepseek/deepseek-r1-0528": "open_router",
|
||||
"dolphin-mistral:latest": "ollama",
|
||||
"google/gemini-2.0-flash-001": "open_router",
|
||||
"google/gemini-2.0-flash-lite-001": "open_router",
|
||||
"google/gemini-2.5-flash": "open_router",
|
||||
"google/gemini-2.5-flash-lite-preview-06-17": "open_router",
|
||||
"google/gemini-2.5-pro-preview-03-25": "open_router",
|
||||
"google/gemini-3-pro-preview": "open_router",
|
||||
"gpt-3.5-turbo": "openai",
|
||||
"gpt-4-turbo": "openai",
|
||||
"gpt-4.1-2025-04-14": "openai",
|
||||
"gpt-4.1-mini-2025-04-14": "openai",
|
||||
"gpt-4o": "openai",
|
||||
"gpt-4o-mini": "openai",
|
||||
"gpt-5-2025-08-07": "openai",
|
||||
"gpt-5-chat-latest": "openai",
|
||||
"gpt-5-mini-2025-08-07": "openai",
|
||||
"gpt-5-nano-2025-08-07": "openai",
|
||||
"gpt-5.1-2025-11-13": "openai",
|
||||
"gryphe/mythomax-l2-13b": "open_router",
|
||||
"llama-3.1-8b-instant": "groq",
|
||||
"llama-3.3-70b-versatile": "groq",
|
||||
"llama3": "ollama",
|
||||
"llama3.1:405b": "ollama",
|
||||
"llama3.2": "ollama",
|
||||
"llama3.3": "ollama",
|
||||
"meta-llama/Llama-3.2-3B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/Llama-3.3-70B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/llama-4-maverick": "open_router",
|
||||
"meta-llama/llama-4-scout": "open_router",
|
||||
"microsoft/wizardlm-2-8x22b": "open_router",
|
||||
"mistralai/mistral-nemo": "open_router",
|
||||
"moonshotai/kimi-k2": "open_router",
|
||||
"nousresearch/hermes-3-llama-3.1-405b": "open_router",
|
||||
"nousresearch/hermes-3-llama-3.1-70b": "open_router",
|
||||
"nvidia/llama-3.1-nemotron-70b-instruct": "aiml_api",
|
||||
"o1": "openai",
|
||||
"o1-mini": "openai",
|
||||
"o3-2025-04-16": "openai",
|
||||
"o3-mini": "openai",
|
||||
"openai/gpt-oss-120b": "open_router",
|
||||
"openai/gpt-oss-20b": "open_router",
|
||||
"perplexity/sonar": "open_router",
|
||||
"perplexity/sonar-deep-research": "open_router",
|
||||
"perplexity/sonar-pro": "open_router",
|
||||
"qwen/qwen3-235b-a22b-thinking-2507": "open_router",
|
||||
"qwen/qwen3-coder": "open_router",
|
||||
"v0-1.0-md": "v0",
|
||||
"v0-1.5-lg": "v0",
|
||||
"v0-1.5-md": "v0",
|
||||
"x-ai/grok-4": "open_router",
|
||||
"x-ai/grok-4-fast": "open_router",
|
||||
"x-ai/grok-4.1-fast": "open_router",
|
||||
"x-ai/grok-code-fast-1": "open_router"
|
||||
},
|
||||
"discriminator_values": [
|
||||
"gpt-4o"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"jina_api_key_credentials",
|
||||
"openai_api_key_credentials"
|
||||
],
|
||||
"title": "AIWebpageCopyImproverCredentialsInputSchema",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
{
|
||||
"id": "4c6b68cb-bb75-4044-b1cb-2cee3fd39b26",
|
||||
"version": 29,
|
||||
"is_active": true,
|
||||
"name": "Email Address Finder",
|
||||
"description": "Input information of a business and find their email address",
|
||||
"instructions": null,
|
||||
"recommended_schedule_cron": null,
|
||||
"nodes": [
|
||||
{
|
||||
"id": "04cad535-9f1a-4876-8b07-af5897d8c282",
|
||||
"block_id": "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b",
|
||||
"input_default": {
|
||||
"name": "Address",
|
||||
"value": "USA"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 1047.9357219838776,
|
||||
"y": 1067.9123910370954
|
||||
}
|
||||
},
|
||||
"input_links": [],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "aac29f7b-3cd1-4c91-9a2a-72a8301c0957",
|
||||
"source_id": "04cad535-9f1a-4876-8b07-af5897d8c282",
|
||||
"sink_id": "28b5ddcc-dc20-41cc-ad21-c54ff459f694",
|
||||
"source_name": "result",
|
||||
"sink_name": "values_#_ADDRESS",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"graph_id": "4c6b68cb-bb75-4044-b1cb-2cee3fd39b26",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "a6e7355e-5bf8-4b09-b11c-a5e140389981",
|
||||
"block_id": "3146e4fe-2cdd-4f29-bd12-0c9d5bb4deb0",
|
||||
"input_default": {
|
||||
"group": 1,
|
||||
"pattern": "<email>(.*?)<\\/email>"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 3381.2821481740634,
|
||||
"y": 246.091098184158
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "9f8188ce-1f3d-46fb-acda-b2a57c0e5da6",
|
||||
"source_id": "510937b3-0134-4e45-b2ba-05a447bbaf50",
|
||||
"sink_id": "a6e7355e-5bf8-4b09-b11c-a5e140389981",
|
||||
"source_name": "response",
|
||||
"sink_name": "text",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "b15b5143-27b7-486e-a166-4095e72e5235",
|
||||
"source_id": "a6e7355e-5bf8-4b09-b11c-a5e140389981",
|
||||
"sink_id": "266b7255-11c4-4b88-99e2-85db31a2e865",
|
||||
"source_name": "negative",
|
||||
"sink_name": "values_#_Result",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "23591872-3c6b-4562-87d3-5b6ade698e48",
|
||||
"source_id": "a6e7355e-5bf8-4b09-b11c-a5e140389981",
|
||||
"sink_id": "310c8fab-2ae6-4158-bd48-01dbdc434130",
|
||||
"source_name": "positive",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "4c6b68cb-bb75-4044-b1cb-2cee3fd39b26",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "310c8fab-2ae6-4158-bd48-01dbdc434130",
|
||||
"block_id": "363ae599-353e-4804-937e-b2ee3cef3da4",
|
||||
"input_default": {
|
||||
"name": "Email"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 4525.4246310882,
|
||||
"y": 246.36913665010354
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "d87b07ea-dcec-4d38-a644-2c1d741ea3cb",
|
||||
"source_id": "266b7255-11c4-4b88-99e2-85db31a2e865",
|
||||
"sink_id": "310c8fab-2ae6-4158-bd48-01dbdc434130",
|
||||
"source_name": "output",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "23591872-3c6b-4562-87d3-5b6ade698e48",
|
||||
"source_id": "a6e7355e-5bf8-4b09-b11c-a5e140389981",
|
||||
"sink_id": "310c8fab-2ae6-4158-bd48-01dbdc434130",
|
||||
"source_name": "positive",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [],
|
||||
"graph_id": "4c6b68cb-bb75-4044-b1cb-2cee3fd39b26",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "4a41df99-ffe2-4c12-b528-632979c9c030",
|
||||
"block_id": "87840993-2053-44b7-8da4-187ad4ee518c",
|
||||
"input_default": {},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 2182.7499999999995,
|
||||
"y": 242.00001144409185
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "2e411d3d-79ba-4958-9c1c-b76a45a2e649",
|
||||
"source_id": "28b5ddcc-dc20-41cc-ad21-c54ff459f694",
|
||||
"sink_id": "4a41df99-ffe2-4c12-b528-632979c9c030",
|
||||
"source_name": "output",
|
||||
"sink_name": "query",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "899cc7d8-a96b-4107-b3c6-4c78edcf0c6b",
|
||||
"source_id": "4a41df99-ffe2-4c12-b528-632979c9c030",
|
||||
"sink_id": "510937b3-0134-4e45-b2ba-05a447bbaf50",
|
||||
"source_name": "results",
|
||||
"sink_name": "prompt_values_#_WEBSITE_CONTENT",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "4c6b68cb-bb75-4044-b1cb-2cee3fd39b26",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "9708a10a-8be0-4c44-abb3-bd0f7c594794",
|
||||
"block_id": "c0a8e994-ebf1-4a9c-a4d8-89d09c86741b",
|
||||
"input_default": {
|
||||
"name": "Business Name",
|
||||
"value": "Tim Cook"
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 1049.9704155272595,
|
||||
"y": 244.49931152418344
|
||||
}
|
||||
},
|
||||
"input_links": [],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "946b522c-365f-4ee0-96f9-28863d9882ea",
|
||||
"source_id": "9708a10a-8be0-4c44-abb3-bd0f7c594794",
|
||||
"sink_id": "28b5ddcc-dc20-41cc-ad21-c54ff459f694",
|
||||
"source_name": "result",
|
||||
"sink_name": "values_#_NAME",
|
||||
"is_static": true
|
||||
},
|
||||
{
|
||||
"id": "43e920a7-0bb4-4fae-9a22-91df95c7342a",
|
||||
"source_id": "9708a10a-8be0-4c44-abb3-bd0f7c594794",
|
||||
"sink_id": "510937b3-0134-4e45-b2ba-05a447bbaf50",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_BUSINESS_NAME",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"graph_id": "4c6b68cb-bb75-4044-b1cb-2cee3fd39b26",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "28b5ddcc-dc20-41cc-ad21-c54ff459f694",
|
||||
"block_id": "db7d8f02-2f44-4c55-ab7a-eae0941f0c30",
|
||||
"input_default": {
|
||||
"format": "Email Address of {{NAME}}, {{ADDRESS}}",
|
||||
"values": {}
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 1625.25,
|
||||
"y": 243.25001144409185
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "946b522c-365f-4ee0-96f9-28863d9882ea",
|
||||
"source_id": "9708a10a-8be0-4c44-abb3-bd0f7c594794",
|
||||
"sink_id": "28b5ddcc-dc20-41cc-ad21-c54ff459f694",
|
||||
"source_name": "result",
|
||||
"sink_name": "values_#_NAME",
|
||||
"is_static": true
|
||||
},
|
||||
{
|
||||
"id": "aac29f7b-3cd1-4c91-9a2a-72a8301c0957",
|
||||
"source_id": "04cad535-9f1a-4876-8b07-af5897d8c282",
|
||||
"sink_id": "28b5ddcc-dc20-41cc-ad21-c54ff459f694",
|
||||
"source_name": "result",
|
||||
"sink_name": "values_#_ADDRESS",
|
||||
"is_static": true
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "2e411d3d-79ba-4958-9c1c-b76a45a2e649",
|
||||
"source_id": "28b5ddcc-dc20-41cc-ad21-c54ff459f694",
|
||||
"sink_id": "4a41df99-ffe2-4c12-b528-632979c9c030",
|
||||
"source_name": "output",
|
||||
"sink_name": "query",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "4c6b68cb-bb75-4044-b1cb-2cee3fd39b26",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "266b7255-11c4-4b88-99e2-85db31a2e865",
|
||||
"block_id": "db7d8f02-2f44-4c55-ab7a-eae0941f0c30",
|
||||
"input_default": {
|
||||
"format": "Failed to find email. \nResult:\n{{RESULT}}",
|
||||
"values": {}
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 3949.7493830805934,
|
||||
"y": 705.209819698647
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "b15b5143-27b7-486e-a166-4095e72e5235",
|
||||
"source_id": "a6e7355e-5bf8-4b09-b11c-a5e140389981",
|
||||
"sink_id": "266b7255-11c4-4b88-99e2-85db31a2e865",
|
||||
"source_name": "negative",
|
||||
"sink_name": "values_#_Result",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "d87b07ea-dcec-4d38-a644-2c1d741ea3cb",
|
||||
"source_id": "266b7255-11c4-4b88-99e2-85db31a2e865",
|
||||
"sink_id": "310c8fab-2ae6-4158-bd48-01dbdc434130",
|
||||
"source_name": "output",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "4c6b68cb-bb75-4044-b1cb-2cee3fd39b26",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
},
|
||||
{
|
||||
"id": "510937b3-0134-4e45-b2ba-05a447bbaf50",
|
||||
"block_id": "1f292d4a-41a4-4977-9684-7c8d560b9f91",
|
||||
"input_default": {
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"prompt": "<business_website>\n{{WEBSITE_CONTENT}}\n</business_website>\n\nExtract the Contact Email of {{BUSINESS_NAME}}.\n\nIf no email that can be used to contact {{BUSINESS_NAME}} is present, output `N/A`.\nDo not share any emails other than the email for this specific entity.\n\nIf multiple present pick the likely best one.\n\nRespond with the email (or N/A) inside <email></email> tags.\n\nExample Response:\n\n<thoughts_or_comments>\nThere were many emails present, but luckily one was for {{BUSINESS_NAME}} which I have included below.\n</thoughts_or_comments>\n<email>\nexample@email.com\n</email>",
|
||||
"prompt_values": {}
|
||||
},
|
||||
"metadata": {
|
||||
"position": {
|
||||
"x": 2774.879259081777,
|
||||
"y": 243.3102035752969
|
||||
}
|
||||
},
|
||||
"input_links": [
|
||||
{
|
||||
"id": "43e920a7-0bb4-4fae-9a22-91df95c7342a",
|
||||
"source_id": "9708a10a-8be0-4c44-abb3-bd0f7c594794",
|
||||
"sink_id": "510937b3-0134-4e45-b2ba-05a447bbaf50",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_BUSINESS_NAME",
|
||||
"is_static": true
|
||||
},
|
||||
{
|
||||
"id": "899cc7d8-a96b-4107-b3c6-4c78edcf0c6b",
|
||||
"source_id": "4a41df99-ffe2-4c12-b528-632979c9c030",
|
||||
"sink_id": "510937b3-0134-4e45-b2ba-05a447bbaf50",
|
||||
"source_name": "results",
|
||||
"sink_name": "prompt_values_#_WEBSITE_CONTENT",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"output_links": [
|
||||
{
|
||||
"id": "9f8188ce-1f3d-46fb-acda-b2a57c0e5da6",
|
||||
"source_id": "510937b3-0134-4e45-b2ba-05a447bbaf50",
|
||||
"sink_id": "a6e7355e-5bf8-4b09-b11c-a5e140389981",
|
||||
"source_name": "response",
|
||||
"sink_name": "text",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"graph_id": "4c6b68cb-bb75-4044-b1cb-2cee3fd39b26",
|
||||
"graph_version": 29,
|
||||
"webhook_id": null,
|
||||
"webhook": null
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "9f8188ce-1f3d-46fb-acda-b2a57c0e5da6",
|
||||
"source_id": "510937b3-0134-4e45-b2ba-05a447bbaf50",
|
||||
"sink_id": "a6e7355e-5bf8-4b09-b11c-a5e140389981",
|
||||
"source_name": "response",
|
||||
"sink_name": "text",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "b15b5143-27b7-486e-a166-4095e72e5235",
|
||||
"source_id": "a6e7355e-5bf8-4b09-b11c-a5e140389981",
|
||||
"sink_id": "266b7255-11c4-4b88-99e2-85db31a2e865",
|
||||
"source_name": "negative",
|
||||
"sink_name": "values_#_Result",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "d87b07ea-dcec-4d38-a644-2c1d741ea3cb",
|
||||
"source_id": "266b7255-11c4-4b88-99e2-85db31a2e865",
|
||||
"sink_id": "310c8fab-2ae6-4158-bd48-01dbdc434130",
|
||||
"source_name": "output",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "946b522c-365f-4ee0-96f9-28863d9882ea",
|
||||
"source_id": "9708a10a-8be0-4c44-abb3-bd0f7c594794",
|
||||
"sink_id": "28b5ddcc-dc20-41cc-ad21-c54ff459f694",
|
||||
"source_name": "result",
|
||||
"sink_name": "values_#_NAME",
|
||||
"is_static": true
|
||||
},
|
||||
{
|
||||
"id": "23591872-3c6b-4562-87d3-5b6ade698e48",
|
||||
"source_id": "a6e7355e-5bf8-4b09-b11c-a5e140389981",
|
||||
"sink_id": "310c8fab-2ae6-4158-bd48-01dbdc434130",
|
||||
"source_name": "positive",
|
||||
"sink_name": "value",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "43e920a7-0bb4-4fae-9a22-91df95c7342a",
|
||||
"source_id": "9708a10a-8be0-4c44-abb3-bd0f7c594794",
|
||||
"sink_id": "510937b3-0134-4e45-b2ba-05a447bbaf50",
|
||||
"source_name": "result",
|
||||
"sink_name": "prompt_values_#_BUSINESS_NAME",
|
||||
"is_static": true
|
||||
},
|
||||
{
|
||||
"id": "2e411d3d-79ba-4958-9c1c-b76a45a2e649",
|
||||
"source_id": "28b5ddcc-dc20-41cc-ad21-c54ff459f694",
|
||||
"sink_id": "4a41df99-ffe2-4c12-b528-632979c9c030",
|
||||
"source_name": "output",
|
||||
"sink_name": "query",
|
||||
"is_static": false
|
||||
},
|
||||
{
|
||||
"id": "aac29f7b-3cd1-4c91-9a2a-72a8301c0957",
|
||||
"source_id": "04cad535-9f1a-4876-8b07-af5897d8c282",
|
||||
"sink_id": "28b5ddcc-dc20-41cc-ad21-c54ff459f694",
|
||||
"source_name": "result",
|
||||
"sink_name": "values_#_ADDRESS",
|
||||
"is_static": true
|
||||
},
|
||||
{
|
||||
"id": "899cc7d8-a96b-4107-b3c6-4c78edcf0c6b",
|
||||
"source_id": "4a41df99-ffe2-4c12-b528-632979c9c030",
|
||||
"sink_id": "510937b3-0134-4e45-b2ba-05a447bbaf50",
|
||||
"source_name": "results",
|
||||
"sink_name": "prompt_values_#_WEBSITE_CONTENT",
|
||||
"is_static": false
|
||||
}
|
||||
],
|
||||
"forked_from_id": null,
|
||||
"forked_from_version": null,
|
||||
"sub_graphs": [],
|
||||
"user_id": "",
|
||||
"created_at": "2025-01-03T00:46:30.244Z",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Address": {
|
||||
"advanced": false,
|
||||
"secret": false,
|
||||
"title": "Address",
|
||||
"default": "USA"
|
||||
},
|
||||
"Business Name": {
|
||||
"advanced": false,
|
||||
"secret": false,
|
||||
"title": "Business Name",
|
||||
"default": "Tim Cook"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Email": {
|
||||
"advanced": false,
|
||||
"secret": false,
|
||||
"title": "Email"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Email"
|
||||
]
|
||||
},
|
||||
"has_external_trigger": false,
|
||||
"has_human_in_the_loop": false,
|
||||
"trigger_setup_info": null,
|
||||
"credentials_input_schema": {
|
||||
"properties": {
|
||||
"jina_api_key_credentials": {
|
||||
"credentials_provider": [
|
||||
"jina"
|
||||
],
|
||||
"credentials_types": [
|
||||
"api_key"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Title"
|
||||
},
|
||||
"provider": {
|
||||
"const": "jina",
|
||||
"title": "Provider",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "api_key",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"provider",
|
||||
"type"
|
||||
],
|
||||
"title": "CredentialsMetaInput[Literal[<ProviderName.JINA: 'jina'>], Literal['api_key']]",
|
||||
"type": "object",
|
||||
"discriminator_values": []
|
||||
},
|
||||
"anthropic_api_key_credentials": {
|
||||
"credentials_provider": [
|
||||
"anthropic"
|
||||
],
|
||||
"credentials_types": [
|
||||
"api_key"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"title": "Id",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"title": "Title"
|
||||
},
|
||||
"provider": {
|
||||
"const": "anthropic",
|
||||
"title": "Provider",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "api_key",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"provider",
|
||||
"type"
|
||||
],
|
||||
"title": "CredentialsMetaInput[Literal[<ProviderName.ANTHROPIC: 'anthropic'>], Literal['api_key']]",
|
||||
"type": "object",
|
||||
"discriminator": "model",
|
||||
"discriminator_mapping": {
|
||||
"Llama-3.3-70B-Instruct": "llama_api",
|
||||
"Llama-3.3-8B-Instruct": "llama_api",
|
||||
"Llama-4-Maverick-17B-128E-Instruct-FP8": "llama_api",
|
||||
"Llama-4-Scout-17B-16E-Instruct-FP8": "llama_api",
|
||||
"Qwen/Qwen2.5-72B-Instruct-Turbo": "aiml_api",
|
||||
"amazon/nova-lite-v1": "open_router",
|
||||
"amazon/nova-micro-v1": "open_router",
|
||||
"amazon/nova-pro-v1": "open_router",
|
||||
"claude-3-7-sonnet-20250219": "anthropic",
|
||||
"claude-3-haiku-20240307": "anthropic",
|
||||
"claude-haiku-4-5-20251001": "anthropic",
|
||||
"claude-opus-4-1-20250805": "anthropic",
|
||||
"claude-opus-4-20250514": "anthropic",
|
||||
"claude-opus-4-5-20251101": "anthropic",
|
||||
"claude-sonnet-4-20250514": "anthropic",
|
||||
"claude-sonnet-4-5-20250929": "anthropic",
|
||||
"cohere/command-r-08-2024": "open_router",
|
||||
"cohere/command-r-plus-08-2024": "open_router",
|
||||
"deepseek/deepseek-chat": "open_router",
|
||||
"deepseek/deepseek-r1-0528": "open_router",
|
||||
"dolphin-mistral:latest": "ollama",
|
||||
"google/gemini-2.0-flash-001": "open_router",
|
||||
"google/gemini-2.0-flash-lite-001": "open_router",
|
||||
"google/gemini-2.5-flash": "open_router",
|
||||
"google/gemini-2.5-flash-lite-preview-06-17": "open_router",
|
||||
"google/gemini-2.5-pro-preview-03-25": "open_router",
|
||||
"google/gemini-3-pro-preview": "open_router",
|
||||
"gpt-3.5-turbo": "openai",
|
||||
"gpt-4-turbo": "openai",
|
||||
"gpt-4.1-2025-04-14": "openai",
|
||||
"gpt-4.1-mini-2025-04-14": "openai",
|
||||
"gpt-4o": "openai",
|
||||
"gpt-4o-mini": "openai",
|
||||
"gpt-5-2025-08-07": "openai",
|
||||
"gpt-5-chat-latest": "openai",
|
||||
"gpt-5-mini-2025-08-07": "openai",
|
||||
"gpt-5-nano-2025-08-07": "openai",
|
||||
"gpt-5.1-2025-11-13": "openai",
|
||||
"gryphe/mythomax-l2-13b": "open_router",
|
||||
"llama-3.1-8b-instant": "groq",
|
||||
"llama-3.3-70b-versatile": "groq",
|
||||
"llama3": "ollama",
|
||||
"llama3.1:405b": "ollama",
|
||||
"llama3.2": "ollama",
|
||||
"llama3.3": "ollama",
|
||||
"meta-llama/Llama-3.2-3B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/Llama-3.3-70B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": "aiml_api",
|
||||
"meta-llama/llama-4-maverick": "open_router",
|
||||
"meta-llama/llama-4-scout": "open_router",
|
||||
"microsoft/wizardlm-2-8x22b": "open_router",
|
||||
"mistralai/mistral-nemo": "open_router",
|
||||
"moonshotai/kimi-k2": "open_router",
|
||||
"nousresearch/hermes-3-llama-3.1-405b": "open_router",
|
||||
"nousresearch/hermes-3-llama-3.1-70b": "open_router",
|
||||
"nvidia/llama-3.1-nemotron-70b-instruct": "aiml_api",
|
||||
"o1": "openai",
|
||||
"o1-mini": "openai",
|
||||
"o3-2025-04-16": "openai",
|
||||
"o3-mini": "openai",
|
||||
"openai/gpt-oss-120b": "open_router",
|
||||
"openai/gpt-oss-20b": "open_router",
|
||||
"perplexity/sonar": "open_router",
|
||||
"perplexity/sonar-deep-research": "open_router",
|
||||
"perplexity/sonar-pro": "open_router",
|
||||
"qwen/qwen3-235b-a22b-thinking-2507": "open_router",
|
||||
"qwen/qwen3-coder": "open_router",
|
||||
"v0-1.0-md": "v0",
|
||||
"v0-1.5-lg": "v0",
|
||||
"v0-1.5-md": "v0",
|
||||
"x-ai/grok-4": "open_router",
|
||||
"x-ai/grok-4-fast": "open_router",
|
||||
"x-ai/grok-4.1-fast": "open_router",
|
||||
"x-ai/grok-code-fast-1": "open_router"
|
||||
},
|
||||
"discriminator_values": [
|
||||
"claude-sonnet-4-5-20250929"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"jina_api_key_credentials",
|
||||
"anthropic_api_key_credentials"
|
||||
],
|
||||
"title": "EmailAddressFinderCredentialsInputSchema",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,8 @@
|
||||
import asyncio
|
||||
import mimetypes
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from backend.data.model import SchemaField
|
||||
from backend.util.file import get_exec_file_path
|
||||
from backend.util.request import Requests
|
||||
from backend.util.type import MediaFileType
|
||||
from backend.util.virus_scanner import scan_content_safe
|
||||
|
||||
AttachmentView = Literal[
|
||||
"DOCS",
|
||||
@@ -30,8 +22,8 @@ ATTACHMENT_VIEWS: tuple[AttachmentView, ...] = (
|
||||
)
|
||||
|
||||
|
||||
class GoogleDriveFile(BaseModel):
|
||||
"""Represents a single file/folder picked from Google Drive"""
|
||||
class _GoogleDriveFileBase(BaseModel):
|
||||
"""Internal base class for Google Drive file representation."""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
@@ -49,144 +41,115 @@ class GoogleDriveFile(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
def GoogleDrivePickerField(
|
||||
multiselect: bool = False,
|
||||
allow_folder_selection: bool = False,
|
||||
allowed_views: Optional[list[AttachmentView]] = None,
|
||||
allowed_mime_types: Optional[list[str]] = None,
|
||||
scopes: Optional[list[str]] = None,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
placeholder: Optional[str] = None,
|
||||
**kwargs,
|
||||
class GoogleDriveFile(_GoogleDriveFileBase):
|
||||
"""
|
||||
Represents a Google Drive file/folder with optional credentials for chaining.
|
||||
|
||||
Used for both inputs and outputs in Google Drive blocks. The `_credentials_id`
|
||||
field enables chaining between blocks - when one block outputs a file, the
|
||||
next block can use the same credentials to access it.
|
||||
|
||||
When used with GoogleDriveFileField(), the frontend renders a combined
|
||||
auth + file picker UI that automatically populates `_credentials_id`.
|
||||
"""
|
||||
|
||||
# Hidden field for credential ID - populated by frontend, preserved in outputs
|
||||
credentials_id: Optional[str] = Field(
|
||||
None,
|
||||
alias="_credentials_id",
|
||||
description="Internal: credential ID for authentication",
|
||||
)
|
||||
|
||||
|
||||
def GoogleDriveFileField(
|
||||
*,
|
||||
title: str,
|
||||
description: str | None = None,
|
||||
credentials_kwarg: str = "credentials",
|
||||
credentials_scopes: list[str] | None = None,
|
||||
allowed_views: list[AttachmentView] | None = None,
|
||||
allowed_mime_types: list[str] | None = None,
|
||||
placeholder: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""
|
||||
Creates a Google Drive Picker input field.
|
||||
Creates a Google Drive file input field with auto-generated credentials.
|
||||
|
||||
This field type produces a single UI element that handles both:
|
||||
1. Google OAuth authentication
|
||||
2. File selection via Google Drive Picker
|
||||
|
||||
The system automatically generates a credentials field, and the credentials
|
||||
are passed to the run() method using the specified kwarg name.
|
||||
|
||||
Args:
|
||||
multiselect: Allow selecting multiple files/folders (default: False)
|
||||
allow_folder_selection: Allow selecting folders (default: False)
|
||||
allowed_views: List of view types to show in picker (default: ["DOCS"])
|
||||
allowed_mime_types: Filter by MIME types (e.g., ["application/pdf"])
|
||||
title: Field title shown in UI
|
||||
description: Field description/help text
|
||||
credentials_kwarg: Name of the kwarg that will receive GoogleCredentials
|
||||
in the run() method (default: "credentials")
|
||||
credentials_scopes: OAuth scopes required (default: drive.file)
|
||||
allowed_views: List of view types to show in picker (default: ["DOCS"])
|
||||
allowed_mime_types: Filter by MIME types
|
||||
placeholder: Placeholder text for the button
|
||||
**kwargs: Additional SchemaField arguments (advanced, hidden, etc.)
|
||||
**kwargs: Additional SchemaField arguments
|
||||
|
||||
Returns:
|
||||
Field definition that produces:
|
||||
- Single GoogleDriveFile when multiselect=False
|
||||
- list[GoogleDriveFile] when multiselect=True
|
||||
Field definition that produces GoogleDriveFile
|
||||
|
||||
Example:
|
||||
>>> class MyBlock(Block):
|
||||
... class Input(BlockSchema):
|
||||
... document: GoogleDriveFile = GoogleDrivePickerField(
|
||||
... title="Select Document",
|
||||
... allowed_views=["DOCUMENTS"],
|
||||
... class Input(BlockSchemaInput):
|
||||
... spreadsheet: GoogleDriveFile = GoogleDriveFileField(
|
||||
... title="Select Spreadsheet",
|
||||
... credentials_kwarg="creds",
|
||||
... allowed_views=["SPREADSHEETS"],
|
||||
... )
|
||||
...
|
||||
... files: list[GoogleDriveFile] = GoogleDrivePickerField(
|
||||
... title="Select Multiple Files",
|
||||
... multiselect=True,
|
||||
... allow_folder_selection=True,
|
||||
... )
|
||||
... async def run(
|
||||
... self, input_data: Input, *, creds: GoogleCredentials, **kwargs
|
||||
... ):
|
||||
... # creds is automatically populated
|
||||
... file = input_data.spreadsheet
|
||||
"""
|
||||
# Build configuration that will be sent to frontend
|
||||
|
||||
# Determine scopes - drive.file is sufficient for picker-selected files
|
||||
scopes = credentials_scopes or ["https://www.googleapis.com/auth/drive.file"]
|
||||
|
||||
# Build picker configuration with auto_credentials embedded
|
||||
picker_config = {
|
||||
"multiselect": multiselect,
|
||||
"allow_folder_selection": allow_folder_selection,
|
||||
"multiselect": False,
|
||||
"allow_folder_selection": False,
|
||||
"allowed_views": list(allowed_views) if allowed_views else ["DOCS"],
|
||||
"scopes": scopes,
|
||||
# Auto-credentials config tells frontend to include _credentials_id in output
|
||||
"auto_credentials": {
|
||||
"provider": "google",
|
||||
"type": "oauth2",
|
||||
"scopes": scopes,
|
||||
"kwarg_name": credentials_kwarg,
|
||||
},
|
||||
}
|
||||
|
||||
# Add optional configurations
|
||||
if allowed_mime_types:
|
||||
picker_config["allowed_mime_types"] = list(allowed_mime_types)
|
||||
|
||||
# Determine required scopes based on config
|
||||
base_scopes = scopes if scopes is not None else []
|
||||
picker_scopes: set[str] = set(base_scopes)
|
||||
if allow_folder_selection:
|
||||
picker_scopes.add("https://www.googleapis.com/auth/drive")
|
||||
else:
|
||||
# Use drive.file for minimal scope - only access files selected by user in picker
|
||||
picker_scopes.add("https://www.googleapis.com/auth/drive.file")
|
||||
|
||||
picker_config["scopes"] = sorted(picker_scopes)
|
||||
|
||||
# Set appropriate default value
|
||||
default_value = [] if multiselect else None
|
||||
|
||||
# Use SchemaField to handle format properly
|
||||
return SchemaField(
|
||||
default=default_value,
|
||||
default=None,
|
||||
title=title,
|
||||
description=description,
|
||||
placeholder=placeholder or "Choose from Google Drive",
|
||||
placeholder=placeholder or "Select from Google Drive",
|
||||
# Use google-drive-picker format so frontend renders existing component
|
||||
format="google-drive-picker",
|
||||
advanced=False,
|
||||
json_schema_extra={
|
||||
"google_drive_picker_config": picker_config,
|
||||
# Also keep auto_credentials at top level for backend detection
|
||||
"auto_credentials": {
|
||||
"provider": "google",
|
||||
"type": "oauth2",
|
||||
"scopes": scopes,
|
||||
"kwarg_name": credentials_kwarg,
|
||||
},
|
||||
**kwargs,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
DRIVE_API_URL = "https://www.googleapis.com/drive/v3/files"
|
||||
_requests = Requests(trusted_origins=["https://www.googleapis.com"])
|
||||
|
||||
|
||||
def GoogleDriveAttachmentField(
|
||||
*,
|
||||
title: str,
|
||||
description: str | None = None,
|
||||
placeholder: str | None = None,
|
||||
multiselect: bool = True,
|
||||
allowed_mime_types: list[str] | None = None,
|
||||
**extra: Any,
|
||||
) -> Any:
|
||||
return GoogleDrivePickerField(
|
||||
multiselect=multiselect,
|
||||
allowed_views=list(ATTACHMENT_VIEWS),
|
||||
allowed_mime_types=allowed_mime_types,
|
||||
title=title,
|
||||
description=description,
|
||||
placeholder=placeholder or "Choose files from Google Drive",
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
||||
async def drive_file_to_media_file(
|
||||
drive_file: GoogleDriveFile, *, graph_exec_id: str, access_token: str
|
||||
) -> MediaFileType:
|
||||
if drive_file.is_folder:
|
||||
raise ValueError("Google Drive selection must be a file.")
|
||||
if not access_token:
|
||||
raise ValueError("Google Drive access token is required for file download.")
|
||||
|
||||
url = f"{DRIVE_API_URL}/{drive_file.id}?alt=media"
|
||||
response = await _requests.get(
|
||||
url, headers={"Authorization": f"Bearer {access_token}"}
|
||||
)
|
||||
|
||||
mime_type = drive_file.mime_type or response.headers.get(
|
||||
"content-type", "application/octet-stream"
|
||||
)
|
||||
|
||||
MAX_FILE_SIZE = 100 * 1024 * 1024
|
||||
if len(response.content) > MAX_FILE_SIZE:
|
||||
raise ValueError(
|
||||
f"File too large: {len(response.content)} bytes > {MAX_FILE_SIZE} bytes"
|
||||
)
|
||||
|
||||
base_path = Path(get_exec_file_path(graph_exec_id, ""))
|
||||
base_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
extension = mimetypes.guess_extension(mime_type, strict=False) or ".bin"
|
||||
filename = f"{uuid.uuid4()}{extension}"
|
||||
target_path = base_path / filename
|
||||
|
||||
await scan_content_safe(response.content, filename=filename)
|
||||
await asyncio.to_thread(target_path.write_bytes, response.content)
|
||||
|
||||
return MediaFileType(str(target_path.relative_to(base_path)))
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import build
|
||||
|
||||
from backend.blocks.google._drive import GoogleDriveFile, GoogleDrivePickerField
|
||||
from backend.blocks.google._drive import GoogleDriveFile, GoogleDriveFileField
|
||||
from backend.data.block import (
|
||||
Block,
|
||||
BlockCategory,
|
||||
@@ -182,6 +182,28 @@ def _build_sheets_service(credentials: GoogleCredentials):
|
||||
return build("sheets", "v4", credentials=creds)
|
||||
|
||||
|
||||
def _build_drive_service(credentials: GoogleCredentials):
|
||||
"""Build Drive service from platform credentials (with refresh token)."""
|
||||
settings = Settings()
|
||||
creds = Credentials(
|
||||
token=(
|
||||
credentials.access_token.get_secret_value()
|
||||
if credentials.access_token
|
||||
else None
|
||||
),
|
||||
refresh_token=(
|
||||
credentials.refresh_token.get_secret_value()
|
||||
if credentials.refresh_token
|
||||
else None
|
||||
),
|
||||
token_uri="https://oauth2.googleapis.com/token",
|
||||
client_id=settings.secrets.google_client_id,
|
||||
client_secret=settings.secrets.google_client_secret,
|
||||
scopes=credentials.scopes,
|
||||
)
|
||||
return build("drive", "v3", credentials=creds)
|
||||
|
||||
|
||||
def _validate_spreadsheet_file(spreadsheet_file: "GoogleDriveFile") -> str | None:
|
||||
"""Validate that the selected file is a Google Sheets spreadsheet.
|
||||
|
||||
@@ -250,10 +272,10 @@ class BatchOperation(BlockSchemaInput):
|
||||
|
||||
class GoogleSheetsReadBlock(Block):
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField([])
|
||||
spreadsheet: GoogleDriveFile = GoogleDrivePickerField(
|
||||
spreadsheet: GoogleDriveFile = GoogleDriveFileField(
|
||||
title="Spreadsheet",
|
||||
description="Select a Google Sheets spreadsheet",
|
||||
credentials_kwarg="credentials",
|
||||
allowed_views=["SPREADSHEETS"],
|
||||
allowed_mime_types=["application/vnd.google-apps.spreadsheet"],
|
||||
)
|
||||
@@ -282,7 +304,6 @@ class GoogleSheetsReadBlock(Block):
|
||||
output_schema=GoogleSheetsReadBlock.Output,
|
||||
disabled=GOOGLE_SHEETS_DISABLED,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"spreadsheet": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Spreadsheet",
|
||||
@@ -308,6 +329,7 @@ class GoogleSheetsReadBlock(Block):
|
||||
url="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -338,7 +360,7 @@ class GoogleSheetsReadBlock(Block):
|
||||
self._read_sheet, service, spreadsheet_id, input_data.range
|
||||
)
|
||||
yield "result", data
|
||||
# Output the GoogleDriveFile for chaining
|
||||
# Output the GoogleDriveFile for chaining (preserves credentials_id)
|
||||
yield "spreadsheet", GoogleDriveFile(
|
||||
id=spreadsheet_id,
|
||||
name=input_data.spreadsheet.name,
|
||||
@@ -346,6 +368,7 @@ class GoogleSheetsReadBlock(Block):
|
||||
url=f"https://docs.google.com/spreadsheets/d/{spreadsheet_id}/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=input_data.spreadsheet.credentials_id,
|
||||
)
|
||||
except Exception as e:
|
||||
yield "error", _handle_sheets_api_error(str(e), "read")
|
||||
@@ -373,10 +396,10 @@ class GoogleSheetsReadBlock(Block):
|
||||
|
||||
class GoogleSheetsWriteBlock(Block):
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField([])
|
||||
spreadsheet: GoogleDriveFile = GoogleDrivePickerField(
|
||||
spreadsheet: GoogleDriveFile = GoogleDriveFileField(
|
||||
title="Spreadsheet",
|
||||
description="Select a Google Sheets spreadsheet",
|
||||
credentials_kwarg="credentials",
|
||||
allowed_views=["SPREADSHEETS"],
|
||||
allowed_mime_types=["application/vnd.google-apps.spreadsheet"],
|
||||
)
|
||||
@@ -408,7 +431,6 @@ class GoogleSheetsWriteBlock(Block):
|
||||
output_schema=GoogleSheetsWriteBlock.Output,
|
||||
disabled=GOOGLE_SHEETS_DISABLED,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"spreadsheet": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Spreadsheet",
|
||||
@@ -435,6 +457,7 @@ class GoogleSheetsWriteBlock(Block):
|
||||
url="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -477,7 +500,7 @@ class GoogleSheetsWriteBlock(Block):
|
||||
input_data.values,
|
||||
)
|
||||
yield "result", result
|
||||
# Output the GoogleDriveFile for chaining
|
||||
# Output the GoogleDriveFile for chaining (preserves credentials_id)
|
||||
yield "spreadsheet", GoogleDriveFile(
|
||||
id=input_data.spreadsheet.id,
|
||||
name=input_data.spreadsheet.name,
|
||||
@@ -485,6 +508,7 @@ class GoogleSheetsWriteBlock(Block):
|
||||
url=f"https://docs.google.com/spreadsheets/d/{input_data.spreadsheet.id}/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=input_data.spreadsheet.credentials_id,
|
||||
)
|
||||
except Exception as e:
|
||||
yield "error", _handle_sheets_api_error(str(e), "write")
|
||||
@@ -509,10 +533,10 @@ class GoogleSheetsWriteBlock(Block):
|
||||
|
||||
class GoogleSheetsAppendBlock(Block):
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField([])
|
||||
spreadsheet: GoogleDriveFile = GoogleDrivePickerField(
|
||||
spreadsheet: GoogleDriveFile = GoogleDriveFileField(
|
||||
title="Spreadsheet",
|
||||
description="Select a Google Sheets spreadsheet",
|
||||
credentials_kwarg="credentials",
|
||||
allowed_views=["SPREADSHEETS"],
|
||||
allowed_mime_types=["application/vnd.google-apps.spreadsheet"],
|
||||
)
|
||||
@@ -566,7 +590,6 @@ class GoogleSheetsAppendBlock(Block):
|
||||
output_schema=GoogleSheetsAppendBlock.Output,
|
||||
disabled=GOOGLE_SHEETS_DISABLED,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"spreadsheet": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Spreadsheet",
|
||||
@@ -586,6 +609,7 @@ class GoogleSheetsAppendBlock(Block):
|
||||
url="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -642,7 +666,7 @@ class GoogleSheetsAppendBlock(Block):
|
||||
input_data.insert_data_option,
|
||||
)
|
||||
yield "result", result
|
||||
# Output the GoogleDriveFile for chaining
|
||||
# Output the GoogleDriveFile for chaining (preserves credentials_id)
|
||||
yield "spreadsheet", GoogleDriveFile(
|
||||
id=input_data.spreadsheet.id,
|
||||
name=input_data.spreadsheet.name,
|
||||
@@ -650,6 +674,7 @@ class GoogleSheetsAppendBlock(Block):
|
||||
url=f"https://docs.google.com/spreadsheets/d/{input_data.spreadsheet.id}/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=input_data.spreadsheet.credentials_id,
|
||||
)
|
||||
except Exception as e:
|
||||
yield "error", f"Failed to append to Google Sheet: {str(e)}"
|
||||
@@ -690,10 +715,10 @@ class GoogleSheetsAppendBlock(Block):
|
||||
|
||||
class GoogleSheetsClearBlock(Block):
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField([])
|
||||
spreadsheet: GoogleDriveFile = GoogleDrivePickerField(
|
||||
spreadsheet: GoogleDriveFile = GoogleDriveFileField(
|
||||
title="Spreadsheet",
|
||||
description="Select a Google Sheets spreadsheet",
|
||||
credentials_kwarg="credentials",
|
||||
allowed_views=["SPREADSHEETS"],
|
||||
allowed_mime_types=["application/vnd.google-apps.spreadsheet"],
|
||||
)
|
||||
@@ -722,7 +747,6 @@ class GoogleSheetsClearBlock(Block):
|
||||
output_schema=GoogleSheetsClearBlock.Output,
|
||||
disabled=GOOGLE_SHEETS_DISABLED,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"spreadsheet": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Spreadsheet",
|
||||
@@ -742,6 +766,7 @@ class GoogleSheetsClearBlock(Block):
|
||||
url="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -774,7 +799,7 @@ class GoogleSheetsClearBlock(Block):
|
||||
input_data.range,
|
||||
)
|
||||
yield "result", result
|
||||
# Output the GoogleDriveFile for chaining
|
||||
# Output the GoogleDriveFile for chaining (preserves credentials_id)
|
||||
yield "spreadsheet", GoogleDriveFile(
|
||||
id=input_data.spreadsheet.id,
|
||||
name=input_data.spreadsheet.name,
|
||||
@@ -782,6 +807,7 @@ class GoogleSheetsClearBlock(Block):
|
||||
url=f"https://docs.google.com/spreadsheets/d/{input_data.spreadsheet.id}/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=input_data.spreadsheet.credentials_id,
|
||||
)
|
||||
except Exception as e:
|
||||
yield "error", f"Failed to clear Google Sheet range: {str(e)}"
|
||||
@@ -798,10 +824,10 @@ class GoogleSheetsClearBlock(Block):
|
||||
|
||||
class GoogleSheetsMetadataBlock(Block):
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField([])
|
||||
spreadsheet: GoogleDriveFile = GoogleDrivePickerField(
|
||||
spreadsheet: GoogleDriveFile = GoogleDriveFileField(
|
||||
title="Spreadsheet",
|
||||
description="Select a Google Sheets spreadsheet",
|
||||
credentials_kwarg="credentials",
|
||||
allowed_views=["SPREADSHEETS"],
|
||||
allowed_mime_types=["application/vnd.google-apps.spreadsheet"],
|
||||
)
|
||||
@@ -826,7 +852,6 @@ class GoogleSheetsMetadataBlock(Block):
|
||||
output_schema=GoogleSheetsMetadataBlock.Output,
|
||||
disabled=GOOGLE_SHEETS_DISABLED,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"spreadsheet": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Spreadsheet",
|
||||
@@ -851,6 +876,7 @@ class GoogleSheetsMetadataBlock(Block):
|
||||
url="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -883,7 +909,7 @@ class GoogleSheetsMetadataBlock(Block):
|
||||
input_data.spreadsheet.id,
|
||||
)
|
||||
yield "result", result
|
||||
# Output the GoogleDriveFile for chaining
|
||||
# Output the GoogleDriveFile for chaining (preserves credentials_id)
|
||||
yield "spreadsheet", GoogleDriveFile(
|
||||
id=input_data.spreadsheet.id,
|
||||
name=input_data.spreadsheet.name,
|
||||
@@ -891,6 +917,7 @@ class GoogleSheetsMetadataBlock(Block):
|
||||
url=f"https://docs.google.com/spreadsheets/d/{input_data.spreadsheet.id}/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=input_data.spreadsheet.credentials_id,
|
||||
)
|
||||
except Exception as e:
|
||||
yield "error", f"Failed to get spreadsheet metadata: {str(e)}"
|
||||
@@ -918,10 +945,10 @@ class GoogleSheetsMetadataBlock(Block):
|
||||
|
||||
class GoogleSheetsManageSheetBlock(Block):
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField([])
|
||||
spreadsheet: GoogleDriveFile = GoogleDrivePickerField(
|
||||
spreadsheet: GoogleDriveFile = GoogleDriveFileField(
|
||||
title="Spreadsheet",
|
||||
description="Select a Google Sheets spreadsheet",
|
||||
credentials_kwarg="credentials",
|
||||
allowed_views=["SPREADSHEETS"],
|
||||
allowed_mime_types=["application/vnd.google-apps.spreadsheet"],
|
||||
)
|
||||
@@ -955,7 +982,6 @@ class GoogleSheetsManageSheetBlock(Block):
|
||||
output_schema=GoogleSheetsManageSheetBlock.Output,
|
||||
disabled=GOOGLE_SHEETS_DISABLED,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"spreadsheet": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Spreadsheet",
|
||||
@@ -976,6 +1002,7 @@ class GoogleSheetsManageSheetBlock(Block):
|
||||
url="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1012,7 +1039,7 @@ class GoogleSheetsManageSheetBlock(Block):
|
||||
input_data.destination_sheet_name,
|
||||
)
|
||||
yield "result", result
|
||||
# Output the GoogleDriveFile for chaining
|
||||
# Output the GoogleDriveFile for chaining (preserves credentials_id)
|
||||
yield "spreadsheet", GoogleDriveFile(
|
||||
id=input_data.spreadsheet.id,
|
||||
name=input_data.spreadsheet.name,
|
||||
@@ -1020,6 +1047,7 @@ class GoogleSheetsManageSheetBlock(Block):
|
||||
url=f"https://docs.google.com/spreadsheets/d/{input_data.spreadsheet.id}/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=input_data.spreadsheet.credentials_id,
|
||||
)
|
||||
except Exception as e:
|
||||
yield "error", f"Failed to manage sheet: {str(e)}"
|
||||
@@ -1073,10 +1101,10 @@ class GoogleSheetsManageSheetBlock(Block):
|
||||
|
||||
class GoogleSheetsBatchOperationsBlock(Block):
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField([])
|
||||
spreadsheet: GoogleDriveFile = GoogleDrivePickerField(
|
||||
spreadsheet: GoogleDriveFile = GoogleDriveFileField(
|
||||
title="Spreadsheet",
|
||||
description="Select a Google Sheets spreadsheet",
|
||||
credentials_kwarg="credentials",
|
||||
allowed_views=["SPREADSHEETS"],
|
||||
allowed_mime_types=["application/vnd.google-apps.spreadsheet"],
|
||||
)
|
||||
@@ -1104,7 +1132,6 @@ class GoogleSheetsBatchOperationsBlock(Block):
|
||||
output_schema=GoogleSheetsBatchOperationsBlock.Output,
|
||||
disabled=GOOGLE_SHEETS_DISABLED,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"spreadsheet": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Spreadsheet",
|
||||
@@ -1135,6 +1162,7 @@ class GoogleSheetsBatchOperationsBlock(Block):
|
||||
url="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1168,6 +1196,7 @@ class GoogleSheetsBatchOperationsBlock(Block):
|
||||
input_data.operations,
|
||||
)
|
||||
yield "result", result
|
||||
# Output the GoogleDriveFile for chaining (preserves credentials_id)
|
||||
yield "spreadsheet", GoogleDriveFile(
|
||||
id=input_data.spreadsheet.id,
|
||||
name=input_data.spreadsheet.name,
|
||||
@@ -1175,6 +1204,7 @@ class GoogleSheetsBatchOperationsBlock(Block):
|
||||
url=f"https://docs.google.com/spreadsheets/d/{input_data.spreadsheet.id}/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=input_data.spreadsheet.credentials_id,
|
||||
)
|
||||
except Exception as e:
|
||||
yield "error", f"Failed to perform batch operations: {str(e)}"
|
||||
@@ -1228,10 +1258,10 @@ class GoogleSheetsBatchOperationsBlock(Block):
|
||||
|
||||
class GoogleSheetsFindReplaceBlock(Block):
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField([])
|
||||
spreadsheet: GoogleDriveFile = GoogleDrivePickerField(
|
||||
spreadsheet: GoogleDriveFile = GoogleDriveFileField(
|
||||
title="Spreadsheet",
|
||||
description="Select a Google Sheets spreadsheet",
|
||||
credentials_kwarg="credentials",
|
||||
allowed_views=["SPREADSHEETS"],
|
||||
allowed_mime_types=["application/vnd.google-apps.spreadsheet"],
|
||||
)
|
||||
@@ -1274,7 +1304,6 @@ class GoogleSheetsFindReplaceBlock(Block):
|
||||
output_schema=GoogleSheetsFindReplaceBlock.Output,
|
||||
disabled=GOOGLE_SHEETS_DISABLED,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"spreadsheet": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Spreadsheet",
|
||||
@@ -1297,6 +1326,7 @@ class GoogleSheetsFindReplaceBlock(Block):
|
||||
url="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1331,6 +1361,7 @@ class GoogleSheetsFindReplaceBlock(Block):
|
||||
input_data.match_entire_cell,
|
||||
)
|
||||
yield "result", result
|
||||
# Output the GoogleDriveFile for chaining (preserves credentials_id)
|
||||
yield "spreadsheet", GoogleDriveFile(
|
||||
id=input_data.spreadsheet.id,
|
||||
name=input_data.spreadsheet.name,
|
||||
@@ -1338,6 +1369,7 @@ class GoogleSheetsFindReplaceBlock(Block):
|
||||
url=f"https://docs.google.com/spreadsheets/d/{input_data.spreadsheet.id}/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=input_data.spreadsheet.credentials_id,
|
||||
)
|
||||
except Exception as e:
|
||||
yield "error", f"Failed to find/replace in Google Sheet: {str(e)}"
|
||||
@@ -1376,10 +1408,10 @@ class GoogleSheetsFindReplaceBlock(Block):
|
||||
|
||||
class GoogleSheetsFindBlock(Block):
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField([])
|
||||
spreadsheet: GoogleDriveFile = GoogleDrivePickerField(
|
||||
spreadsheet: GoogleDriveFile = GoogleDriveFileField(
|
||||
title="Spreadsheet",
|
||||
description="Select a Google Sheets spreadsheet",
|
||||
credentials_kwarg="credentials",
|
||||
allowed_views=["SPREADSHEETS"],
|
||||
allowed_mime_types=["application/vnd.google-apps.spreadsheet"],
|
||||
)
|
||||
@@ -1434,7 +1466,6 @@ class GoogleSheetsFindBlock(Block):
|
||||
output_schema=GoogleSheetsFindBlock.Output,
|
||||
disabled=GOOGLE_SHEETS_DISABLED,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"spreadsheet": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Spreadsheet",
|
||||
@@ -1467,6 +1498,7 @@ class GoogleSheetsFindBlock(Block):
|
||||
url="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1511,6 +1543,7 @@ class GoogleSheetsFindBlock(Block):
|
||||
yield "count", result["count"]
|
||||
yield "locations", result["locations"]
|
||||
yield "result", {"success": True}
|
||||
# Output the GoogleDriveFile for chaining (preserves credentials_id)
|
||||
yield "spreadsheet", GoogleDriveFile(
|
||||
id=input_data.spreadsheet.id,
|
||||
name=input_data.spreadsheet.name,
|
||||
@@ -1518,6 +1551,7 @@ class GoogleSheetsFindBlock(Block):
|
||||
url=f"https://docs.google.com/spreadsheets/d/{input_data.spreadsheet.id}/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=input_data.spreadsheet.credentials_id,
|
||||
)
|
||||
except Exception as e:
|
||||
yield "error", f"Failed to find text in Google Sheet: {str(e)}"
|
||||
@@ -1682,10 +1716,10 @@ class GoogleSheetsFindBlock(Block):
|
||||
|
||||
class GoogleSheetsFormatBlock(Block):
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField([])
|
||||
spreadsheet: GoogleDriveFile = GoogleDrivePickerField(
|
||||
spreadsheet: GoogleDriveFile = GoogleDriveFileField(
|
||||
title="Spreadsheet",
|
||||
description="Select a Google Sheets spreadsheet",
|
||||
credentials_kwarg="credentials",
|
||||
allowed_views=["SPREADSHEETS"],
|
||||
allowed_mime_types=["application/vnd.google-apps.spreadsheet"],
|
||||
)
|
||||
@@ -1717,7 +1751,6 @@ class GoogleSheetsFormatBlock(Block):
|
||||
output_schema=GoogleSheetsFormatBlock.Output,
|
||||
disabled=GOOGLE_SHEETS_DISABLED,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"spreadsheet": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Spreadsheet",
|
||||
@@ -1739,6 +1772,7 @@ class GoogleSheetsFormatBlock(Block):
|
||||
url="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1775,6 +1809,7 @@ class GoogleSheetsFormatBlock(Block):
|
||||
yield "error", result["error"]
|
||||
else:
|
||||
yield "result", result
|
||||
# Output the GoogleDriveFile for chaining (preserves credentials_id)
|
||||
yield "spreadsheet", GoogleDriveFile(
|
||||
id=input_data.spreadsheet.id,
|
||||
name=input_data.spreadsheet.name,
|
||||
@@ -1782,6 +1817,7 @@ class GoogleSheetsFormatBlock(Block):
|
||||
url=f"https://docs.google.com/spreadsheets/d/{input_data.spreadsheet.id}/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=input_data.spreadsheet.credentials_id,
|
||||
)
|
||||
except Exception as e:
|
||||
yield "error", f"Failed to format Google Sheet cells: {str(e)}"
|
||||
@@ -1855,7 +1891,10 @@ class GoogleSheetsFormatBlock(Block):
|
||||
|
||||
class GoogleSheetsCreateSpreadsheetBlock(Block):
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField([])
|
||||
# Explicit credentials since this block creates a file (no file picker)
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField(
|
||||
["https://www.googleapis.com/auth/drive.file"]
|
||||
)
|
||||
title: str = SchemaField(
|
||||
description="The title of the new spreadsheet",
|
||||
)
|
||||
@@ -1890,9 +1929,9 @@ class GoogleSheetsCreateSpreadsheetBlock(Block):
|
||||
output_schema=GoogleSheetsCreateSpreadsheetBlock.Output,
|
||||
disabled=GOOGLE_SHEETS_DISABLED,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"title": "Test Spreadsheet",
|
||||
"sheet_names": ["Sheet1", "Data", "Summary"],
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
},
|
||||
test_credentials=TEST_CREDENTIALS,
|
||||
test_output=[
|
||||
@@ -1905,6 +1944,9 @@ class GoogleSheetsCreateSpreadsheetBlock(Block):
|
||||
url="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=TEST_CREDENTIALS_INPUT[
|
||||
"id"
|
||||
], # Preserves credential ID for chaining
|
||||
),
|
||||
),
|
||||
("spreadsheet_id", "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms"),
|
||||
@@ -1926,10 +1968,12 @@ class GoogleSheetsCreateSpreadsheetBlock(Block):
|
||||
async def run(
|
||||
self, input_data: Input, *, credentials: GoogleCredentials, **kwargs
|
||||
) -> BlockOutput:
|
||||
service = _build_sheets_service(credentials)
|
||||
drive_service = _build_drive_service(credentials)
|
||||
sheets_service = _build_sheets_service(credentials)
|
||||
result = await asyncio.to_thread(
|
||||
self._create_spreadsheet,
|
||||
service,
|
||||
drive_service,
|
||||
sheets_service,
|
||||
input_data.title,
|
||||
input_data.sheet_names,
|
||||
)
|
||||
@@ -1939,7 +1983,7 @@ class GoogleSheetsCreateSpreadsheetBlock(Block):
|
||||
else:
|
||||
spreadsheet_id = result["spreadsheetId"]
|
||||
spreadsheet_url = result["spreadsheetUrl"]
|
||||
# Output the full GoogleDriveFile object for easy chaining
|
||||
# Output the GoogleDriveFile for chaining (includes credentials_id)
|
||||
yield "spreadsheet", GoogleDriveFile(
|
||||
id=spreadsheet_id,
|
||||
name=result.get("title", input_data.title),
|
||||
@@ -1947,40 +1991,68 @@ class GoogleSheetsCreateSpreadsheetBlock(Block):
|
||||
url=spreadsheet_url,
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=input_data.credentials.id, # Preserve credentials for chaining
|
||||
)
|
||||
yield "spreadsheet_id", spreadsheet_id
|
||||
yield "spreadsheet_url", spreadsheet_url
|
||||
yield "result", {"success": True}
|
||||
|
||||
def _create_spreadsheet(self, service, title: str, sheet_names: list[str]) -> dict:
|
||||
def _create_spreadsheet(
|
||||
self, drive_service, sheets_service, title: str, sheet_names: list[str]
|
||||
) -> dict:
|
||||
try:
|
||||
# Create the initial spreadsheet
|
||||
spreadsheet_body = {
|
||||
"properties": {"title": title},
|
||||
"sheets": [
|
||||
{
|
||||
"properties": {
|
||||
"title": sheet_names[0] if sheet_names else "Sheet1"
|
||||
}
|
||||
}
|
||||
],
|
||||
# Create blank spreadsheet using Drive API
|
||||
file_metadata = {
|
||||
"name": title,
|
||||
"mimeType": "application/vnd.google-apps.spreadsheet",
|
||||
}
|
||||
result = (
|
||||
drive_service.files()
|
||||
.create(body=file_metadata, fields="id, webViewLink")
|
||||
.execute()
|
||||
)
|
||||
|
||||
result = service.spreadsheets().create(body=spreadsheet_body).execute()
|
||||
spreadsheet_id = result["spreadsheetId"]
|
||||
spreadsheet_url = result["spreadsheetUrl"]
|
||||
spreadsheet_id = result["id"]
|
||||
spreadsheet_url = result.get(
|
||||
"webViewLink",
|
||||
f"https://docs.google.com/spreadsheets/d/{spreadsheet_id}/edit",
|
||||
)
|
||||
|
||||
# Rename first sheet if custom name provided (default is "Sheet1")
|
||||
if sheet_names and sheet_names[0] != "Sheet1":
|
||||
# Get first sheet ID and rename it
|
||||
meta = (
|
||||
sheets_service.spreadsheets()
|
||||
.get(spreadsheetId=spreadsheet_id)
|
||||
.execute()
|
||||
)
|
||||
first_sheet_id = meta["sheets"][0]["properties"]["sheetId"]
|
||||
sheets_service.spreadsheets().batchUpdate(
|
||||
spreadsheetId=spreadsheet_id,
|
||||
body={
|
||||
"requests": [
|
||||
{
|
||||
"updateSheetProperties": {
|
||||
"properties": {
|
||||
"sheetId": first_sheet_id,
|
||||
"title": sheet_names[0],
|
||||
},
|
||||
"fields": "title",
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
).execute()
|
||||
|
||||
# Add additional sheets if requested
|
||||
if len(sheet_names) > 1:
|
||||
requests = []
|
||||
for sheet_name in sheet_names[1:]:
|
||||
requests.append({"addSheet": {"properties": {"title": sheet_name}}})
|
||||
|
||||
if requests:
|
||||
batch_body = {"requests": requests}
|
||||
service.spreadsheets().batchUpdate(
|
||||
spreadsheetId=spreadsheet_id, body=batch_body
|
||||
).execute()
|
||||
requests = [
|
||||
{"addSheet": {"properties": {"title": name}}}
|
||||
for name in sheet_names[1:]
|
||||
]
|
||||
sheets_service.spreadsheets().batchUpdate(
|
||||
spreadsheetId=spreadsheet_id, body={"requests": requests}
|
||||
).execute()
|
||||
|
||||
return {
|
||||
"spreadsheetId": spreadsheet_id,
|
||||
@@ -1995,10 +2067,10 @@ class GoogleSheetsUpdateCellBlock(Block):
|
||||
"""Update a single cell in a Google Sheets spreadsheet."""
|
||||
|
||||
class Input(BlockSchemaInput):
|
||||
credentials: GoogleCredentialsInput = GoogleCredentialsField([])
|
||||
spreadsheet: GoogleDriveFile = GoogleDrivePickerField(
|
||||
spreadsheet: GoogleDriveFile = GoogleDriveFileField(
|
||||
title="Spreadsheet",
|
||||
description="Select a Google Sheets spreadsheet",
|
||||
credentials_kwarg="credentials",
|
||||
allowed_views=["SPREADSHEETS"],
|
||||
allowed_mime_types=["application/vnd.google-apps.spreadsheet"],
|
||||
)
|
||||
@@ -2035,7 +2107,6 @@ class GoogleSheetsUpdateCellBlock(Block):
|
||||
output_schema=GoogleSheetsUpdateCellBlock.Output,
|
||||
disabled=GOOGLE_SHEETS_DISABLED,
|
||||
test_input={
|
||||
"credentials": TEST_CREDENTIALS_INPUT,
|
||||
"spreadsheet": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Spreadsheet",
|
||||
@@ -2059,6 +2130,7 @@ class GoogleSheetsUpdateCellBlock(Block):
|
||||
url="https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=None,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -2096,6 +2168,7 @@ class GoogleSheetsUpdateCellBlock(Block):
|
||||
)
|
||||
|
||||
yield "result", result
|
||||
# Output the GoogleDriveFile for chaining (preserves credentials_id)
|
||||
yield "spreadsheet", GoogleDriveFile(
|
||||
id=input_data.spreadsheet.id,
|
||||
name=input_data.spreadsheet.name,
|
||||
@@ -2103,6 +2176,7 @@ class GoogleSheetsUpdateCellBlock(Block):
|
||||
url=f"https://docs.google.com/spreadsheets/d/{input_data.spreadsheet.id}/edit",
|
||||
iconUrl="https://www.gstatic.com/images/branding/product/1x/sheets_48dp.png",
|
||||
isFolder=False,
|
||||
_credentials_id=input_data.spreadsheet.credentials_id,
|
||||
)
|
||||
except Exception as e:
|
||||
yield "error", _handle_sheets_api_error(str(e), "update")
|
||||
|
||||
@@ -2,6 +2,8 @@ import copy
|
||||
from datetime import date, time
|
||||
from typing import Any, Optional
|
||||
|
||||
# Import for Google Drive file input block
|
||||
from backend.blocks.google._drive import AttachmentView, GoogleDriveFile
|
||||
from backend.data.block import (
|
||||
Block,
|
||||
BlockCategory,
|
||||
@@ -646,6 +648,119 @@ class AgentTableInputBlock(AgentInputBlock):
|
||||
yield "result", input_data.value if input_data.value is not None else []
|
||||
|
||||
|
||||
class AgentGoogleDriveFileInputBlock(AgentInputBlock):
|
||||
"""
|
||||
This block allows users to select a file from Google Drive.
|
||||
|
||||
It provides a Google Drive file picker UI that handles both authentication
|
||||
and file selection. The selected file information (ID, name, URL, etc.)
|
||||
is output for use by other blocks like Google Sheets Read.
|
||||
"""
|
||||
|
||||
class Input(AgentInputBlock.Input):
|
||||
value: Optional[GoogleDriveFile] = SchemaField(
|
||||
description="The selected Google Drive file.",
|
||||
default=None,
|
||||
advanced=False,
|
||||
title="Selected File",
|
||||
)
|
||||
allowed_views: list[AttachmentView] = SchemaField(
|
||||
description="Which views to show in the file picker (DOCS, SPREADSHEETS, PRESENTATIONS, etc.).",
|
||||
default_factory=lambda: ["DOCS", "SPREADSHEETS", "PRESENTATIONS"],
|
||||
advanced=False,
|
||||
title="Allowed Views",
|
||||
)
|
||||
allow_folder_selection: bool = SchemaField(
|
||||
description="Whether to allow selecting folders.",
|
||||
default=False,
|
||||
advanced=True,
|
||||
title="Allow Folder Selection",
|
||||
)
|
||||
|
||||
def generate_schema(self):
|
||||
"""Generate schema for the value field with Google Drive picker format."""
|
||||
schema = super().generate_schema()
|
||||
|
||||
# Default scopes for drive.file access
|
||||
scopes = ["https://www.googleapis.com/auth/drive.file"]
|
||||
|
||||
# Build picker configuration
|
||||
picker_config = {
|
||||
"multiselect": False, # Single file selection only for now
|
||||
"allow_folder_selection": self.allow_folder_selection,
|
||||
"allowed_views": (
|
||||
list(self.allowed_views) if self.allowed_views else ["DOCS"]
|
||||
),
|
||||
"scopes": scopes,
|
||||
# Auto-credentials config tells frontend to include _credentials_id in output
|
||||
"auto_credentials": {
|
||||
"provider": "google",
|
||||
"type": "oauth2",
|
||||
"scopes": scopes,
|
||||
"kwarg_name": "credentials",
|
||||
},
|
||||
}
|
||||
|
||||
# Set format and config for frontend to render Google Drive picker
|
||||
schema["format"] = "google-drive-picker"
|
||||
schema["google_drive_picker_config"] = picker_config
|
||||
# Also keep auto_credentials at top level for backend detection
|
||||
schema["auto_credentials"] = {
|
||||
"provider": "google",
|
||||
"type": "oauth2",
|
||||
"scopes": scopes,
|
||||
"kwarg_name": "credentials",
|
||||
}
|
||||
|
||||
if self.value is not None:
|
||||
schema["default"] = self.value.model_dump()
|
||||
|
||||
return schema
|
||||
|
||||
class Output(AgentInputBlock.Output):
|
||||
result: GoogleDriveFile = SchemaField(
|
||||
description="The selected Google Drive file with ID, name, URL, and other metadata."
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
test_file = GoogleDriveFile.model_validate(
|
||||
{
|
||||
"id": "test-file-id",
|
||||
"name": "Test Spreadsheet",
|
||||
"mimeType": "application/vnd.google-apps.spreadsheet",
|
||||
"url": "https://docs.google.com/spreadsheets/d/test-file-id",
|
||||
}
|
||||
)
|
||||
super().__init__(
|
||||
id="d3b32f15-6fd7-40e3-be52-e083f51b19a2",
|
||||
description="Block for selecting a file from Google Drive.",
|
||||
disabled=not config.enable_agent_input_subtype_blocks,
|
||||
input_schema=AgentGoogleDriveFileInputBlock.Input,
|
||||
output_schema=AgentGoogleDriveFileInputBlock.Output,
|
||||
test_input=[
|
||||
{
|
||||
"name": "spreadsheet_input",
|
||||
"description": "Select a spreadsheet from Google Drive",
|
||||
"allowed_views": ["SPREADSHEETS"],
|
||||
"value": {
|
||||
"id": "test-file-id",
|
||||
"name": "Test Spreadsheet",
|
||||
"mimeType": "application/vnd.google-apps.spreadsheet",
|
||||
"url": "https://docs.google.com/spreadsheets/d/test-file-id",
|
||||
},
|
||||
}
|
||||
],
|
||||
test_output=[("result", test_file)],
|
||||
)
|
||||
|
||||
async def run(self, input_data: Input, *args, **kwargs) -> BlockOutput:
|
||||
"""
|
||||
Yields the selected Google Drive file.
|
||||
"""
|
||||
if input_data.value is not None:
|
||||
yield "result", input_data.value
|
||||
|
||||
|
||||
IO_BLOCK_IDs = [
|
||||
AgentInputBlock().id,
|
||||
AgentOutputBlock().id,
|
||||
@@ -658,4 +773,5 @@ IO_BLOCK_IDs = [
|
||||
AgentDropdownInputBlock().id,
|
||||
AgentToggleInputBlock().id,
|
||||
AgentTableInputBlock().id,
|
||||
AgentGoogleDriveFileInputBlock().id,
|
||||
]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from typing import Type
|
||||
from typing import Any, Type
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.data.block import Block, get_blocks
|
||||
from backend.data.block import Block, BlockSchemaInput, get_blocks
|
||||
from backend.data.model import SchemaField
|
||||
from backend.util.test import execute_block_test
|
||||
|
||||
SKIP_BLOCK_TESTS = {
|
||||
@@ -132,3 +133,148 @@ async def test_block_ids_valid(block: Type[Block]):
|
||||
), f"Block {block.name} ID is UUID version {parsed_uuid.version}, expected version 4"
|
||||
except ValueError:
|
||||
pytest.fail(f"Block {block.name} has invalid UUID format: {block_instance.id}")
|
||||
|
||||
|
||||
class TestAutoCredentialsFieldsValidation:
|
||||
"""Tests for auto_credentials field validation in BlockSchema."""
|
||||
|
||||
def test_duplicate_auto_credentials_kwarg_name_raises_error(self):
|
||||
"""Test that duplicate kwarg_name in auto_credentials raises ValueError."""
|
||||
|
||||
class DuplicateKwargSchema(BlockSchemaInput):
|
||||
"""Schema with duplicate auto_credentials kwarg_name."""
|
||||
|
||||
# Both fields explicitly use the same kwarg_name "credentials"
|
||||
file1: dict[str, Any] | None = SchemaField(
|
||||
description="First file input",
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"auto_credentials": {
|
||||
"provider": "google",
|
||||
"type": "oauth2",
|
||||
"scopes": ["https://www.googleapis.com/auth/drive.file"],
|
||||
"kwarg_name": "credentials",
|
||||
}
|
||||
},
|
||||
)
|
||||
file2: dict[str, Any] | None = SchemaField(
|
||||
description="Second file input",
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"auto_credentials": {
|
||||
"provider": "google",
|
||||
"type": "oauth2",
|
||||
"scopes": ["https://www.googleapis.com/auth/drive.file"],
|
||||
"kwarg_name": "credentials", # Duplicate kwarg_name!
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
DuplicateKwargSchema.get_auto_credentials_fields()
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert "Duplicate auto_credentials kwarg_name 'credentials'" in error_message
|
||||
assert "file1" in error_message
|
||||
assert "file2" in error_message
|
||||
|
||||
def test_unique_auto_credentials_kwarg_names_succeed(self):
|
||||
"""Test that unique kwarg_name values work correctly."""
|
||||
|
||||
class UniqueKwargSchema(BlockSchemaInput):
|
||||
"""Schema with unique auto_credentials kwarg_name values."""
|
||||
|
||||
file1: dict[str, Any] | None = SchemaField(
|
||||
description="First file input",
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"auto_credentials": {
|
||||
"provider": "google",
|
||||
"type": "oauth2",
|
||||
"scopes": ["https://www.googleapis.com/auth/drive.file"],
|
||||
"kwarg_name": "file1_credentials",
|
||||
}
|
||||
},
|
||||
)
|
||||
file2: dict[str, Any] | None = SchemaField(
|
||||
description="Second file input",
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"auto_credentials": {
|
||||
"provider": "google",
|
||||
"type": "oauth2",
|
||||
"scopes": ["https://www.googleapis.com/auth/drive.file"],
|
||||
"kwarg_name": "file2_credentials", # Different kwarg_name
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Should not raise
|
||||
result = UniqueKwargSchema.get_auto_credentials_fields()
|
||||
|
||||
assert "file1_credentials" in result
|
||||
assert "file2_credentials" in result
|
||||
assert result["file1_credentials"]["field_name"] == "file1"
|
||||
assert result["file2_credentials"]["field_name"] == "file2"
|
||||
|
||||
def test_default_kwarg_name_is_credentials(self):
|
||||
"""Test that missing kwarg_name defaults to 'credentials'."""
|
||||
|
||||
class DefaultKwargSchema(BlockSchemaInput):
|
||||
"""Schema with auto_credentials missing kwarg_name."""
|
||||
|
||||
file: dict[str, Any] | None = SchemaField(
|
||||
description="File input",
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"auto_credentials": {
|
||||
"provider": "google",
|
||||
"type": "oauth2",
|
||||
"scopes": ["https://www.googleapis.com/auth/drive.file"],
|
||||
# No kwarg_name specified - should default to "credentials"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
result = DefaultKwargSchema.get_auto_credentials_fields()
|
||||
|
||||
assert "credentials" in result
|
||||
assert result["credentials"]["field_name"] == "file"
|
||||
|
||||
def test_duplicate_default_kwarg_name_raises_error(self):
|
||||
"""Test that two fields with default kwarg_name raises ValueError."""
|
||||
|
||||
class DefaultDuplicateSchema(BlockSchemaInput):
|
||||
"""Schema where both fields omit kwarg_name, defaulting to 'credentials'."""
|
||||
|
||||
file1: dict[str, Any] | None = SchemaField(
|
||||
description="First file input",
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"auto_credentials": {
|
||||
"provider": "google",
|
||||
"type": "oauth2",
|
||||
"scopes": ["https://www.googleapis.com/auth/drive.file"],
|
||||
# No kwarg_name - defaults to "credentials"
|
||||
}
|
||||
},
|
||||
)
|
||||
file2: dict[str, Any] | None = SchemaField(
|
||||
description="Second file input",
|
||||
default=None,
|
||||
json_schema_extra={
|
||||
"auto_credentials": {
|
||||
"provider": "google",
|
||||
"type": "oauth2",
|
||||
"scopes": ["https://www.googleapis.com/auth/drive.file"],
|
||||
# No kwarg_name - also defaults to "credentials"
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
DefaultDuplicateSchema.get_auto_credentials_fields()
|
||||
|
||||
assert "Duplicate auto_credentials kwarg_name 'credentials'" in str(
|
||||
exc_info.value
|
||||
)
|
||||
|
||||
@@ -266,14 +266,61 @@ class BlockSchema(BaseModel):
|
||||
)
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_auto_credentials_fields(cls) -> dict[str, dict[str, Any]]:
|
||||
"""
|
||||
Get fields that have auto_credentials metadata (e.g., GoogleDriveFileInput).
|
||||
|
||||
Returns a dict mapping kwarg_name -> {field_name, auto_credentials_config}
|
||||
|
||||
Raises:
|
||||
ValueError: If multiple fields have the same kwarg_name, as this would
|
||||
cause silent overwriting and only the last field would be processed.
|
||||
"""
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
schema = cls.jsonschema()
|
||||
properties = schema.get("properties", {})
|
||||
|
||||
for field_name, field_schema in properties.items():
|
||||
auto_creds = field_schema.get("auto_credentials")
|
||||
if auto_creds:
|
||||
kwarg_name = auto_creds.get("kwarg_name", "credentials")
|
||||
if kwarg_name in result:
|
||||
raise ValueError(
|
||||
f"Duplicate auto_credentials kwarg_name '{kwarg_name}' "
|
||||
f"in fields '{result[kwarg_name]['field_name']}' and "
|
||||
f"'{field_name}' on {cls.__qualname__}"
|
||||
)
|
||||
result[kwarg_name] = {
|
||||
"field_name": field_name,
|
||||
"config": auto_creds,
|
||||
}
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def get_credentials_fields_info(cls) -> dict[str, CredentialsFieldInfo]:
|
||||
return {
|
||||
field_name: CredentialsFieldInfo.model_validate(
|
||||
result = {}
|
||||
|
||||
# Regular credentials fields
|
||||
for field_name in cls.get_credentials_fields().keys():
|
||||
result[field_name] = CredentialsFieldInfo.model_validate(
|
||||
cls.get_field_schema(field_name), by_alias=True
|
||||
)
|
||||
for field_name in cls.get_credentials_fields().keys()
|
||||
}
|
||||
|
||||
# Auto-generated credentials fields (from GoogleDriveFileInput etc.)
|
||||
for kwarg_name, info in cls.get_auto_credentials_fields().items():
|
||||
config = info["config"]
|
||||
# Build a schema-like dict that CredentialsFieldInfo can parse
|
||||
auto_schema = {
|
||||
"credentials_provider": [config.get("provider", "google")],
|
||||
"credentials_types": [config.get("type", "oauth2")],
|
||||
"credentials_scopes": config.get("scopes"),
|
||||
}
|
||||
result[kwarg_name] = CredentialsFieldInfo.model_validate(
|
||||
auto_schema, by_alias=True
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def get_input_defaults(cls, data: BlockInput) -> BlockInput:
|
||||
|
||||
@@ -218,15 +218,53 @@ async def execute_node(
|
||||
# changes during execution. ⚠️ This means a set of credentials can only be used by
|
||||
# one (running) block at a time; simultaneous execution of blocks using same
|
||||
# credentials is not supported.
|
||||
creds_lock = None
|
||||
creds_locks: list[AsyncRedisLock] = []
|
||||
input_model = cast(type[BlockSchema], node_block.input_schema)
|
||||
|
||||
# Handle regular credentials fields
|
||||
for field_name, input_type in input_model.get_credentials_fields().items():
|
||||
credentials_meta = input_type(**input_data[field_name])
|
||||
credentials, creds_lock = await creds_manager.acquire(
|
||||
user_id, credentials_meta.id
|
||||
)
|
||||
credentials, lock = await creds_manager.acquire(user_id, credentials_meta.id)
|
||||
creds_locks.append(lock)
|
||||
extra_exec_kwargs[field_name] = credentials
|
||||
|
||||
# Handle auto-generated credentials (e.g., from GoogleDriveFileInput)
|
||||
for kwarg_name, info in input_model.get_auto_credentials_fields().items():
|
||||
field_name = info["field_name"]
|
||||
field_data = input_data.get(field_name)
|
||||
if field_data and isinstance(field_data, dict):
|
||||
# Check if _credentials_id key exists in the field data
|
||||
if "_credentials_id" in field_data:
|
||||
cred_id = field_data["_credentials_id"]
|
||||
if cred_id:
|
||||
# Credential ID provided - acquire credentials
|
||||
provider = info.get("config", {}).get(
|
||||
"provider", "external service"
|
||||
)
|
||||
file_name = field_data.get("name", "selected file")
|
||||
try:
|
||||
credentials, lock = await creds_manager.acquire(
|
||||
user_id, cred_id
|
||||
)
|
||||
creds_locks.append(lock)
|
||||
extra_exec_kwargs[kwarg_name] = credentials
|
||||
except ValueError:
|
||||
# Credential was deleted or doesn't exist
|
||||
raise ValueError(
|
||||
f"Authentication expired for '{file_name}' in field '{field_name}'. "
|
||||
f"The saved {provider.capitalize()} credentials no longer exist. "
|
||||
f"Please re-select the file to re-authenticate."
|
||||
)
|
||||
# else: _credentials_id is explicitly None, skip credentials (for chained data)
|
||||
else:
|
||||
# _credentials_id key missing entirely - this is an error
|
||||
provider = info.get("config", {}).get("provider", "external service")
|
||||
file_name = field_data.get("name", "selected file")
|
||||
raise ValueError(
|
||||
f"Authentication missing for '{file_name}' in field '{field_name}'. "
|
||||
f"Please re-select the file to authenticate with {provider.capitalize()}."
|
||||
)
|
||||
|
||||
output_size = 0
|
||||
|
||||
# sentry tracking nonsense to get user counts for blocks because isolation scopes don't work :(
|
||||
@@ -260,12 +298,17 @@ async def execute_node(
|
||||
# Re-raise to maintain normal error flow
|
||||
raise
|
||||
finally:
|
||||
# Ensure credentials are released even if execution fails
|
||||
if creds_lock and (await creds_lock.locked()) and (await creds_lock.owned()):
|
||||
try:
|
||||
await creds_lock.release()
|
||||
except Exception as e:
|
||||
log_metadata.error(f"Failed to release credentials lock: {e}")
|
||||
# Ensure all credentials are released even if execution fails
|
||||
for creds_lock in creds_locks:
|
||||
if (
|
||||
creds_lock
|
||||
and (await creds_lock.locked())
|
||||
and (await creds_lock.owned())
|
||||
):
|
||||
try:
|
||||
await creds_lock.release()
|
||||
except Exception as e:
|
||||
log_metadata.error(f"Failed to release credentials lock: {e}")
|
||||
|
||||
# Update execution stats
|
||||
if execution_stats is not None:
|
||||
|
||||
@@ -273,6 +273,8 @@ async def list_providers(
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load blocks: {e}")
|
||||
|
||||
from backend.sdk.registry import AutoRegistry
|
||||
|
||||
providers = []
|
||||
for name in get_all_provider_names():
|
||||
supports_oauth = name in HANDLERS_BY_NAME
|
||||
@@ -281,13 +283,27 @@ async def list_providers(
|
||||
getattr(handler_class, "DEFAULT_SCOPES", []) if handler_class else []
|
||||
)
|
||||
|
||||
# Check if provider has specific auth types from SDK registration
|
||||
sdk_provider = AutoRegistry.get_provider(name)
|
||||
if sdk_provider and sdk_provider.supported_auth_types:
|
||||
supports_api_key = "api_key" in sdk_provider.supported_auth_types
|
||||
supports_user_password = (
|
||||
"user_password" in sdk_provider.supported_auth_types
|
||||
)
|
||||
supports_host_scoped = "host_scoped" in sdk_provider.supported_auth_types
|
||||
else:
|
||||
# Fallback for legacy providers
|
||||
supports_api_key = True # All providers can accept API keys
|
||||
supports_user_password = name in ("smtp",)
|
||||
supports_host_scoped = name == "http"
|
||||
|
||||
providers.append(
|
||||
ProviderInfo(
|
||||
name=name,
|
||||
supports_oauth=supports_oauth,
|
||||
supports_api_key=True, # All providers can accept API keys
|
||||
supports_user_password=name in ("smtp",), # SMTP uses user/password
|
||||
supports_host_scoped=name == "http", # HTTP block uses host-scoped
|
||||
supports_api_key=supports_api_key,
|
||||
supports_user_password=supports_user_password,
|
||||
supports_host_scoped=supports_host_scoped,
|
||||
default_scopes=default_scopes,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -144,6 +144,8 @@ async def execute_block_test(block: Block):
|
||||
"execution_context": ExecutionContext(),
|
||||
}
|
||||
input_model = cast(type[BlockSchema], block.input_schema)
|
||||
|
||||
# Handle regular credentials fields
|
||||
credentials_input_fields = input_model.get_credentials_fields()
|
||||
if len(credentials_input_fields) == 1 and isinstance(
|
||||
block.test_credentials, _BaseCredentials
|
||||
@@ -158,6 +160,18 @@ async def execute_block_test(block: Block):
|
||||
if field_name in block.test_credentials:
|
||||
extra_exec_kwargs[field_name] = block.test_credentials[field_name]
|
||||
|
||||
# Handle auto-generated credentials (e.g., from GoogleDriveFileInput)
|
||||
auto_creds_fields = input_model.get_auto_credentials_fields()
|
||||
if auto_creds_fields and block.test_credentials:
|
||||
if isinstance(block.test_credentials, _BaseCredentials):
|
||||
# Single credentials object - use for all auto_credentials kwargs
|
||||
for kwarg_name in auto_creds_fields.keys():
|
||||
extra_exec_kwargs[kwarg_name] = block.test_credentials
|
||||
elif isinstance(block.test_credentials, dict):
|
||||
for kwarg_name in auto_creds_fields.keys():
|
||||
if kwarg_name in block.test_credentials:
|
||||
extra_exec_kwargs[kwarg_name] = block.test_credentials[kwarg_name]
|
||||
|
||||
for input_data in block.test_input:
|
||||
log.info(f"{prefix} in: {input_data}")
|
||||
|
||||
|
||||
431
autogpt_platform/backend/test/load_store_agents.py
Normal file
431
autogpt_platform/backend/test/load_store_agents.py
Normal file
@@ -0,0 +1,431 @@
|
||||
"""
|
||||
Load Store Agents Script
|
||||
|
||||
This script loads the exported store agents from the agents/ folder into the test database.
|
||||
It creates:
|
||||
- A user and profile for the 'autogpt' creator
|
||||
- AgentGraph records from JSON files
|
||||
- StoreListing and StoreListingVersion records from CSV metadata
|
||||
- Approves agents that have is_available=true in the CSV
|
||||
|
||||
Usage:
|
||||
cd backend
|
||||
poetry run python test/load_store_agents.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import prisma.enums
|
||||
from prisma import Json, Prisma
|
||||
from prisma.types import (
|
||||
AgentBlockCreateInput,
|
||||
AgentGraphCreateInput,
|
||||
AgentNodeCreateInput,
|
||||
AgentNodeLinkCreateInput,
|
||||
ProfileCreateInput,
|
||||
StoreListingCreateInput,
|
||||
StoreListingVersionCreateInput,
|
||||
UserCreateInput,
|
||||
)
|
||||
|
||||
# Path to agents folder (relative to backend directory)
|
||||
AGENTS_DIR = Path(__file__).parent.parent / "agents"
|
||||
CSV_FILE = AGENTS_DIR / "StoreAgent_rows.csv"
|
||||
|
||||
# Fixed user ID for the autogpt creator (test data, not production)
|
||||
AUTOGPT_USER_ID = "79d96c73-e6f5-4656-a83a-185b41ee0d06"
|
||||
AUTOGPT_EMAIL = "autogpt-test@agpt.co"
|
||||
AUTOGPT_USERNAME = "autogpt"
|
||||
|
||||
|
||||
async def initialize_blocks(db: Prisma) -> set[str]:
|
||||
"""Initialize agent blocks in the database from the registered blocks.
|
||||
|
||||
Returns a set of block IDs that exist in the database.
|
||||
"""
|
||||
from backend.data.block import get_blocks
|
||||
|
||||
print(" Initializing agent blocks...")
|
||||
blocks = get_blocks()
|
||||
created_count = 0
|
||||
block_ids = set()
|
||||
|
||||
for block_cls in blocks.values():
|
||||
block = block_cls()
|
||||
block_ids.add(block.id)
|
||||
existing_block = await db.agentblock.find_first(
|
||||
where={"OR": [{"id": block.id}, {"name": block.name}]}
|
||||
)
|
||||
if not existing_block:
|
||||
await db.agentblock.create(
|
||||
data=AgentBlockCreateInput(
|
||||
id=block.id,
|
||||
name=block.name,
|
||||
inputSchema=json.dumps(block.input_schema.jsonschema()),
|
||||
outputSchema=json.dumps(block.output_schema.jsonschema()),
|
||||
)
|
||||
)
|
||||
created_count += 1
|
||||
elif block.id != existing_block.id or block.name != existing_block.name:
|
||||
await db.agentblock.update(
|
||||
where={"id": existing_block.id},
|
||||
data={
|
||||
"id": block.id,
|
||||
"name": block.name,
|
||||
"inputSchema": json.dumps(block.input_schema.jsonschema()),
|
||||
"outputSchema": json.dumps(block.output_schema.jsonschema()),
|
||||
},
|
||||
)
|
||||
|
||||
print(f" Initialized {len(blocks)} blocks ({created_count} new)")
|
||||
return block_ids
|
||||
|
||||
|
||||
async def ensure_block_exists(
|
||||
db: Prisma, block_id: str, known_blocks: set[str]
|
||||
) -> bool:
|
||||
"""Ensure a block exists in the database, create a placeholder if needed.
|
||||
|
||||
Returns True if the block exists (or was created), False otherwise.
|
||||
"""
|
||||
if block_id in known_blocks:
|
||||
return True
|
||||
|
||||
# Check if it already exists in the database
|
||||
existing = await db.agentblock.find_unique(where={"id": block_id})
|
||||
if existing:
|
||||
known_blocks.add(block_id)
|
||||
return True
|
||||
|
||||
# Create a placeholder block
|
||||
print(f" Creating placeholder block: {block_id}")
|
||||
try:
|
||||
await db.agentblock.create(
|
||||
data=AgentBlockCreateInput(
|
||||
id=block_id,
|
||||
name=f"Placeholder_{block_id[:8]}",
|
||||
inputSchema="{}",
|
||||
outputSchema="{}",
|
||||
)
|
||||
)
|
||||
known_blocks.add(block_id)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" Warning: Could not create placeholder block {block_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def parse_image_urls(image_str: str) -> list[str]:
|
||||
"""Parse the image URLs from CSV format like ["url1","url2"]."""
|
||||
if not image_str or image_str == "[]":
|
||||
return []
|
||||
try:
|
||||
return json.loads(image_str)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
|
||||
def parse_categories(categories_str: str) -> list[str]:
|
||||
"""Parse categories from CSV format like ["cat1","cat2"]."""
|
||||
if not categories_str or categories_str == "[]":
|
||||
return []
|
||||
try:
|
||||
return json.loads(categories_str)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
|
||||
def sanitize_slug(slug: str) -> str:
|
||||
"""Ensure slug only contains valid characters."""
|
||||
return re.sub(r"[^a-z0-9-]", "", slug.lower())
|
||||
|
||||
|
||||
async def create_user_and_profile(db: Prisma) -> None:
|
||||
"""Create the autogpt user and profile if they don't exist."""
|
||||
# Check if user exists
|
||||
existing_user = await db.user.find_unique(where={"id": AUTOGPT_USER_ID})
|
||||
if existing_user:
|
||||
print(f"User {AUTOGPT_USER_ID} already exists, skipping user creation")
|
||||
else:
|
||||
print(f"Creating user {AUTOGPT_USER_ID}")
|
||||
await db.user.create(
|
||||
data=UserCreateInput(
|
||||
id=AUTOGPT_USER_ID,
|
||||
email=AUTOGPT_EMAIL,
|
||||
name="AutoGPT",
|
||||
metadata=Json({}),
|
||||
integrations="",
|
||||
)
|
||||
)
|
||||
|
||||
# Check if profile exists
|
||||
existing_profile = await db.profile.find_first(where={"userId": AUTOGPT_USER_ID})
|
||||
if existing_profile:
|
||||
print(
|
||||
f"Profile for user {AUTOGPT_USER_ID} already exists, skipping profile creation"
|
||||
)
|
||||
else:
|
||||
print(f"Creating profile for user {AUTOGPT_USER_ID}")
|
||||
await db.profile.create(
|
||||
data=ProfileCreateInput(
|
||||
userId=AUTOGPT_USER_ID,
|
||||
name="AutoGPT",
|
||||
username=AUTOGPT_USERNAME,
|
||||
description="Official AutoGPT agents and templates",
|
||||
links=["https://agpt.co"],
|
||||
avatarUrl="https://storage.googleapis.com/agpt-prod-website-artifacts/users/b3e41ea4-2f4c-4964-927c-fe682d857bad/images/4b5781a6-49e1-433c-9a75-65af1be5c02d.png",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def load_csv_metadata() -> dict[str, dict]:
|
||||
"""Load CSV metadata and return a dict keyed by storeListingVersionId."""
|
||||
metadata = {}
|
||||
with open(CSV_FILE, "r", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
version_id = row["storeListingVersionId"]
|
||||
metadata[version_id] = {
|
||||
"listing_id": row["listing_id"],
|
||||
"store_listing_version_id": version_id,
|
||||
"slug": sanitize_slug(row["slug"]),
|
||||
"agent_name": row["agent_name"],
|
||||
"agent_video": row["agent_video"] if row["agent_video"] else None,
|
||||
"agent_image": parse_image_urls(row["agent_image"]),
|
||||
"featured": row["featured"].lower() == "true",
|
||||
"sub_heading": row["sub_heading"],
|
||||
"description": row["description"],
|
||||
"categories": parse_categories(row["categories"]),
|
||||
"use_for_onboarding": row["useForOnboarding"].lower() == "true",
|
||||
"is_available": row["is_available"].lower() == "true",
|
||||
}
|
||||
return metadata
|
||||
|
||||
|
||||
async def load_agent_json(json_path: Path) -> dict:
|
||||
"""Load and parse an agent JSON file."""
|
||||
with open(json_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
async def create_agent_graph(
|
||||
db: Prisma, agent_data: dict, known_blocks: set[str]
|
||||
) -> tuple[str, int]:
|
||||
"""Create an AgentGraph and its nodes/links from JSON data."""
|
||||
graph_id = agent_data["id"]
|
||||
version = agent_data.get("version", 1)
|
||||
|
||||
# Check if graph already exists
|
||||
existing_graph = await db.agentgraph.find_unique(
|
||||
where={"graphVersionId": {"id": graph_id, "version": version}}
|
||||
)
|
||||
if existing_graph:
|
||||
print(f" Graph {graph_id} v{version} already exists, skipping")
|
||||
return graph_id, version
|
||||
|
||||
print(
|
||||
f" Creating graph {graph_id} v{version}: {agent_data.get('name', 'Unnamed')}"
|
||||
)
|
||||
|
||||
# Create the main graph
|
||||
await db.agentgraph.create(
|
||||
data=AgentGraphCreateInput(
|
||||
id=graph_id,
|
||||
version=version,
|
||||
name=agent_data.get("name"),
|
||||
description=agent_data.get("description"),
|
||||
instructions=agent_data.get("instructions"),
|
||||
recommendedScheduleCron=agent_data.get("recommended_schedule_cron"),
|
||||
isActive=agent_data.get("is_active", True),
|
||||
userId=AUTOGPT_USER_ID,
|
||||
forkedFromId=agent_data.get("forked_from_id"),
|
||||
forkedFromVersion=agent_data.get("forked_from_version"),
|
||||
)
|
||||
)
|
||||
|
||||
# Create nodes
|
||||
nodes = agent_data.get("nodes", [])
|
||||
for node in nodes:
|
||||
block_id = node["block_id"]
|
||||
# Ensure the block exists (create placeholder if needed)
|
||||
await ensure_block_exists(db, block_id, known_blocks)
|
||||
|
||||
await db.agentnode.create(
|
||||
data=AgentNodeCreateInput(
|
||||
id=node["id"],
|
||||
agentBlockId=block_id,
|
||||
agentGraphId=graph_id,
|
||||
agentGraphVersion=version,
|
||||
constantInput=Json(node.get("input_default", {})),
|
||||
metadata=Json(node.get("metadata", {})),
|
||||
)
|
||||
)
|
||||
|
||||
# Create links
|
||||
links = agent_data.get("links", [])
|
||||
for link in links:
|
||||
await db.agentnodelink.create(
|
||||
data=AgentNodeLinkCreateInput(
|
||||
id=link["id"],
|
||||
agentNodeSourceId=link["source_id"],
|
||||
agentNodeSinkId=link["sink_id"],
|
||||
sourceName=link["source_name"],
|
||||
sinkName=link["sink_name"],
|
||||
isStatic=link.get("is_static", False),
|
||||
)
|
||||
)
|
||||
|
||||
# Handle sub_graphs recursively
|
||||
sub_graphs = agent_data.get("sub_graphs", [])
|
||||
for sub_graph in sub_graphs:
|
||||
await create_agent_graph(db, sub_graph, known_blocks)
|
||||
|
||||
return graph_id, version
|
||||
|
||||
|
||||
async def create_store_listing(
|
||||
db: Prisma,
|
||||
graph_id: str,
|
||||
graph_version: int,
|
||||
metadata: dict,
|
||||
) -> None:
|
||||
"""Create StoreListing and StoreListingVersion for an agent."""
|
||||
listing_id = metadata["listing_id"]
|
||||
version_id = metadata["store_listing_version_id"]
|
||||
|
||||
# Check if listing already exists
|
||||
existing_listing = await db.storelisting.find_unique(where={"id": listing_id})
|
||||
if existing_listing:
|
||||
print(f" Store listing {listing_id} already exists, skipping")
|
||||
return
|
||||
|
||||
print(f" Creating store listing: {metadata['agent_name']}")
|
||||
|
||||
# Determine if this should be approved
|
||||
is_approved = metadata["is_available"]
|
||||
submission_status = (
|
||||
prisma.enums.SubmissionStatus.APPROVED
|
||||
if is_approved
|
||||
else prisma.enums.SubmissionStatus.PENDING
|
||||
)
|
||||
|
||||
# Create the store listing first (without activeVersionId - will update after)
|
||||
await db.storelisting.create(
|
||||
data=StoreListingCreateInput(
|
||||
id=listing_id,
|
||||
slug=metadata["slug"],
|
||||
agentGraphId=graph_id,
|
||||
agentGraphVersion=graph_version,
|
||||
owningUserId=AUTOGPT_USER_ID,
|
||||
hasApprovedVersion=is_approved,
|
||||
useForOnboarding=metadata["use_for_onboarding"],
|
||||
)
|
||||
)
|
||||
|
||||
# Create the store listing version
|
||||
await db.storelistingversion.create(
|
||||
data=StoreListingVersionCreateInput(
|
||||
id=version_id,
|
||||
version=1,
|
||||
agentGraphId=graph_id,
|
||||
agentGraphVersion=graph_version,
|
||||
name=metadata["agent_name"],
|
||||
subHeading=metadata["sub_heading"],
|
||||
videoUrl=metadata["agent_video"],
|
||||
imageUrls=metadata["agent_image"],
|
||||
description=metadata["description"],
|
||||
categories=metadata["categories"],
|
||||
isFeatured=metadata["featured"],
|
||||
isAvailable=metadata["is_available"],
|
||||
submissionStatus=submission_status,
|
||||
submittedAt=datetime.now() if is_approved else None,
|
||||
reviewedAt=datetime.now() if is_approved else None,
|
||||
storeListingId=listing_id,
|
||||
)
|
||||
)
|
||||
|
||||
# Update the store listing with the active version if approved
|
||||
if is_approved:
|
||||
await db.storelisting.update(
|
||||
where={"id": listing_id},
|
||||
data={"activeVersionId": version_id},
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function to load all store agents."""
|
||||
print("=" * 60)
|
||||
print("Loading Store Agents into Test Database")
|
||||
print("=" * 60)
|
||||
|
||||
db = Prisma()
|
||||
await db.connect()
|
||||
|
||||
try:
|
||||
# Step 0: Initialize agent blocks
|
||||
print("\n[Step 0] Initializing agent blocks...")
|
||||
known_blocks = await initialize_blocks(db)
|
||||
|
||||
# Step 1: Create user and profile
|
||||
print("\n[Step 1] Creating user and profile...")
|
||||
await create_user_and_profile(db)
|
||||
|
||||
# Step 2: Load CSV metadata
|
||||
print("\n[Step 2] Loading CSV metadata...")
|
||||
csv_metadata = await load_csv_metadata()
|
||||
print(f" Found {len(csv_metadata)} store listing entries in CSV")
|
||||
|
||||
# Step 3: Find all JSON files and match with CSV
|
||||
print("\n[Step 3] Processing agent JSON files...")
|
||||
json_files = list(AGENTS_DIR.glob("agent_*.json"))
|
||||
print(f" Found {len(json_files)} agent JSON files")
|
||||
|
||||
# Build mapping from version_id to json file
|
||||
loaded_graphs = {} # graph_id -> (graph_id, version)
|
||||
|
||||
for json_file in json_files:
|
||||
# Extract the version ID from filename (agent_<version_id>.json)
|
||||
version_id = json_file.stem.replace("agent_", "")
|
||||
|
||||
if version_id not in csv_metadata:
|
||||
print(
|
||||
f" Warning: {json_file.name} not found in CSV metadata, skipping"
|
||||
)
|
||||
continue
|
||||
|
||||
metadata = csv_metadata[version_id]
|
||||
print(f"\nProcessing: {metadata['agent_name']}")
|
||||
|
||||
# Load and create the agent graph
|
||||
agent_data = await load_agent_json(json_file)
|
||||
graph_id, graph_version = await create_agent_graph(
|
||||
db, agent_data, known_blocks
|
||||
)
|
||||
loaded_graphs[graph_id] = (graph_id, graph_version)
|
||||
|
||||
# Create store listing
|
||||
await create_store_listing(db, graph_id, graph_version, metadata)
|
||||
|
||||
# Step 4: Refresh materialized views
|
||||
print("\n[Step 4] Refreshing materialized views...")
|
||||
try:
|
||||
await db.execute_raw("SELECT refresh_store_materialized_views();")
|
||||
print(" Materialized views refreshed successfully")
|
||||
except Exception as e:
|
||||
print(f" Warning: Could not refresh materialized views: {e}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"Successfully loaded {len(loaded_graphs)} agents")
|
||||
print("=" * 60)
|
||||
|
||||
finally:
|
||||
await db.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -72,6 +72,7 @@
|
||||
"dotenv": "17.2.3",
|
||||
"elliptic": "6.6.1",
|
||||
"embla-carousel-react": "8.6.0",
|
||||
"flatbush": "4.5.0",
|
||||
"framer-motion": "12.23.24",
|
||||
"geist": "1.5.1",
|
||||
"highlight.js": "11.11.1",
|
||||
|
||||
31
autogpt_platform/frontend/pnpm-lock.yaml
generated
31
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -140,6 +140,9 @@ importers:
|
||||
embla-carousel-react:
|
||||
specifier: 8.6.0
|
||||
version: 8.6.0(react@18.3.1)
|
||||
flatbush:
|
||||
specifier: 4.5.0
|
||||
version: 4.5.0
|
||||
framer-motion:
|
||||
specifier: 12.23.24
|
||||
version: 12.23.24(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -4835,6 +4838,12 @@ packages:
|
||||
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
|
||||
flatbush@4.5.0:
|
||||
resolution: {integrity: sha512-K7JSilGr4lySRLdJqKY45fu0m/dIs6YAAu/ESqdMsnW3pI0m3gpa6oRc6NDXW161Ov9+rIQjsuyOt5ObdIfgwg==}
|
||||
|
||||
flatqueue@3.0.0:
|
||||
resolution: {integrity: sha512-y1deYaVt+lIc/d2uIcWDNd0CrdQTO5xoCjeFdhX0kSXvm2Acm0o+3bAOiYklTEoRyzwio3sv3/IiBZdusbAe2Q==}
|
||||
|
||||
flatted@3.3.3:
|
||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||
|
||||
@@ -12531,8 +12540,8 @@ snapshots:
|
||||
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
|
||||
eslint-plugin-react: 7.37.5(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
|
||||
@@ -12551,7 +12560,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
@@ -12562,22 +12571,22 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.43.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -12588,7 +12597,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.43.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -12864,6 +12873,12 @@ snapshots:
|
||||
keyv: 4.5.4
|
||||
rimraf: 3.0.2
|
||||
|
||||
flatbush@4.5.0:
|
||||
dependencies:
|
||||
flatqueue: 3.0.0
|
||||
|
||||
flatqueue@3.0.0: {}
|
||||
|
||||
flatted@3.3.3: {}
|
||||
|
||||
for-each@0.3.5:
|
||||
|
||||
@@ -4,7 +4,7 @@ import CustomEdge from "../edges/CustomEdge";
|
||||
import { useFlow } from "./useFlow";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useNodeStore } from "../../../stores/nodeStore";
|
||||
import { useMemo, useEffect } from "react";
|
||||
import { useMemo, useEffect, useCallback } from "react";
|
||||
import { CustomNode } from "../nodes/CustomNode/CustomNode";
|
||||
import { useCustomEdge } from "../edges/useCustomEdge";
|
||||
import { useFlowRealtime } from "./useFlowRealtime";
|
||||
@@ -21,6 +21,7 @@ import { useGetV1GetSpecificGraph } from "@/app/api/__generated__/endpoints/grap
|
||||
import { GraphModel } from "@/app/api/__generated__/models/graphModel";
|
||||
import { okData } from "@/app/api/helpers";
|
||||
import { TriggerAgentBanner } from "./components/TriggerAgentBanner";
|
||||
import { resolveCollisions } from "./helpers/resolve-collision";
|
||||
|
||||
export const Flow = () => {
|
||||
const [{ flowID, flowExecutionID }] = useQueryStates({
|
||||
@@ -40,6 +41,7 @@ export const Flow = () => {
|
||||
);
|
||||
|
||||
const nodes = useNodeStore(useShallow((state) => state.nodes));
|
||||
const setNodes = useNodeStore(useShallow((state) => state.setNodes));
|
||||
const onNodesChange = useNodeStore(
|
||||
useShallow((state) => state.onNodesChange),
|
||||
);
|
||||
@@ -48,6 +50,15 @@ export const Flow = () => {
|
||||
);
|
||||
const nodeTypes = useMemo(() => ({ custom: CustomNode }), []);
|
||||
const edgeTypes = useMemo(() => ({ custom: CustomEdge }), []);
|
||||
const onNodeDragStop = useCallback(() => {
|
||||
setNodes(
|
||||
resolveCollisions(nodes, {
|
||||
maxIterations: Infinity,
|
||||
overlapThreshold: 0.5,
|
||||
margin: 15,
|
||||
}),
|
||||
);
|
||||
}, [setNodes, nodes]);
|
||||
const { edges, onConnect, onEdgesChange } = useCustomEdge();
|
||||
|
||||
// We use this hook to load the graph and convert them into custom nodes and edges.
|
||||
@@ -84,6 +95,7 @@ export const Flow = () => {
|
||||
edges={edges}
|
||||
onConnect={onConnect}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
maxZoom={2}
|
||||
minZoom={0.1}
|
||||
onDragOver={onDragOver}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { CustomNode } from "../../nodes/CustomNode/CustomNode";
|
||||
import Flatbush from "flatbush";
|
||||
|
||||
export type CollisionAlgorithmOptions = {
|
||||
maxIterations: number;
|
||||
overlapThreshold: number;
|
||||
margin: number;
|
||||
};
|
||||
|
||||
export type CollisionAlgorithm = (
|
||||
nodes: CustomNode[],
|
||||
options: CollisionAlgorithmOptions,
|
||||
) => CustomNode[];
|
||||
|
||||
type Box = {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
id: string;
|
||||
moved: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
node: CustomNode;
|
||||
};
|
||||
|
||||
function rebuildFlatbush(boxes: Box[]) {
|
||||
const index = new Flatbush(boxes.length);
|
||||
for (const box of boxes) {
|
||||
index.add(box.minX, box.minY, box.maxX, box.maxY);
|
||||
}
|
||||
index.finish();
|
||||
return index;
|
||||
}
|
||||
|
||||
export const resolveCollisions: CollisionAlgorithm = (
|
||||
nodes,
|
||||
{ maxIterations = 50, overlapThreshold = 0.5, margin = 0 },
|
||||
) => {
|
||||
// Create boxes from nodes
|
||||
const boxes: Box[] = new Array(nodes.length);
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
// Use measured dimensions if available, otherwise use defaults
|
||||
const width = (node.width ?? node.measured?.width ?? 0) + margin * 2;
|
||||
const height = (node.height ?? node.measured?.height ?? 0) + margin * 2;
|
||||
|
||||
console.log("width", width);
|
||||
console.log("height", height);
|
||||
const x = node.position.x - margin;
|
||||
const y = node.position.y - margin;
|
||||
|
||||
const box: Box = {
|
||||
minX: x,
|
||||
minY: y,
|
||||
maxX: x + width,
|
||||
maxY: y + height,
|
||||
id: node.id,
|
||||
moved: false,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
node,
|
||||
};
|
||||
|
||||
boxes[i] = box;
|
||||
}
|
||||
|
||||
let numIterations = 0;
|
||||
let index = rebuildFlatbush(boxes);
|
||||
|
||||
for (let iter = 0; iter <= maxIterations; iter++) {
|
||||
let moved = false;
|
||||
|
||||
// For each box, find potential collisions using spatial search
|
||||
for (let i = 0; i < boxes.length; i++) {
|
||||
const A = boxes[i];
|
||||
// Search for boxes that might overlap with A
|
||||
const candidateIndices = index.search(A.minX, A.minY, A.maxX, A.maxY);
|
||||
|
||||
for (const j of candidateIndices) {
|
||||
const B = boxes[j];
|
||||
// Skip self
|
||||
if (A.id === B.id) continue;
|
||||
|
||||
// Calculate center positions
|
||||
const centerAX = A.x + A.width * 0.5;
|
||||
const centerAY = A.y + A.height * 0.5;
|
||||
const centerBX = B.x + B.width * 0.5;
|
||||
const centerBY = B.y + B.height * 0.5;
|
||||
|
||||
// Calculate distance between centers
|
||||
const dx = centerAX - centerBX;
|
||||
const dy = centerAY - centerBY;
|
||||
|
||||
// Calculate overlap along each axis
|
||||
const px = (A.width + B.width) * 0.5 - Math.abs(dx);
|
||||
const py = (A.height + B.height) * 0.5 - Math.abs(dy);
|
||||
|
||||
// Check if there's significant overlap
|
||||
if (px > overlapThreshold && py > overlapThreshold) {
|
||||
A.moved = B.moved = moved = true;
|
||||
|
||||
// Resolve along the smallest overlap axis
|
||||
if (px < py) {
|
||||
// Move along x-axis
|
||||
const sx = dx > 0 ? 1 : -1;
|
||||
const moveAmount = (px / 2) * sx;
|
||||
|
||||
A.x += moveAmount;
|
||||
A.minX += moveAmount;
|
||||
A.maxX += moveAmount;
|
||||
B.x -= moveAmount;
|
||||
B.minX -= moveAmount;
|
||||
B.maxX -= moveAmount;
|
||||
} else {
|
||||
// Move along y-axis
|
||||
const sy = dy > 0 ? 1 : -1;
|
||||
const moveAmount = (py / 2) * sy;
|
||||
|
||||
A.y += moveAmount;
|
||||
A.minY += moveAmount;
|
||||
A.maxY += moveAmount;
|
||||
B.y -= moveAmount;
|
||||
B.minY -= moveAmount;
|
||||
B.maxY -= moveAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
numIterations = numIterations + 1;
|
||||
|
||||
// Early exit if no overlaps were found
|
||||
if (!moved) {
|
||||
break;
|
||||
}
|
||||
|
||||
index = rebuildFlatbush(boxes);
|
||||
}
|
||||
|
||||
const newNodes = boxes.map((box) => {
|
||||
if (box.moved) {
|
||||
return {
|
||||
...box.node,
|
||||
position: {
|
||||
x: box.x + margin,
|
||||
y: box.y + margin,
|
||||
},
|
||||
};
|
||||
}
|
||||
return box.node;
|
||||
});
|
||||
|
||||
return newNodes;
|
||||
};
|
||||
@@ -139,8 +139,11 @@ export const useNodeStore = create<NodeStore>((set, get) => ({
|
||||
get().nodes.map((node) => ({
|
||||
position: node.position,
|
||||
measured: {
|
||||
width: node.data.uiType === BlockUIType.NOTE ? 300 : 500,
|
||||
height: 400,
|
||||
width:
|
||||
node.width ??
|
||||
node.measured?.width ??
|
||||
(node.data.uiType === BlockUIType.NOTE ? 300 : 500),
|
||||
height: node.height ?? node.measured?.height ?? 400,
|
||||
},
|
||||
})),
|
||||
block.uiType === BlockUIType.NOTE ? 300 : 400,
|
||||
|
||||
@@ -150,3 +150,16 @@ input[type="number"]::-webkit-inner-spin-button {
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
/* Google Drive Picker: ensure picker appears above dialogs and can receive clicks */
|
||||
[class*="picker-dialog"] {
|
||||
z-index: 10000 !important;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* When Google picker is open, lower dialog z-index so picker renders on top */
|
||||
body[data-google-picker-open="true"] [data-dialog-overlay],
|
||||
body[data-google-picker-open="true"] [data-dialog-content] {
|
||||
z-index: 1 !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
@@ -14,12 +14,20 @@ const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
/**
|
||||
* Check if an external picker (like Google Drive) is currently open.
|
||||
*/
|
||||
function isExternalPickerOpen(): boolean {
|
||||
return document.body.hasAttribute("data-google-picker-open");
|
||||
}
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
data-dialog-overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
@@ -32,25 +40,59 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-neutral-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] dark:border-neutral-800 dark:bg-neutral-950 sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none dark:ring-offset-neutral-950 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400 dark:focus:ring-neutral-300">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
onPointerDownOutside,
|
||||
onInteractOutside,
|
||||
onFocusOutside,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
data-dialog-content
|
||||
onPointerDownOutside={(e) => {
|
||||
if (isExternalPickerOpen()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
onPointerDownOutside?.(e);
|
||||
}}
|
||||
onInteractOutside={(e) => {
|
||||
if (isExternalPickerOpen()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
onInteractOutside?.(e);
|
||||
}}
|
||||
onFocusOutside={(e) => {
|
||||
if (isExternalPickerOpen()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
onFocusOutside?.(e);
|
||||
}}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-neutral-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] dark:border-neutral-800 dark:bg-neutral-950 sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-white transition-opacity data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2 disabled:pointer-events-none dark:ring-offset-neutral-950 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400 dark:focus:ring-neutral-300">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
),
|
||||
);
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
import { CredentialsInput } from "@/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/modals/CredentialsInputs/CredentialsInputs";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { CircleNotchIcon, FolderOpenIcon } from "@phosphor-icons/react";
|
||||
import { Props, useGoogleDrivePicker } from "./useGoogleDrivePicker";
|
||||
import {
|
||||
Props as BaseProps,
|
||||
useGoogleDrivePicker,
|
||||
} from "./useGoogleDrivePicker";
|
||||
|
||||
export type Props = BaseProps;
|
||||
|
||||
export function GoogleDrivePicker(props: Props) {
|
||||
const {
|
||||
|
||||
@@ -24,6 +24,9 @@ export function GoogleDrivePickerInput({
|
||||
}: GoogleDrivePickerInputProps) {
|
||||
const [pickerError, setPickerError] = React.useState<string | null>(null);
|
||||
const isMultiSelect = config.multiselect || false;
|
||||
const hasAutoCredentials = !!config.auto_credentials;
|
||||
|
||||
// Strip _credentials_id from value for display purposes
|
||||
const currentFiles = isMultiSelect
|
||||
? Array.isArray(value)
|
||||
? value
|
||||
@@ -33,25 +36,34 @@ export function GoogleDrivePickerInput({
|
||||
: [];
|
||||
|
||||
const handlePicked = useCallback(
|
||||
(files: any[]) => {
|
||||
(files: any[], credentialId?: string) => {
|
||||
// Clear any previous picker errors
|
||||
setPickerError(null);
|
||||
|
||||
// Convert to GoogleDriveFile format
|
||||
const convertedFiles = files.map((f) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
mimeType: f.mimeType,
|
||||
url: f.url,
|
||||
iconUrl: f.iconUrl,
|
||||
isFolder: f.mimeType === "application/vnd.google-apps.folder",
|
||||
}));
|
||||
const convertedFiles = files.map((f) => {
|
||||
const file: any = {
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
mimeType: f.mimeType,
|
||||
url: f.url,
|
||||
iconUrl: f.iconUrl,
|
||||
isFolder: f.mimeType === "application/vnd.google-apps.folder",
|
||||
};
|
||||
|
||||
// Include _credentials_id when auto_credentials is configured
|
||||
if (hasAutoCredentials && credentialId) {
|
||||
file._credentials_id = credentialId;
|
||||
}
|
||||
|
||||
return file;
|
||||
});
|
||||
|
||||
// Store based on multiselect mode
|
||||
const newValue = isMultiSelect ? convertedFiles : convertedFiles[0];
|
||||
onChange(newValue);
|
||||
},
|
||||
[isMultiSelect, onChange],
|
||||
[isMultiSelect, onChange, hasAutoCredentials],
|
||||
);
|
||||
|
||||
const handleRemoveFile = useCallback(
|
||||
@@ -79,6 +91,7 @@ export function GoogleDrivePickerInput({
|
||||
views={config.allowed_views || ["DOCS"]}
|
||||
scopes={config.scopes || ["https://www.googleapis.com/auth/drive.file"]}
|
||||
disabled={false}
|
||||
requirePlatformCredentials={hasAutoCredentials}
|
||||
onPicked={handlePicked}
|
||||
onCanceled={() => {
|
||||
// User canceled - no action needed
|
||||
|
||||
@@ -34,7 +34,9 @@ export type Props = {
|
||||
disableThumbnails?: boolean;
|
||||
buttonText?: string;
|
||||
disabled?: boolean;
|
||||
onPicked: (files: NormalizedPickedFile[]) => void;
|
||||
/** When true, requires saved platform credentials (no consent flow fallback) */
|
||||
requirePlatformCredentials?: boolean;
|
||||
onPicked: (files: NormalizedPickedFile[], credentialId?: string) => void;
|
||||
onCanceled: () => void;
|
||||
onError: (err: unknown) => void;
|
||||
};
|
||||
@@ -65,6 +67,7 @@ export function useGoogleDrivePicker(options: Props) {
|
||||
const accessTokenRef = useRef<string | null>(null);
|
||||
const tokenClientRef = useRef<TokenClient | null>(null);
|
||||
const pickerReadyRef = useRef(false);
|
||||
const usedCredentialIdRef = useRef<string | undefined>(undefined);
|
||||
const credentials = useCredentials(getCredentialsSchema(requestedScopes));
|
||||
const queryClient = useQueryClient();
|
||||
const isReady = pickerReadyRef.current && !!tokenClientRef.current;
|
||||
@@ -114,6 +117,7 @@ export function useGoogleDrivePicker(options: Props) {
|
||||
) {
|
||||
const credentialId =
|
||||
selectedCredential?.id || credentials.savedCredentials[0].id;
|
||||
usedCredentialIdRef.current = credentialId;
|
||||
|
||||
try {
|
||||
const queryOptions = getGetV1GetSpecificCredentialByIdQueryOptions(
|
||||
@@ -178,6 +182,20 @@ export function useGoogleDrivePicker(options: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
// If platform credentials are required but none exist, show error
|
||||
if (options?.requirePlatformCredentials) {
|
||||
const error = new Error(
|
||||
"Please connect your Google account in Settings before using this feature.",
|
||||
);
|
||||
toast({
|
||||
title: "Google Account Required",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
if (onError) onError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const token = accessTokenRef.current || (await requestAccessToken());
|
||||
buildAndShowPicker(token);
|
||||
} catch (e) {
|
||||
@@ -242,6 +260,24 @@ export function useGoogleDrivePicker(options: Props) {
|
||||
}
|
||||
|
||||
function buildAndShowPicker(accessToken: string): void {
|
||||
if (!developerKey) {
|
||||
const error = new Error(
|
||||
"Missing Google Drive Picker Configuration: developer key is not set",
|
||||
);
|
||||
console.error("[useGoogleDrivePicker]", error.message);
|
||||
onError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appId) {
|
||||
const error = new Error(
|
||||
"Missing Google Drive Picker Configuration: app ID is not set",
|
||||
);
|
||||
console.error("[useGoogleDrivePicker]", error.message);
|
||||
onError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const gp = window.google!.picker!;
|
||||
|
||||
const builder = new gp.PickerBuilder()
|
||||
@@ -269,19 +305,40 @@ export function useGoogleDrivePicker(options: Props) {
|
||||
});
|
||||
|
||||
const picker = builder.build();
|
||||
|
||||
// Mark picker as open - prevents parent dialogs from closing on outside clicks
|
||||
document.body.setAttribute("data-google-picker-open", "true");
|
||||
|
||||
picker.setVisible(true);
|
||||
}
|
||||
|
||||
function handlePickerData(data: any): void {
|
||||
// Google Picker fires callback on multiple events: LOADED, PICKED, CANCEL
|
||||
// Only remove the marker and process when picker is actually closed (PICKED or CANCEL)
|
||||
const gp = window.google?.picker;
|
||||
if (!gp || !data) return;
|
||||
|
||||
const action = data[gp.Response.ACTION];
|
||||
|
||||
// Ignore LOADED action - picker is still open
|
||||
// Note: gp.Action.LOADED exists at runtime but not in types
|
||||
if (action === "loaded") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the marker when picker closes (PICKED or CANCEL)
|
||||
document.body.removeAttribute("data-google-picker-open");
|
||||
|
||||
try {
|
||||
const files = normalizePickerResponse(data);
|
||||
if (files.length) {
|
||||
onPicked(files);
|
||||
// Pass the credential ID that was used for this picker session
|
||||
onPicked(files, usedCredentialIdRef.current);
|
||||
} else {
|
||||
onCanceled();
|
||||
}
|
||||
} catch (e) {
|
||||
if (onError) onError(e);
|
||||
onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,5 +364,6 @@ export function useGoogleDrivePicker(options: Props) {
|
||||
accessToken: accessTokenRef.current,
|
||||
selectedCredential,
|
||||
setSelectedCredential,
|
||||
usedCredentialId: usedCredentialIdRef.current,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as RXDialog from "@radix-ui/react-dialog";
|
||||
import {
|
||||
CSSProperties,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -20,6 +21,14 @@ interface Props extends BaseProps {
|
||||
withGradient?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an external picker (like Google Drive) is currently open.
|
||||
* Used to prevent dialog from closing when user interacts with the picker.
|
||||
*/
|
||||
function isExternalPickerOpen(): boolean {
|
||||
return document.body.hasAttribute("data-google-picker-open");
|
||||
}
|
||||
|
||||
export function DialogWrap({
|
||||
children,
|
||||
title,
|
||||
@@ -30,6 +39,30 @@ export function DialogWrap({
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const [hasVerticalScrollbar, setHasVerticalScrollbar] = useState(false);
|
||||
|
||||
// Prevent dialog from closing when external picker is open
|
||||
const handleInteractOutside = useCallback(
|
||||
(event: Event) => {
|
||||
if (isExternalPickerOpen()) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
handleClose();
|
||||
},
|
||||
[handleClose],
|
||||
);
|
||||
|
||||
const handlePointerDownOutside = useCallback((event: Event) => {
|
||||
if (isExternalPickerOpen()) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleFocusOutside = useCallback((event: Event) => {
|
||||
if (isExternalPickerOpen()) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function update() {
|
||||
const el = scrollRef.current;
|
||||
@@ -48,12 +81,15 @@ export function DialogWrap({
|
||||
|
||||
return (
|
||||
<RXDialog.Portal>
|
||||
<RXDialog.Overlay className={modalStyles.overlay} />
|
||||
<RXDialog.Overlay data-dialog-overlay className={modalStyles.overlay} />
|
||||
<RXDialog.Content
|
||||
onInteractOutside={handleClose}
|
||||
data-dialog-content
|
||||
onInteractOutside={handleInteractOutside}
|
||||
onPointerDownOutside={handlePointerDownOutside}
|
||||
onFocusOutside={handleFocusOutside}
|
||||
onEscapeKeyDown={handleClose}
|
||||
aria-describedby={undefined}
|
||||
className={cn(modalStyles.content)}
|
||||
className={modalStyles.content}
|
||||
style={{
|
||||
...styling,
|
||||
}}
|
||||
|
||||
@@ -141,6 +141,16 @@ export type GoogleDrivePickerConfig = {
|
||||
allowed_views?: AttachmentView[];
|
||||
allowed_mime_types?: string[];
|
||||
scopes?: string[];
|
||||
/**
|
||||
* Auto-credentials configuration for combined picker + credentials fields.
|
||||
* When present, the picker will include _credentials_id in the output.
|
||||
*/
|
||||
auto_credentials?: {
|
||||
provider: string;
|
||||
type: string;
|
||||
scopes?: string[];
|
||||
kwarg_name: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user