trypost/composer.json
Paulo Castellano 886fca3152 feat: use LLM to polish brand autofill when provider is configured
When config('trypost.ai.text_provider') points to a provider whose
API key is populated (services.gemini.api_key or
services.openai.api_key), the autofill action pipes the homepage
markdown through a BrandAnalyzer agent with structured output to
produce higher-quality values for:

- brand_description — 2-3 sentences summarizing the company based on
  the actual page content, not the raw meta description (which is
  often generic SEO boilerplate)
- brand_tone — classified into one of our seven enum values from the
  writing style on the page
- content_language — detected from the actual content (more reliable
  than <html lang> which is often wrong)
- brand_voice_notes — concrete writing guidelines inferred from the
  site's style, written in the detected language

When the LLM provider is NOT configured (open-source self-hosted
deploys without API keys), the action keeps the existing
deterministic meta-tag-only flow — no crash, no noise, no LLM cost.
When the LLM fails mid-request, we log a warning and fall back to
the meta-tag values so the user still gets something useful.

Stack additions:
- league/html-to-markdown ^5.1 converts the main body to clean
  markdown for the LLM input (truncated to 4000 chars).
- BrandAnalyzer agent (Agent + HasStructuredOutput) with schema
  enums matching our allowed tones and languages.
- resources/views/prompts/brand_analyzer.blade.php holds the
  instructions, including explicit enum lists and examples of good
  voice_notes.

Frontend: Brand.vue now also fills brand_tone and brand_voice_notes
from the response when present.

Tests (+3): LLM-configured happy path, no-credentials fallback
(asserts BrandAnalyzer is never prompted via preventStrayPrompts),
and LLM-exception fallback (meta tags win, brand_tone stays null).
2026-04-16 11:33:02 -03:00

160 lines
5.2 KiB
JSON

{
"$schema": "https://getcomposer.org/schema.json",
"name": "trypost-it/trypost",
"type": "project",
"description": "Open-source social media scheduling for creators and teams",
"keywords": [
"open-source",
"self-hosted",
"laravel",
"social-media",
"social-media-scheduler",
"social-media-management",
"content-scheduling",
"twitter",
"linkedin",
"instagram",
"facebook",
"tiktok",
"youtube",
"threads",
"pinterest"
],
"license": "MIT",
"homepage": "https://trypost.it",
"support": {
"issues": "https://github.com/trypost-it/trypost/issues",
"source": "https://github.com/trypost-it/trypost"
},
"authors": [
{
"name": "Paulo Castellano",
"homepage": "https://github.com/paulocastellano"
}
],
"require": {
"php": "^8.2",
"google/apiclient": "^2.19",
"inertiajs/inertia-laravel": "^3.0",
"intervention/image": "^4.0",
"laravel/ai": "^0.5.1",
"laravel/boost": "^2.0",
"laravel/cashier": "^16.2",
"laravel/framework": "^13.0",
"laravel/horizon": "^5.42",
"laravel/mcp": "^0.6.4",
"laravel/nightwatch": "^1.22",
"laravel/pennant": "^1.23",
"laravel/reverb": "^1.0",
"laravel/socialite": "^5.24",
"laravel/tinker": "^3.0",
"laravel/wayfinder": "^0.1.9",
"league/flysystem-aws-s3-v3": "^3.0",
"league/html-to-markdown": "^5.1",
"posthog/posthog-php": "^4.1",
"predis/predis": "^3.3",
"sendkit/sendkit-laravel": "^1.1",
"socialiteproviders/facebook": "^4.1",
"socialiteproviders/instagram": "^5.1",
"socialiteproviders/linkedin": "^5.0",
"socialiteproviders/pinterest": "^4.3",
"socialiteproviders/tiktok": "^5.2",
"socialiteproviders/twitter": "^4.1",
"symfony/css-selector": "^8.0",
"symfony/dom-crawler": "^8.0"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
"laravel/sail": "*",
"laravel/telescope": "^5.19",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^4.4",
"pestphp/pest-plugin-laravel": "^4.1",
"spatie/laravel-horizon-watcher": "^1.1"
},
"autoload": {
"files": [
"app/Helpers/Upload.php"
],
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"setup": [
"composer install",
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"npm install",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"dev:ssr": [
"npm run build:ssr",
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"php artisan inertia:start-ssr\" --names=server,queue,logs,ssr --kill-others"
],
"lint": [
"pint --parallel"
],
"test:lint": [
"pint --parallel --test"
],
"test": [
"@php artisan config:clear --ansi",
"@test:lint",
"@php artisan test"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
"@php artisan boost:update --ansi"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
]
},
"extra": {
"laravel": {
"dont-discover": [
"laravel/telescope"
]
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}